Skip to content

Instantly share code, notes, and snippets.

@codedokode
Last active December 27, 2021 16:38
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save codedokode/e1d31a31b37d5f635057 to your computer and use it in GitHub Desktop.
Save codedokode/e1d31a31b37d5f635057 to your computer and use it in GitHub Desktop.
DI, IoC, ServiceLocator, Registry

Этот урок переехал в мой гитхаб по адресу: https://github.com/codedokode/pasta/blob/master/arch/di.md

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


Зачем нужны 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 расползается по всем классам проекта, словно вирус.

Dependency Injection

Решением этих проблем является 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 = ...

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

Конструктор используется для обязательных зависимостей, сеттер для опциональных. Обрати вниамние, где здесь инверсия управления (IoC): класс 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) и сам догадывается объект какого класса ему нужен:

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

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

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

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

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

@firestorm23
Copy link

Пишите еще.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment