Правила     Закладки     Карма    Календарь    Журналы    Помощь    Поиск    PDA    Чат   
        СМС-ки
   
Пейджер выключен!
 
Фильтр авторов:    показать 
  скрыть
  Ответ в темуСоздание новой темыСоздание опроса

> Колупаем DI Container, Кто такой и с чем едят.
twin  
 ۩  [x] Дата
Цитировать сообщение

Пользователь сейчас на форуме



Глухой нуб
******

Профиль
Группа: Администратор
Почтальон группы
Сообщений: 15562
Пользователь №: 6543
На форуме: 8 лет, 2 месяца, 3 дня
Карма: 299

Трезвый :
5 лет, 11 месяцев, 13 дней


Немного теории. За одно по паттернам побежимся.

Эволюция развития IoC (Inversion of Control).

DI (Dependency Injection), это по сути передача объекта параметром.
Вот элементарный пример. Допустим есть класс-служба. Или сервис. Вобщем то, что часто нужно:
class Example
{
public function run()
{
echo 'cработало ';
}
}

(new Example)->run();

Теперь есть второй класс, использующий его:
class Example
{
public function run()
{
echo 'cработало ';
}
}


class UsingClass
{
public function __construct()
{
$obj = new Example;
echo 'и тут ';
$obj->run();
}
}


(new UsingClass);



Но тут возникает проблема. Хардкод. Класс UsingClass жестко зависит от Example. Если потребуется изменить его (допустим для юнит-тестирования), то придется лезть в код. А это нарушение SOLID. А классов, использующих Example может быть много, и это будет адъ и израиль.


Решается это созданием фабрики:

// Фабрика
class Factory
{
public $container = [];

public function __construct()
{
$this->container['Example'] = new Example;
// $this->container['SomeClass'] = new SomeClass;
// $this->container['AnotherClass'] = new AnotherClass;

}

public function get($class)
{
return new $this->container[$class];
}
}


class Example
{
public function run()
{
echo 'cработало ';
}
}


class UsingClass
{
public function __construct()
{
$obj = new Factory;
echo 'и тут ';
$obj->get('Example')->run();
}
}


(new UsingClass);

Уже лучше получилось. Подменить классы можно в одном месте. Однако в проекте может быть несколько разных фабрик. Инициализировать их все в методе, значит неимоверно утяжелять код. Можно создать фабрику фабрик. Это весело. :)

Кроме того, хардкод остался, хоть и в одном месте. Подменить класс не получится, без модификации кода класса, а это опять не по правилам. Но есть другое решение.

Легким движением руки фабрика превращается в абстрактную фабрику.

// Абстрактная фабрика
class AbstractFactory
{
public $container = [];

public function set($class)
{
$this->container[$class] = new $class;
}

public function get($class)
{
return new $this->container[$class];
}
}


class Example
{
public function run()
{
echo 'cработало ';
}
}


class UsingClass
{
public function __construct()
{
$obj = new AbstractFactory;
echo 'и тут ';
$obj->set('Example');
$obj->get('Example')->run();
}
}


(new UsingClass);

Теперь можно подменить контейнер, и все классы одним махом заменятся на юнит-тесты допустим. Однако в таком случае мы опять нарушим принцип SOLID. Потому что для этого всё равно нужно будет залазить в код класса фабрики.

И вот тут наступает очередь DI. Мы отправляем весь объект фабрики в класс из контекста:
class AbstractFactory
{
public $container = [];

public function instance($class)
{
$this->container[$class] = new $class;
}

public function get($class)
{
return $this->container[$class];
}
}


class Example
{
public function run()
{
echo 'cработало ';
}
}


class UsingClass
{
public function __construct($factory)
{
echo 'и тут ';
$factory->instance('Example');
$factory->get('Example')->run();
}
}


//////////////////////////////////////////
//Отправляем весь объект фабрики прямо в конструктор.
(new UsingClass(new AbstractFactory));


Это называется "внедрение зависимости через конструктор", или "Constructor Injection". Еще можно внедрять объекты через свойства или сеттеры. Не суть. Суть в том, что теперь не нужно трогать сами классы. Достаточно подменить всю фабрику при вызове.

Но все равно код получается сильно связанным. Опять нарушен один из пунктов SOLID. Потому что работа UsingClass зависит от конкретных классов. А хотелось бы повторного использования.

Так вот, раз мы отправляем эту фабрику из контекста, то можно и настроить её в том же месте. А это мы полную бочку туда катим. Это уже контейнерная перевозка получается. :)

Такая контейнерная перевозка называется DI-контейнер. Самая простая реализация:

class DiContainer
{
public $container = [];

public function set($key, $class)
{
$this->container[$key] = $class;
}

public function get($key)
{
return new $this->container[$key];
}
}


class Example
{
public function run()
{
echo 'cработало ';
}
}


class UsingClass
{
public function __construct($container)
{
echo 'и тут ';
$container->get('work')->run();
}
}


//////////////////////////////////////////

$container = new DiContainer;
$container->set('work', 'Example');

(
new UsingClass($container));

Смысл в том, что теперь можно набить контейнер службами (объектами или интерфейсами библиотек), а инстанцировать их только в момент обращения, создав централизацию управления и не заботясь о лишнем потреблении ресурсов. Кроме того, соблюдается принцип инверсии контроля. Класс UsingClass может работать теперь и с другими классами. Вроде бы одни плюсы...

Но не все так гладко. Завтра продолжу.


--------------------
Если вам недостаточно собственных заблуждений, можно расширить их мнениями экспертов.

Нужно уважать мнение оппонета. Ведь заблуждаться - его святое право.

Зачем ворошить старое, когда можно наворотить новое?

user posted image
PMСайт пользователяICQ
    1   Для быстрого поиска похожих сообщений выделите 1-2 слова в тексте и нажмите сюда Для быстрой цитаты из этого сообщения выделите текст и нажмите сюда
twin  
 ۩  [x] Дата
Цитировать сообщение

Пользователь сейчас на форуме



Глухой нуб
******

Профиль
Группа: Администратор
Почтальон группы
Сообщений: 15562
Пользователь №: 6543
На форуме: 8 лет, 2 месяца, 3 дня
Карма: 299

Трезвый :
5 лет, 11 месяцев, 13 дней


Продолжим поползновения.
Гладко было на бумаге, да забыли про овраги. Такая реализация хороша тогда, когда внедряемый объект изначально готов к использованию и не требует предварительных настроек. Но довольно часто нужно при инициализации объекта передать ему какие то парамеры. А как это сделать, если объект создается только при обращении к нему, непосредственно в использующем контейнер классе?

Эволюция продолжается. Теперь мы будем хранить не идентификатор (в моем примере это "work"), а анонимную функцию, которая при обращении к контейнеру создаст объект с уже подготовленными параметрами. Самым показательным является пример с коннектом. Нужно же как то передать пароль, пользователя, название базы.
class DiContainer
{
public $container = [];

public function set($key, $callable)
{
$this->container[$key] = $callable;
}

public function get($key)
{
return call_user_func($this->container[$key]);
}
}


class DB
{
public $connect;

public function __construct($data)
{
// тут как бы коннект
$this->connect = 'PDO: '. implode(';', $data);
}
}


class UsingClass
{

public function __construct($container)
{
$db = $container->get('db');
echo $db->connect;
}
}


//////////////////////////////////////////

$container = new DiContainer;

$container->set('db', function () {
return new DB(
array(
'host' => 'localhost',
'username' => 'root',
'password' => 'qwerty',
'dbname' => 'some_base'
)
);

});

(
new UsingClass($container));


На самом деле это вовсе не DI-контейнер. Это обычный сервис-локатор. Хотя разница между ними на первый взгляд небольшая, поэтому многие путают одно с другим. Настоящий контейнер, это служба, которая сама разбирается с нужными сервисами. Контейнер должен быть "запрограммирован" так, чтобы мог сам генерировать вложенные зависимости.

Реализация контекста еще больше становится сложной, на лицо начало оверинжениринга и подмена синтаксиса. Некоторые контейнеры используют ArrayAccess, и тогда синтаксис вообще улетает в сторону письменности майя:
    $container['db'] = function () {
return new DB(
array(
'host' => 'localhost',
'username' => 'root',
'password' => 'qwerty',
'dbname' => 'some_base'
)
);

});

С какого перепуга тут взялся массив... Если инициализация объекта $container рядом, мжно интуитивно понять, что имеется ввиду. Но если человек не сталкивался с DI-container и ArrayAccess раньше, то это пердимонокль.

Кроме того, сам контейнер должен уметь выполнять кучу дополнительных плюшек, таких как валидация аргументов, проверка доступности, глобальный доступ, клонирование, инстанцирование с данными из файла (конфиги допустим), постустановка параметров, и пошло, пошло, поехало.

Тут две проблемы. Первая - дополнительный синтаксис и соглашения. Для того, чтобы начать полноценно использовать DI-контейнер, пользователю фреймворка придется перлопатить и запомнить кучу информации из доки.

И вторая. Ресурс. Я посмотрел реализацию сервиса служб у symfony, и мне стало дурно. Это сколько нужно тащить в память, чтобы внедрить контейнером элементарный коннект. :blink:

Некоторые решения основываются на рефлексии, как у ZEND к прмеру. Но даже они сами предупреждают, что использование этой фичи полностью ложится на совесть разработчика, так как нещадно жрет ресурс и время.

Но это для настоящего tru-программиста вовсе не помеха. Он давно уже положил на это болт с крупной конусной резьбой. Ведь мы живем в век быстродействия и железо стоит копейки. Однако забывая, что многие шаред-хостинги ограничивают потребление ресурса.

Тогда придумали кэширование, что еще больше усложняет конструкцию. И так далее и тому подобное. Вобщем Остапа понесло. :) C изобретением DI-контейнера произошел резкий скачек в эволции ООП. Но помоему это скачек в сторону. :)

Резюме.

Но вот в чем парадокс на мой взгляд. Для чего вообще нужна инверсия зависимостей с помощью контейнера? Вот две основные причины:

1. Слабая связанность проекта
2. Легкость тестирования.

И если с первым пунктом можно согласиться, когда создается не модульная (сервисная), а монолитная архитектура. С высокой степенью абстракции и полиморфизма. Когда изменения в каком-нибудь классе может аукнуться на другом конце системы.

В сервисной архитектуре это не обязательный пункт за. Потому что модуль должен быть самодостаточным и не использовать другие модули. А значит нечего и некуда внедрять за его пределами. Внутри же вполне можно обойтись более простыми паттернами, как тот же сервис-локатор.

А со вторым - отдельная история. Это же чистейшее нарушение принципа KISS. используя DI-container для облегчения тестирования, мы переворачиваем все с ног на голову. Усложняем систему, которая будет работать годами, ради упрощения тестов, которые требуются только при разработке и рефакторинге. Очевидно и логично потратить больше времени на изготовление тестов, чем на реализацию самого проекта.

Вывод.
В моем фреймворке скрипт, реализующий DI контейнер, в данный момент не нужен. Обойдемся обычным сервис-локатором. Быть может позже, в виде отдельного компонента, реализуем полноценный DIC.

Его философия идет в разрез с задекларированными в концепции ценностями. Такими как простота, низкое потребление ресурсов и низкий порог вхождения. Использование его внутри системы ударит по производительности. Снаружи никто не запрещал использовать в приложении свою библиотеку, как допустим Arh внизу успел отписаться. :)

Ну или поставить как расширение тот же прыщ или кусок simfony. Тот же Silex не стесняясь это делает, хотя он вроде как отпочковался от Симфонии, не знаю.

Суть в том, что полноценное использование контейнера требуется только на очень больших проектах, где тысячи служб и все их нужно как то упорядочить. Использование этого паттерна в кустарной CMS или другом проекте до среднего, не имеет никакого смысла. Это ООП ради ООП.

UPD. Кстати, так я и не знаю, что считать большим проектом. У меня на работе основной сервис содержит около 3000 файлов, а я не считаю его большим... И как то обходился без DI и даже ООП :D


--------------------
Если вам недостаточно собственных заблуждений, можно расширить их мнениями экспертов.

Нужно уважать мнение оппонета. Ведь заблуждаться - его святое право.

Зачем ворошить старое, когда можно наворотить новое?

user posted image
PMСайт пользователяICQ
    0   Для быстрого поиска похожих сообщений выделите 1-2 слова в тексте и нажмите сюда Для быстрой цитаты из этого сообщения выделите текст и нажмите сюда
twin  
 ۩  Дата
Цитировать сообщение

Пользователь сейчас на форуме



Глухой нуб
******

Профиль
Группа: Администратор
Почтальон группы
Сообщений: 15562
Пользователь №: 6543
На форуме: 8 лет, 2 месяца, 3 дня
Карма: 299

Трезвый :
5 лет, 11 месяцев, 13 дней


Еще один минус забыл написать. Неявность интерфейсов. Попробуем сделать так:

class UsingClass
{
public function __construct($container)
{
// Попробуем посмотреть что в контейнере
var_dump($container);
$db = $container->get('db');
echo $db->connect;
}
}

и получим вот такое:
object(DiContainer)[1]
public 'container' =>
array (size=1)
'db' =>
object(Closure)
[2]


Что в объекте, как его зовут, я его, слющай, первий раз в жизни вижю. :)
Чтобы понять что и как, нужно лезть в исходник. Я не говорю уже про значения.
Вобщем пока я минусов вижу больше чем плюсов. В защиту потерпевшего кто-нибудь скажет? :)


--------------------
Если вам недостаточно собственных заблуждений, можно расширить их мнениями экспертов.

Нужно уважать мнение оппонета. Ведь заблуждаться - его святое право.

Зачем ворошить старое, когда можно наворотить новое?

user posted image
PMСайт пользователяICQ
    0   Для быстрого поиска похожих сообщений выделите 1-2 слова в тексте и нажмите сюда Для быстрой цитаты из этого сообщения выделите текст и нажмите сюда
twin  
 ۩  [x] Дата
Цитировать сообщение

Пользователь сейчас на форуме



Глухой нуб
******

Профиль
Группа: Администратор
Почтальон группы
Сообщений: 15562
Пользователь №: 6543
На форуме: 8 лет, 2 месяца, 3 дня
Карма: 299

Трезвый :
5 лет, 11 месяцев, 13 дней


Собрал я сервис-локатор по образу и подобию. Вобщем вот код с пояснениями:
class ServiceLocator 
{
// Сюда помещаются анонимные функции, инициализирующие объекты
protected $ServiceStorage = [];
// Здесь хранятся ID сервисов, которые должны быть заморожены по принципу singlton
protected $ServiceFrozen = [];
// Здесь хранятся объекты, которые сформированы "глобально" (singlton)
protected static $ObjectStorage = [];

/**
* Записывает сервис в хранилище
*
*
@param string $ServiceId
*
@param callable $callable
*
*
@return void
*/

public function set($ServiceId, $callable)
{// Тут небольшая валидация
$ServiceId = $this->validateService($ServiceId);
$callable = $this->validateCallable($callable);
// помещаем функцию в хранилище
$this->ServiceStorage[$ServiceId] = $callable;
}

/**
* Записывает сервис в глобальное хранилище
*
*
@param string $ServiceId
*
@param callable $callable
*
*
@return void
*/

public function setGlobal($ServiceId, $callable)
{ // Записываем на общих принципах
$this->set($ServiceId, $callable);
// Но делаем пометку, что он заморожен
$this->ServiceFrozen[strtolower($ServiceId)] = true;
}

/**
* Инициализирует и возвращает объект сервиса
*
*
@param string $ServiceId
*
*
@return object
*/

public function get($ServiceId)
{ // Не помешает валидация
$ServiceId = $this->validateService($ServiceId);
// Если объект заморожен, включаем синглтон
if (isset($this->ServiceFrozen[$ServiceId])) {
// При первом обращении инициализируем объект
if (empty(self::$ObjectStorage[$ServiceId])) {
self::$ObjectStorage[$ServiceId] = $this->ServiceStorage[$ServiceId]->__invoke();
}
// При последующих отдаем уже готовый из хранилища
return self::$ObjectStorage[$ServiceId];
// Если не заморожен, то создаем объект при каждом обращении
} elseif (!empty($this->ServiceStorage[$ServiceId])) {
return $this->ServiceStorage[$ServiceId]->__invoke();
}
// Если нет нигде такого объекта - выход из за печки
return false;
}

/**
* Удаляет объект из хранилища
*
*
@param string $ServiceId
*
*
@return void
*/

public function unsetService($ServiceId)
{ // Валидация - святое дело
$ServiceId = $this->validateService($ServiceId);
// Проверим, есть ли такой в хранилище
if (!isset($this->ServiceStorage[$ServiceId])) {
return false;
}
// Просто стираем все
unset($this->ServiceStorage[$ServiceId]);
unset(self::$ObjectStorage[$ServiceId]);

}

/**
* Проверяет корректность ID сервиса
*
*
@param string $ServiceId
*
*
@return string
*/

protected function validateService($ServiceId)
{ // Проверка на вшивость
if (empty($ServiceId) || !is_string($ServiceId)) {
trigger_error('ID service should be a string', E_USER_WARNING);
}
// За одно приведем к одному виду
return strtolower($ServiceId);
}

/**
* Проверяет корректность анонимной функции
*
*
@param callable $callable
*
*
@return callable
*/

protected function validateCallable($callable)
{ // Не подсунули ли чего вместо функции
if (!is_callable($callable)) {
trigger_error('Callable must be a function of anonymity is conferred', E_USER_WARNING);
}

return $callable;
}
}


Работает очень просто. Как и многие другие.
$locator = ServiсeLocator;
// В локальное
$locator->set('MyClass', function() {
return new MyClass;
}
);
// В глобальное
$locator->setGlobal('MyClass', function() {
return new MyClass;
}
);
// Так можно достать
$obj = $locator->get('MyClass');
// Так стирается, чтобы новый записать или память поэкономить
$locator->unsetService('MyClass');


Вобщем то это санминимум, потом в библиотеке можно расширить возможности всякими плюшками и получится кондовый DIC :)


--------------------
Если вам недостаточно собственных заблуждений, можно расширить их мнениями экспертов.

Нужно уважать мнение оппонета. Ведь заблуждаться - его святое право.

Зачем ворошить старое, когда можно наворотить новое?

user posted image
PMСайт пользователяICQ
    0   Для быстрого поиска похожих сообщений выделите 1-2 слова в тексте и нажмите сюда Для быстрой цитаты из этого сообщения выделите текст и нажмите сюда
twin  
 ۩  [x] Дата
Цитировать сообщение

Пользователь сейчас на форуме



Глухой нуб
******

Профиль
Группа: Администратор
Почтальон группы
Сообщений: 15562
Пользователь №: 6543
На форуме: 8 лет, 2 месяца, 3 дня
Карма: 299

Трезвый :
5 лет, 11 месяцев, 13 дней


Дополнил я класс хитрым методом, теперь это полноценный (правда аскетичненький) IoC. Обошелся без рефлексии, честно говоря вообще не понял, нафига она там. Правда при внедрениях не получилось без создания на короткий срок объектов, пиковую память подожрет чутка. Да и фиг с ней. Пользователям фреймворков ведь плевать на память, правда? :D А в самом фреймворке этот метод не используется.

Код здесь, если интересно.

Теперь контейнер может сам собирать новые сервисы из уже существующих. Или внедрять один сервис в другой. Вот так примерно:


Допустим есть два класса

class Example1
{
public $var;
protected $dep;

public function __construct($dep = null)
{
if (is_object($dep)) {
$this->dep = $dep;
}
}


public function run()
{
$this->dep->display($this->var);
}
}


class Example2
{
public function display($var)
{
echo $var;
}
}





$cont = new DiC;
// Создаем первый сервис
$cont->set('service',
function() {
return new Example1;
}
);
// Затем второй
$cont->set('dependence',
function() {
return new Example2;
}
);

// Запихиваем второй в первый, как зависимость через конструктор
$cont->injection('dependence', 'service');

// Вуаля.
$obj = $cont->get('service');
$obj->run();


Можно из двух собрать третий, не изменяя первые два:
    // Собираем третий сервис, запихав второй в первый
$cont->injection('dependence', 'service', 'newService');

// От так.
$obj = $cont->get('newService');
$obj->run();

И еще дополнительная плюшка, можно напихать зависимостей или просто данных, прямо в свойства:
    // Дополнительно устанавливаем значение свойства (можно несколько)
$cont->injection('dependence', 'service', 'newService', ['var' => 'Hello, World!']);

// И вот так еще.
$obj = $cont->get('newService');
$obj->run();


Вот теперь это уже не сервис-локатор. Это гордый IOC. Ну или DIC, как его почему то любят называть. :)


--------------------
Если вам недостаточно собственных заблуждений, можно расширить их мнениями экспертов.

Нужно уважать мнение оппонета. Ведь заблуждаться - его святое право.

Зачем ворошить старое, когда можно наворотить новое?

user posted image
PMСайт пользователяICQ
    0   Для быстрого поиска похожих сообщений выделите 1-2 слова в тексте и нажмите сюда Для быстрой цитаты из этого сообщения выделите текст и нажмите сюда
twin  
 ۩  Дата
Цитировать сообщение

Пользователь сейчас на форуме



Глухой нуб
******

Профиль
Группа: Администратор
Почтальон группы
Сообщений: 15562
Пользователь №: 6543
На форуме: 8 лет, 2 месяца, 3 дня
Карма: 299

Трезвый :
5 лет, 11 месяцев, 13 дней




--------------------
Если вам недостаточно собственных заблуждений, можно расширить их мнениями экспертов.

Нужно уважать мнение оппонета. Ведь заблуждаться - его святое право.

Зачем ворошить старое, когда можно наворотить новое?

user posted image
PMСайт пользователяICQ
    0   Для быстрого поиска похожих сообщений выделите 1-2 слова в тексте и нажмите сюда Для быстрой цитаты из этого сообщения выделите текст и нажмите сюда
  Быстрый ответ
Информация о Госте
Введите Ваше имя
Кнопки кодов
Для вставки цитаты, выделите нужный текст и
НАЖМИТЕ СЮДА
Введите сообщение
Смайлики
:huh:  :o  ;) 
:P  :D  :lol: 
B)  :rolleyes:  <_< 
:)  :angry:  :( 
:unsure:  :blink:  :ph34r: 
     
Показать всё

Опции сообщения  Включить смайлики?
 Включить подпись?
 
1 Пользователей читают эту тему (1 Гостей и 0 Скрытых Пользователей)
0 Пользователей:

Опции темы Ответ в темуСоздание новой темыСоздание опроса