Skip to content

Instantly share code, notes, and snippets.

@codedokode
Last active December 27, 2021 16:38
Show Gist options
  • Save codedokode/e1d31a31b37d5f635057 to your computer and use it in GitHub Desktop.
Save codedokode/e1d31a31b37d5f635057 to your computer and use it in GitHub Desktop.

Revisions

  1. codedokode revised this gist May 22, 2016. 1 changed file with 6 additions and 0 deletions.
    6 changes: 6 additions & 0 deletions DI, IoC.md
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,9 @@
    Этот урок переехал в мой гитхаб по адресу: https://github.com/codedokode/pasta/blob/master/arch/di.md

    Ниже устаревшая версия урока.

    ------------------

    # Зачем нужны Depencdency Injection, IoC, ServiceLocator, Registry (и что это?)

    Проблема, которую мы решаем — связность классов. Если в классе A написано
  2. codedokode revised this gist Oct 3, 2014. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions DI, IoC.md
    Original file line number Diff line number Diff line change
    @@ -129,8 +129,8 @@ http://symfony.com/doc/current/components/dependency_injection/introduction.html

    В ZF2 настройка контейнера возможна еще автоматически: специальный код умеет читать тайпхинты в конструкторе (`DB $db`) и сам догадывается объект какого класса ему нужен:

    http://framework.zend.com/manual/2.0/en/modules/zend.di.introduction.html
    http://zf2.com.ua/doc/48
    - http://framework.zend.com/manual/2.0/en/modules/zend.di.introduction.html
    - http://zf2.com.ua/doc/48

    Наконец, добавлю еще мелочь: вместо указания классов лучше может быть указывать интерфйесы: не `construct(DB $db)` а `construct (DBInterface $db)`. Почему? Потому что в первом случае мы можем передать только DB или его наследника, а во втором случае мы можем передать любой класс, поддерживающий интерфейс. Например, класс-заглушка для тестов.

  3. codedokode revised this gist Oct 3, 2014. 1 changed file with 3 additions and 1 deletion.
    4 changes: 3 additions & 1 deletion DI, IoC.md
    Original file line number Diff line number Diff line change
    @@ -86,6 +86,8 @@ SL определенно лучше Registry. Во-первых, мы види
    - не очевидно, какие сервисы использует класс `A` без полного изучения его кода.
    - `SL` отравляет код. Если ты хочешь использовать класс, которому нужен `SL`, ты тоже должен его начать хранить. Так зависимость от `SL` расползается по всем классам проекта, словно вирус.

    ## Dependency Injection

    Решением этих проблем является DI/IoC (инверсия управления/внедренеи зависимостей). До сих пор у нас класс `A` сам искал и получал нужные ему зависимости:

    $b = new B();
    @@ -107,7 +109,7 @@ SL определенно лучше Registry. Во-первых, мы види

    Либо как-то через интерфейс, я не разбирался как так как это непринципиально.

    Конструктор используется для обязательных зависимостей, сеттер для опциональных. Обрати вниамние, где здесь инверсия управления: класс `A` больше не ищет зависимости, их в него внедряют снаружи. Код поиска зависимостей удалось вынести из класса (он был там не нужен с самого начала!) и он теперь не связан сильно ни с `DB`, ни с `Registry`, ни с `ServiceLocator`. Ты можешь использовать его как хочешь.
    Конструктор используется для обязательных зависимостей, сеттер для опциональных. Обрати вниамние, где здесь *инверсия управления* (IoC): класс `A` больше не ищет зависимости, их в него внедряют снаружи. Код поиска зависимостей удалось вынести из класса (он был там не нужен с самого начала!) и он теперь не связан сильно ни с `DB`, ни с `Registry`, ни с `ServiceLocator`. Ты можешь использовать его как хочешь.

    Остается небольшая проблема: теперь чтобы создать класс A мы должны писать много букв:

  4. codedokode revised this gist Oct 3, 2014. 1 changed file with 8 additions and 0 deletions.
    8 changes: 8 additions & 0 deletions DI, IoC.md
    Original file line number Diff line number Diff line change
    @@ -135,3 +135,11 @@ http://zf2.com.ua/doc/48
    В общем, все эти сложности и непонятные слова служат одной цели: мы хотим использовать все возможности ООП и писать программу как набор повторно используемых и взаимозаменяемых модулей, которые можно соединять между собой. Чтобы мы могли заменить модуль работы с БД на другой, не трогая остальной код. Если проводить аналогии, то правильный ООП — это как универсальная зарядка: любая зарядка может заряжать любой телефон, если у них имеется определенной формы разъем. Или как разъем монитора: ты можешь подключить к компьютеру мониторы разных типов и даже с разным числом пикселей — и все будет работать. Я думаю, любому очевидно что единый стандарт лучше множества закрытых несовместимых решений, верно?

    Ты можешь еще попытаться возразить «но зачем городить весь этот DI, если мне надо логгировать запросы, я просто впишу пару строчек в класс DB. Или опцию в конфиге.». Предлагаю контраргументы тебе привести самому.

    ## Дополнительное чтение

    Все эти штуки описал и разложил по полочкам Фаулер (он очень умный) в своей статье: http://www.martinfowler.com/articles/injection.html (англ)
    Переводы на русский:

    - http://www.argc-argv.com/4_2011/article01.pdf
    - http://yugeon-dev.blogspot.ru/2010/07/inversion-of-control-containers-and_21.html
  5. codedokode revised this gist Oct 3, 2014. 1 changed file with 1 addition and 2 deletions.
    3 changes: 1 addition & 2 deletions DI, IoC.md
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,3 @@

    # Зачем нужны Depencdency Injection, IoC, ServiceLocator, Registry (и что это?)

    Проблема, которую мы решаем — связность классов. Если в классе A написано
    @@ -20,7 +19,7 @@

    ## глобальные переменные

    Ее рассматривается
    Не рассматривается

    ## паттерн Registry

  6. codedokode created this gist Oct 2, 2014.
    138 changes: 138 additions & 0 deletions DI, IoC.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,138 @@

    # Зачем нужны Depencdency Injection, IoC, ServiceLocator, Registry (и что это?)

    Проблема, которую мы решаем — связность классов. Если в классе A написано

    $b = new B;
    $c = B::getC();

    то мы получаем жестко прописанную зависимость A от B на которую не можем повлиять никак (сильную связанность). Мы не можем подсунуть классу A что-то другое вместо B. Мы не можем распространять его отдельно от B.

    Пример: если класс, который через `PDO` что-то делает с базой данных. Мы бы хотели подменить `PDO` на наш класс, который печатает все выполняемые запросы и время их выполнения. Но мы не можем это сделать из-за неграмотной архитектуры. Печаль.

    Другой пример: для тестирования мы хотим подменить реальную базу на класс-заглушку. Это тоже невозможно.

    Третий пример: наш телефон можно заряжать только идущей в комплекте зарядкой (допустим он проверяет серийный номер). Она белая с яблочком. Мы бы хотели использовать розовую, или зарядку с более длинным проводом да еще и более дешевую, но увы, они не подходят.

    В то же время, в ООП есть средства решения проблемы: использование интерфейсов или передача класса-наследника. Но в данной архитектуре они становятся недоступны.

    Решения:

    ## глобальные переменные

    Ее рассматривается

    ## паттерн Registry

    Cоздается класс-массив со статическими методами, в который можно положить объект по имени и достать его:

    Registry::set('db', new PDO);

    // ... пишем в классе A....
    $db = Registry::get('db');

    Это почти то же, что глобальный массив — с той разницей что ты можешь вписать какой-то хитрый код в `get`/`set` например для проверки правильности названия.

    Теперь мы можем вместо класса `B` засунуть в `Registry` что-то другое.

    Но в общем это плохое решение. Почему? Потому что засунув что-то другое в `Registry`, оно будет использовано всеми экземплярами класса `A`. Мы не можем создать один экземпляр с одним значением `db`, а другой с другим. Теперь у нас будут логгироваться все запросы, а не только запросы которые делает один класс. То есть опять же теряем возможность ООП создавать сколько угодно объектов с разными настройками.

    Также, `Registry` никак не проверяет что мы в него засунули. Это тоже нехорошо так как лучше бы он сразу писал про ошибку чем она выяснится в день сдачи проекта.

    `Registry` не документируется. Ты не знаешь что в нем хранится, пока не просмотришь весь код.

    Также, мы не можем иметь 2 и более registry из-за статических методов. Например, мы не можем создать registry с объектами-заглушками для тестов (да, многие из проблем о которых я пишу всплывают именно при попытке наладить тестирование).

    Тем не менее, этот паттерн примеряется из-за своей простоты, смотри ZF например: http://framework.zend.com/manual/1.12/ru/zend.registry.using.html

    ## паттерн ServiceLocator

    Более интересная штука, но в плане эволюции она недалеко ушла от `Registry`. Идея в том, что мы создаем класс, в нем много методов, каждый из которых возвращает какую-то зависимость:

    class ServiceLocator
    {
    public function setDb(PDO $db)
    {
    $this->db = $db;
    }

    public function getDb()
    {
    return $this->db;
    }
    }

    Альтернативная версия SL без сеттеров:

    public function getDb()
    {
    return new PDO;
    }

    Код в классе A теперь выглядит так:

    construct($serviceLocator)
    {
    $this->serviceLocator = $serviceLocator;
    }

    ....
    $db = $this->serviceLocator->getDb();

    SL определенно лучше Registry. Во-первых, мы видим список всех функций в нем и понимаем какие в нем есть сервисы. Во-вторых, он не использует статические методы и мы можем создать несколько SL с разными настройками и соответственно несколько экхемпляров класса A.

    Но он по-прежнему обладает недостатками:

    - у класса `A` теперь есть зависимость от `ServiceLocator` (хотя тот ему нужен лишь для получения нескольких объектов). Ты не можешь распространять класс `A`отдельно.
    - не очевидно, какие сервисы использует класс `A` без полного изучения его кода.
    - `SL` отравляет код. Если ты хочешь использовать класс, которому нужен `SL`, ты тоже должен его начать хранить. Так зависимость от `SL` расползается по всем классам проекта, словно вирус.

    Решением этих проблем является DI/IoC (инверсия управления/внедренеи зависимостей). До сих пор у нас класс `A` сам искал и получал нужные ему зависимости:

    $b = new B();
    ...
    $db = $this->sl->getDb();

    В случае с DI зависимости внедряются (инжектируются, впрыскиваются) в класс извне. Это можно сделать через конструктор класса `А`:

    function construct(DB $db, Logger $logger, Something $smth)
    {
    $this->db = $db
    ....

    Либо через метод-сеттер в классе `А`:

    function setDb(DB $db)
    {
    $this->db = ...

    Либо как-то через интерфейс, я не разбирался как так как это непринципиально.

    Конструктор используется для обязательных зависимостей, сеттер для опциональных. Обрати вниамние, где здесь инверсия управления: класс `A` больше не ищет зависимости, их в него внедряют снаружи. Код поиска зависимостей удалось вынести из класса (он был там не нужен с самого начала!) и он теперь не связан сильно ни с `DB`, ни с `Registry`, ни с `ServiceLocator`. Ты можешь использовать его как хочешь.

    Остается небольшая проблема: теперь чтобы создать класс A мы должны писать много букв:

    $db = new DB();
    $logger = new Logger(.....);
    $smth = new Smth(...);

    $a = new A($db, $logger, $smth);

    Это уныло. Потому в фреймворках есть решения. В Симфони 2 используется DI container: ты описываешь зависимости класса в конфиге, либо кодом, либо аннотациями в коде и при вызове `$container->get('a')` он создает экземпляр по описанным правилам. Инфо:

    http://symfony.com/doc/current/components/dependency_injection/introduction.html

    Кроме объектов, разумеется можно передавать и числа/строки как дополнительные настройки.

    Заметь что контейнер — внешняя вешь по отношению к `A`. Мы не передаем сам контейнер в `A` (иначе это будет `ServiceLocator`). Класс `А` от него не зависит, мы можем в любой момент выкинуть контейнер и создать объект руками или взять другой контейнер от другого производителя. Мы можем описать в конфиге и создать несколько экземпляров `A` с разными настройками. Ты чувствуешь силу ООП и зришь свет разума, падаван?

    В ZF2 настройка контейнера возможна еще автоматически: специальный код умеет читать тайпхинты в конструкторе (`DB $db`) и сам догадывается объект какого класса ему нужен:

    http://framework.zend.com/manual/2.0/en/modules/zend.di.introduction.html
    http://zf2.com.ua/doc/48

    Наконец, добавлю еще мелочь: вместо указания классов лучше может быть указывать интерфйесы: не `construct(DB $db)` а `construct (DBInterface $db)`. Почему? Потому что в первом случае мы можем передать только DB или его наследника, а во втором случае мы можем передать любой класс, поддерживающий интерфейс. Например, класс-заглушка для тестов.

    В общем, все эти сложности и непонятные слова служат одной цели: мы хотим использовать все возможности ООП и писать программу как набор повторно используемых и взаимозаменяемых модулей, которые можно соединять между собой. Чтобы мы могли заменить модуль работы с БД на другой, не трогая остальной код. Если проводить аналогии, то правильный ООП — это как универсальная зарядка: любая зарядка может заряжать любой телефон, если у них имеется определенной формы разъем. Или как разъем монитора: ты можешь подключить к компьютеру мониторы разных типов и даже с разным числом пикселей — и все будет работать. Я думаю, любому очевидно что единый стандарт лучше множества закрытых несовместимых решений, верно?

    Ты можешь еще попытаться возразить «но зачем городить весь этот DI, если мне надо логгировать запросы, я просто впишу пару строчек в класс DB. Или опцию в конфиге.». Предлагаю контраргументы тебе привести самому.