Skip to content

Latest commit

 

History

History
executable file
·
517 lines (372 loc) · 50.9 KB

di.md

File metadata and controls

executable file
·
517 lines (372 loc) · 50.9 KB

Зачем нужно Dependency Injection (и что это?)

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

Принцип одной обязанности

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

Принцип единственной обязанности заключается в том, что каждый класс занимается только своим делом. Например, один класс отвечает только за взаимодействие с БД, другой за проверку правильности введенных данных, третий за выставление и проверку кук для авторизованных пользователей.

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

Зависимости

Если мы разбили код на классы, то может оказаться, что одному из них нужен другой. Ну например, если у нас есть класс авторизации, а в нем метод, проверяющий логин и пароль пользователя на правильность, которые хранятся в базе данных, ему понадобится класс, достающий их оттуда.

В таких случаях говорят, что класс A зависит от класса B, или B является зависимостью (dependency) класса A.

Зависимости бывают обязательные (без которых класс работать не может) и необязательные. Пример необязательной зависимости - это класс-логгер. Он может быть нужен только при отладке кода, а на боевом сервере не требуется.

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

Чем плохи глобальные переменные

Глобальная переменная - это переменная, которая всегда существует в одном экземпляре и доступна из любого места кода. Статические поля классов тоже существуют в одном экземпляре, потому их можно рассматривать как разновидность глобальной переменной.

На первый взгляд они хорошо подходят для хранения таких вещей, как: настройки (например имя и пароль к базе данных), или язык выводимых сообщений на многоязычном сайте, но если присмотреться, то это почти всегда плохое решение.

То, что глобальная переменная доступна из любого места кода - это плохо. Это значит, что при изменении кода, связанного с этой переменной, нам придется делать поиск по всему коду и изучать каждый случай использования. Ну например, мы хотим понять, откуда берется значение в глобальной переменной, и обнаружили, что оно задается в нескольких разных файлах. Как понять, чему же эта переменная равна в нашем случае? Скорее всего, придется разбирать код в этих файлах, смотреть откуда он вызывается, в общем, тратить много времени.

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

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

Глобальные переменные создают нежелательные побочные эффекты. Допустим, у нас есть многоязычный сайт, работающий на 2 доменах (example.com на английском и example.ru на русском). В начале обработки запроса мы по названию домена определяем язык пользователя и сохраняем в глобальную переменную $language. А различные функции в зависимости от ее значения возвращают результат на нужном языке. Ну например, метод отправки письма об успешной регистрации пользователю выглядит так:

public function sendRegistrationMessage($userEmail) 

Глядя на заголовок, невозможно догадаться что на результат его работы влияет еще и $language. И когда мы захотим вызвать эту функцию, не задав перед этим значение глобальной переменной, мы получим ошибку. Или хуже, письмо отправится, но на неправильном языке. Или, допустим, мы хотим отправить письмо на английском, независимо от текущего глобального языка. Нам придется писать такой громоздкий код:

global $language;
$oldLanguage = $language;
$language = 'en';
$mailer->sendRegistrationMessage('user@example.com');
$language = $oldLanguage;

В этой ситуации код был бы понятнее, если бы язык передавался явно через аргументы функции, либо передавался через конструктор класса, в котором находится метод.

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

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

Чем плохи классы из статических методов

Вот недостатки классов, где все методы статические (в сравнении с классами из обычных методов, где надо сначала создать объект и только потом вызывать у него методы):

  • так как свойства у таких классов тоже статические, то нельзя создать второй объект с немного другими значениями свойств. Ну например, нельзя создать 2 класса-валидатора, выдающих сообщения об ошибке на разных языках, нельзя создать объект, работающий с другими настройками базы данных. Нельзя создать временный объект, попользоваться и выбросить его, не повлияв на остальной код.
  • статические методы не могут обращаться к $this, и могут хранить данные только в статических полях, которые по сути аналогичны глобальными переменным, и их изменение влияет на весь остальной код.
  • статический метод можно вызвать из любого места кода. А вот в случае использования обычных методов, их можно вызывать только там, куда передан объект. Это позволяет ограничить зону его использования и намекнуть, что работать с объектом нужно только тут.
  • связи между классами жестко прописаны в коде. Когда один класс A вызывает статический метод другого класса B, мы не можем как-то указать классу A использовать класс C вместо B.
  • связи между классами не очевидны. Чтобы понять, что класс A использует класс B, надо изучить его код целиком. Мы не можем определить это только по конструктору или заголовкам методов.

Допустим, что мы сделали метод проверки правильности данных пользователя статическим. Он принимает на вход объект пользователя и возвращает массив сообщений об ошибках:

class UserValidator
{
    public static function validate(User $user) { ... }
}

Далее, мы пишем метод регистрации пользователя, который в том числе вызывает метод валидации:

class RegistrationService
{
    public static function register(User $user)
    {
        $errors = UserValidator::validate($user);
        ...
    }
}

Теперь мы хотим добавить проверку, что email пользователя уникальный и не принадлежит другому зарегистрированному пользователю. Для этого классу UserValidator надо обратиться к базе данных. Но как ему получить объект UserTableGateway, который содержит методы поиска в таблице пользователей? Нам придется либо передавать его как аргумент в validate() (а значит и в register()), либо хранить где-то в статическом поле класса UserValidator. В первом случае нам приходится править оба класса вместо одного (хотя RegistrationService не занимается валидацией). Во втором мы получаем глобальную переменную и побочные эффекты.

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

Однако, иногда лучше использовать именно статические методы. Они подходят для простых функций, не привязанных ни к какому объекту, не использующих поля класса, результат которых зависит только от переданных аргументов. Например, функция перевода градусов в радианы, функция, генерирующая случайный пароль или определяющая расширение по имени файла. Если такую функцию нельзя отнести к какому-то классу, ее помещают в вспомогательный класс с названием вроде Util, StringUtil или MathUtil (паттерн Utility Class).

Также, статические методы можно использовать как дополнительные конструкторы в классе (паттерн Static Constructor).

Внедрение зависимостей

Внедрение зависимостей (dependency injection) - это передача зависимостей в класс снаружи. Внедрять их можно через конструктор или отдельный метод-сеттер ("сеттером" его называют, так как его название начинается с set - "установить", "задать").

Хорошая функция получает нужные ей значения через аргументы, а хороший класс получает свои обязательные зависимости через конструктор. Это имеет такие преимущества:

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

Необязательные зависимости обычно либо передают через отдельный метод-сеттер (вроде setLogger(Logger $logger)), либо через конструктор с указанием null в качестве значения по умолчанию.

Вот пример передачи зависимостей классу-валидатору:

class UserValidator
{
    public function __construct(UserTableGateway $userTableGateway) { ... }
    public function setLogger(Logger $logger) { ... }
    ...
}

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

$realUserTableGateway = new UserTableGateway(...);
$realValidator = new UserValidator($realUserTableGateway);

$testUserGateway = new TestUserGateway();
$testValidator = new UserValidator($testUserGateway);

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

В данном случае TestUserGateway должен быть наследником UserTableGateway, чтобы пройти проверку в тайп-хинте. Чтобы избежать необходимости применять наследование, мы можем добавить интерфейс UserTableGatewayInterface и указать его как тайп-хинт.

Поскольку тут зависимости передаются снаружи, а не класс сам ищет их, это называется инверсия управления (IoC, inversion of control) ("Инверсия" значит "замена на противоположный вариант"). Такой подход дает нам максимальную гибкость в том, как создавать и связывать между собой объекты разных классов.

Увы, иногда можно встретить менее удачные способы получения зависимостей. Разберем их недостатки.

Registry и чем он плох

Registry (реестр) - это класс, как правило, со статическими методами, который хранит в себе другие объекты. В начале скрипта мы помещаем объекты в Registry, и затем другие классы могут оттуда их брать. Как правило, класс Registry доступен глобально, из любой точки кода, за счет использования статических методов. Например:

// Создаем и помещаем зависимость в registry
// Объект соединения с БД
$pdo = new PDO(...);
Registry::setPdo($pdo);

// Класс работы с таблицей пользователей
$userDataGateway = new UserDataGateway;
Registry::setUserDataGateway($userDataGateway);

$validator = new UserValidator;
Registry::setValidator($validator);

Когда классу UserValidator нужно что-то получить из БД, он находит объект для работы с ней в Registry:

class UserValidator
{
    public function validate(User $user)
    {
        $udg = Registry::getUserDataGateway();
        if ($udg->isEmailInDb($user->email)) {
            ...

Недостатки Registry:

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

Этот паттерн применялся в Zend Framework 1: http://framework.zend.com/manual/1.12/ru/zend.registry.using.html

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

Чем плох ServiceLocator

ServiceLocator - это объект, способный находить или создавать другие объекты (сервисы). Мы создаем объект ServiceLocator, заполняем его объектами (либо указываем, как их создавать), и передаем классу в конструктор, а класс может взять из него то, что ему требуется:

$sl = new ServiceLocator;

$pdo = new PDO(...);
$sl->setPdo($pdo);

// передаем ServiceLocator, из которого UDG возьмет объект PDO
$userDataGateway = new UserDataGateway($sl);
$sl->setUserDataGateway($userDataGateway);

А вот, как он используется:

class UserDataGateway
{
    private $sl;

    public function __construct(ServiceLocator $sl)
    {
        $this->sl = $sl;
    }

    public function getUserById($id)
    {
        $pdo = $this->sl->getPdo();
        ...

Этот подход исправляет часть недостатков Registry (например, он доступен только в тех классах, куда мы его передали), но имеет такие недостатки:

  • зависимости класса не видны. Непонятно, какие сервисы должны быть добавлены в ServiceLocator
  • ServiceLocator отравляет код и становится лишней зависимостью каждого класса

Однако, использование ServiceLocator иногда оправданно. В некоторых фреймворках объект ServiceLocator (в качестве которого выступает DI Container) передается в конструктор контроллера, чтобы тот мог найти и вызвать нужные ему сервисы. Это проще, чем передавать каждый по отдельности в конструктор. Контроллер является чем-то вроде стартовой точки обработки запроса, так что там это может быть приемлемо.

Кстати, самой простой реализацией ServiceLocator может быть простой массив:

$serviceLocator = [];
// Добавление сервиса
$serviceLocator['service'] = new Service(...);
// Получение сервиса
$service = $serviceLocator['service'];

Но на мой взгляд, это плохо соответствует ООП и имеет недостатки: мы, например, не можем ставить тайп хинты на него.

Чем хорош DI container

При использовании DI в небольшом приложении мы можем вручную создать все нужные объекты в самом начале:

$a = new A;
$b = new B($a);
$c = new C;

Но когда классов становится много, код усложняется. Мы должны создавать объекты в правильном порядке, и нам приходится создавать все объекты, даже если часть из них нам далее не понадобится. Для решения этой проблемы придуманы DI контейнеры. Это класс, отвечающий за создание нужных нам сервисов. В нем мы описываем классы и их зависимости (например, как функции для создания каждого объекта), и после этого можем получать эти объекты из контейнера. Один из простых контейнеров - это Pimple (англ.). Вот пример кода с его использованием:

$container = new Pimple\Container;

// Описываем как создавать объект PDO
$container['pdo'] = function ($container) {
    return new PDO(...);
};

// Описываем как создать объект работы с БД, зависящий от PDO
$container['userDataGateway'] = function ($container) {
    return new UserDataGateway($container['pdo']);
};

// Получаем нужный нам объект
$udg = $container['userDataGateway'];

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

Иногда в контейнер, кроме классов, кладут еще настройки приложения, например, настройки соединения с БД.

Здесь у нас для каждого класса регистрируется соответствующий сервис в контейнере, но вообще, для одного класса можно создать несколько сервисов с разными именами и настройками. Ну например, у нас может быть 2 объекта PDO с разными настройками, если мы по каким-то причинам используем 2 разные базы данных.

DI container внешне напоминает ServiceLocator. Но если присмотреться, то между ними есть принципиальная разница: при использовании ServiceLocator все классы начинают зависеть от него, так как мы передаем им в конструктор этот самый ServiceLocator. Когда класс хочет получить объект, он сам вызывает методы ServiceLocator. В случае же с DI container классы о нем ничего не знают, и получают от него в конструктор только нужные им объекты-сервисы. Потому DI container не имеет недостатков ServiceLocator. Ты можешь использовать DI контейнер с любыми классами, в них не надо специально что-то дописывать.

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

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

Есть много реализаций DI container, например более сложный и мощный Symfony DI Container (англ.), где можно описывать зависимости классов в файлах конфигурации.

В некоторых фреймворках, правда, объект DI container передается в контроллер (фактически получается паттерн ServiceLocator) вместо того, чтобы передавать явно только нужные контроллеру сервисы. Это делается, чтобы сэкономить время и не прописывать каждый контроллер в DI контейнере.

Autowiring

Некоторые DI контейнеры могут также автоматически находить нужные классу зависимости, например, по тайп-хинтам в конструкторе. Вот здесь описана опция autowiring в Symfony (англ.).

К примеру, если у класса A в конструкторе стоит тайп-хинт с указанием класса B: __construct(B $b), и в DI контейнере описан ровно один объект класса B, тот может автоматически передать его как зависимость.

Написание своего собственного DI контейнера

В реальном проекте лучше использовать готовый DI контейнер, но в учебных целях можно попробовать написать его самому. DI контейнер - это такой объект, который "знает", как создавать другие объекты (которые называют сервисы) и создает их при необходимости. Вот как используется DI контейнер:

  • в начале программы создаем объект контейнера
  • регистрируем в нем сервисы (то есть описываем, как создать тот или иной объект-сервис, какой у него класс, какие аргументы надо передать в конструктор)
  • запрашиваем нужные нам сервисы из контейнера

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

Иногда контейнер наделяют дополнительными функциями, например, хранить параметры конфигурации (вроде хоста, имени и пароля для доступа к БД). Параметры помещаются в контейнер в начале программы и могут использоваться при создании сервисов. В нашем контейнере этого не будет.

Наш простой контейнер будет содержать всего лишь 2 метода:

  • register($name, callable $factory) - регистрирует в контейнере новый сервис под именем $name, объект которого создается с помощью вызова функции $factory (factory переводится как "фабрика", фабрикой называют функцию или класс, задачей которого является создание объектов). Попытка зарегистрировать 2 сервиса под одинаковым именем должна вызывать выброс исключения DIContainerException, так как у каждого сервиса должно быть свое, уникальное имя.
  • get($name) - получает объект сервиса, зарегистрированного под именем $name. Объект создается с помощью указанной при регистрации функции $factory. При втором вызове должен возвращать ранее созданный объект, а не создавать новый. Если указано имя несуществующего сервиса, то выбрасывает исключение DIContainerException.

Функция-фабрика выглядит так: function (DIContainer $container) { ... }. В качестве аргумента она получает контейнер (и может запрашивать из него нужные сервисы-зависимости), и ее задача - создать и вернуть объект сервиса.

Вот пример использования нашего контейнера:

$container = new DIContainer;

// Регистрируем "сервис", который является просто массивом, хранящим данные
// конфига. Можно было бы сделать класс-обертку для этого массива, но ради 
// упрощения кода не будем этого делать
$container->register('config', function (DIContainer $container) {
    $configPath = __DIR__ . '/config.ini';
    $config = parse_ini_file($configPath);
    return $config;
});

// Регистрируем объект PDO как сервис
$container->register('PDO', function (DIContainer $container) {
    // Для соединения с БД нам нужны данные из конфига, получаем их
    // из контейнера
    $config = $container->get('config');
    $dsn = "mysql:host={$config['host']};dbname={$config['dbname']};charset=utf8";
    $pdo = new PDO($dsn, $config['user'], $config['password'], [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
    ]);
    return $pdo;
});

// Регистрируем сервис, который зависит от PDO
$container->register('UserDataGateway', function (DIContainer $container) {
    return new UserDataGateway($container->get('PDO'));
});

// теперь мы можем в любой момент получить нужный нам сервис
$userGateway = $container->get('UserDataGateway');

Попробуем написать класс контейнера:

class DIContainer
{
    /* 
        Здесь нужно добавить поле, которое будут хранить список зарегистрированных 
        сервисов, например в виде массива ['имя' => функция-фабрика] и поле, 
        которое будет хранить список созданных объектов сервисов, например, 
        в формате ['имя' => объект]
     */
    
    /**
     * Регистрирует новый сервис под именем $name, объект сервиса 
     * создается функцией-фабрикой $factory
     */
    public function register($name, callable $factory)
    {
        /*  
            - Проверяем, что сервиса с таким именем нет в списке 
                зарегистрированных сервисов
            - Добавляем имя сервиса и фабрику в список сервисов
        */
    }
    
    /**
     * Возвращает сервис под именем $name
     */
    public function get($name)
    {
        /*
            - Если у нас есть ранее созданный объект-сервис с таким именем, возвращаем его
            - Если нет, то ищем функцию-фабрику для такого сервиса
            - Если ее нет, то указано неверное имя сервиса, бросаем исключение
            - Вызываем функцию-фабрику, и сохраняем объект сервиса 
        */        
    }
}

Дописать недостающие части кода оставим в качестве домашнего задания читателю.

Пример использования преимуществ DI

Попробуем сделать пример кода, который использует возможности DI. Допустим, у нас есть класс, который загружает данные из интернета и как-то их обрабатывает (например, это скрипт, который раз в сутки загружает курсы валют с официального сайта центрального банка и сохраняет в нашу базу данных). Если писать код без использования DI, он будет выглядеть примерно так:

class RateLoader
{
    public function load() { ... }
}

и используется он так:

$loader = new RateLoader();
$loader->load();

Допустим, нам захотелось протестировать этот класс и для теста нам надо, чтобы он не скачивал данные с удаленного сайта, а использовал бы подготовленный нами файл. Или например, мы хотим для отладки сделать логгирование - выводить какие именно данные скачиваются. Но сделать этого без правки кода нельзя, так как класс монолитный и заменить компонент, отвечающий за работу с сетью, невозможно. Исправить эту проблему можно с помощью Dependency Injection. Для этого мы выносим часть кода, которая отвечает за скачивание данных из сети, в отдельный класс HttpClient. Разумеется, мы также вынесем в зависимости класс RateDBGateway для работы с базой данных. Вот как теперь выглядит код:

class HttpClient
{
    /** скачивает файл по указанному URL */
    public function download($url) { .. }
}

class RateLoader
{
    public function __construct(HttpClient $httpClient, RateDBGateway $gateway) { .. }
    public function load();
}
// Использование
$httpClient = new HttpClient;
$rdg = new RateDBGateway(..);
$loader = new RateLoader($httpClient, $rdg);
$loader->load();

Теперь мы можем при создании объекта HttpClient указать ему какие-то дополнительные настройки. Также, благодаря разделению на 3 класса код стал немного проще. Но из-за тайп-хинта в конструкторе класса RateLoader мы можем передать только объект класса HttpClient или его наследника. А для загрузки данных из файла вместо сети нам нужна возможность передать другой класс. Эту проблему можно решить, используя интерфейсы. Интерфейс - это набор требований (в виде списка методов), которым должен соответствовать класс. С его помощью можно пометить разные классы, обладающие какой-то способностью. Оф. мануал по интерфейсам, мой урок про интерфейсы.

В нашем случае мы создаем интерфейс, описывающий классы, которые умеют загружать данные по URL (или делать вид, что загружают, и подсовывать данные из файла). Эти классы должны содержать метод download, который мы и опишем:

interface DownloaderInterface
{
    public function download($url);
}

Класс HttpClient уже соответствует этому интерфейсу (так как в нем есть нужный метод), и нам остается только указать на это с помощью конструкции implements. Также, мы напишем класс FileLoader, который вместо реального скачивания берет данные из файла. И укажем в конструкторе класса RateLoader название нашего интерфейса:

class FileLoader implements DownloaderInterface
{
    public function __construct($fileName) { ... }
    public function download($url) { ... }
}

class HttpClient implements DownloaderInterface
{
    /** скачивает файл по указанному URL */
    public function download($url) { .. }
}

class RateLoader
{
    // указываем интерфейс в качестве тайп-хинта вместо конкретного имени класса
    public function __construct(DownloaderInterface $downloader, RateDBGateway $gateway) { .. }
    public function load();
}

// Использование для скачивания с сети
$httpClient = new HttpClient;
$rdg = new RateDBGateway(...);
$loader = new RateLoader($httpClient, $rdg);
$loader->load();

// использование для загрузки из файла
$fileLoader = new FileLoader('/tmp/data.json');
$loader = new RateLoader($fileLoader, $rdg);
$loader->load();

Но и это еще не все. Теперь мы можем использовать и другие паттерны, например, паттерн Декоратор. Увы, примеры использования этого паттерна в статьях часто запутанные, потому попробую привести простой пример. Допустим, мы обнаружили что удаленный сервер, с которого скачиваются данные, иногда выдает ошибку, и мы бы хотели делать в таких случаях повторную попытку после небольшой паузы. Конечно, можно внести изменения в класс HttpClient, но иногда это не хочется делать, а иногда это класс из сторонней библиотеки и поменять его нельзя. В таком случае мы можем написать класс-декоратор, который будет вызывать HttpClient и при ошибке делать повторные запросы. Мы сделаем так, чтобы он тоже соответствовал интерфейсу DownloaderInterface и в этом случае мы можем передать наш декоратор вместо клиента, не меняя ни одного существующего класса. Вот как он будет выглядеть:

// Класс-декоратор для повторения попыток загрузки
class RetryDownloader implements DownloaderInterface
{
    public function __construct(DownloaderInterface $downloader, $pauseSeconds = 10, $retryCount = 3) { ... }
    public function download($url) { ... }
}

// Использование декоратора 
$client = new HttpClient;
$retryDownloader = new RetryDownloader($client, 20, 5);
$rdg = new RateDBGateway(...);
$rateLoader = new RateLoader($retryDownloader, $rdg);

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

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

Внедрение через интерфейс

Выше описано 2 варианта внедрения зависимостей - через конструктор и через сеттер. Есть еще третий, экзотичный, "внедрение через интерфейс" - это почти то же самое, что и способ с сеттером, только этот сеттер дополнительно описывается с помощью интерфейса. Допустим, у нас есть класс A, зависящий от B и C. Мы можем описать этот факт с помощью интерфейсов. Создадим интерфейс InjectB, означающий, что класс зависит от B, и позволяющий его внедрить:

interface InjectB
{
    public function setB(B $b);
}

Аналогично опишем и InjectC. Теперь мы готовы написать класс A, реализующий эти интерфейсы:

class A implements InjectB, InjectC
{
    public function setB(B $b) { ... }
    public function setC(C $c) { ... }
}

Как видно, теперь зависимости класса указаны с помощью списка интерфейсов в заголовке. В чем преимущества этого подхода, я предлагаю поискать в первоисточнике - статье Мартина Фаулера (англ.): http://www.martinfowler.com/articles/injection.html#InterfaceInjection . Как я понимаю, при использовании совместно с DI контейнером тот по интерфейсам может понять, какие зависимости нужны классу, и передать их ему.

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

Все эти штуки описал и разложил по полочкам Фаулер (он очень умный) в своей статье: http://www.martinfowler.com/articles/injection.html (англ.) Перевод на русский: http://yugeon-dev.blogspot.ru/2010/07/inversion-of-control-containers-and_21.html

Связаться с автором

codedokode (あ) gmail.com