Что такое dependency injection






Если вы занимаетесь разработкой программного обеспечения хотя бы какое-то время, вы, скорее всего, уже натыкались на термин «dependency injection» или «внедрение зависимости». Если вы ещё только-только присоединились к миру разработки ПО, то, вероятно, пока старались избегать попыток разобраться с этой концепцией. Но что бы вам ни казалось, внедрение зависимости является отличным инструментом при разработке поддерживаемого и тестируемого кода. В сегодняшней статье автор попытается рассказать, что же такое dependency injection настолько просто, насколько он сможет.

Injection

Рассмотрим простой кусок кода:

class Photo {
   /**
    * @var PDO Подключение к БД
    */
   protected $db;
   /**
    * Конструктор
    */
   public function __construct()
   {
      $this->db = DB::getInstance();
   }
}

На первый взгляд, всё просто и безобидно. Да, если не обращать внимания на тот факт, что в класс уже жёстко внедрена зависимость в виде подключения к БД. Что мы будем делать, если понадобиться хранить фотографии не в базе данных, а, скажем, на диске? Да и вообще, подумайте, с какого перепугу объект фотографии должен осуществлять изнутри себя какое-либо взаимодействие с внешним миром? Разве это не противоречит принципу разделения ответственности? Однозначно противоречит. Объект не должен  знать ни о чём другом, кроме того, что касается непосредственно самого объекта.

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

Итак, давайте переконструируем наш объект Photo так, чтобы можно было снаружи определять его поведение в отношении внешнего хранилища. Сделать это можно двумя способами. Давайте рассмотрим каждый из них.

Внедрение в конструкторе

class Photo {
   /**
    * @var PDO Подключение к БД
    */
   protected $db;

   /**
    * Конструктор
    *
    * @param PDO $dbConn Подключение к БД
    */
   public function __construct($dbConn)
   {
      $this->db = $dbConn;
   }
}
$photo = new Photo($dbConn);

В приведённом примере внедрение зависимости происходит в момент создания экземпляра объекта, вместо того, чтобы жёстко внедрять зависимость в классе.

Внедрение методом

class Photo {
   /**
    * @var PDO Подключение к БД
    */
   protected $db;

   /**
    * Конструктор
    */
   public function __construct($dbConn) {};

   /**
    * Внедряет подключение к БД
    *
    * @param PDO $dbConn Подключение к БД
    */
   public function setDB($dbConn)
   {
      $this->db = $dbConn;
   }
}
$photo = new Photo;
$photo->setDB($dbConn);

В результате небольшого исправления объекты класса теперь не зависят от подключения к БД в момент создания. При этом сам класс стал удобней для юнит-тестирования. Ко всему прочему стало возможным теперь в любой момент менять подсистему хранения наших фотографий.

Проблема

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

$photo = new Photo;
$photo->setDB($dbConn);
$photo->setConfig($config);
$photo->setResponse($response);

Упс. Вместо того, чтобы облегчить жизнь, мы добавили лишней сложности. Если раньше пользователь просто создавал объект и работал с ним, то теперь ему придётся изрядно потанцевать с бубном, прежде чем двигаться дальше.

Решение

Решение заключается в том, чтобы создать отдельный класс-контейнер, который будет делать за нас всю тяжёлую работу. Если вы когда-нибудь сталкивались с термином Inversion of Control (IoC) — инверсия управления, то вы, вероятно, в курсе, о чём речь.

Класс-контейнер хранит информацию обо всех зависимостях, присутствующих в проекте, и выполняет всю работу по созданию зависимых объектов, а также внедрению в них зависимостей. Можно разными способами реализовать этот подход, мы же пока ясности ради реализуем всё прямо в методах класса-контейнера:

class IoC {
   /**
    * @var PDO Соединение с БД
    */
   protected $db;

   /**
    * Создаёт экземпляр Photo и внедряет зависимости
    */
   public static newPhoto()
   {
      $photo = new Photo;
      $photo->setDB(static::$db);
      $photo->setConfig();
      $photo->setResponse();
      return $photo;
   }
}
$photo = IoC::newPhoto();

Теперь всё вернулось на круги своя: одним-единственным вызовом метода класса-контейнера мы получаем полностью готовый к употреблению экземпляр объекта Photo.

Лучшим решением, всё-таки, будет более общее решение класса-контейнера:

class IoC {
   /**
    * @var Реестр
    */
   protected static $registry = array();

   /**
    * Добавляет resolver
    *
    * @param  string $name Идентификатор
    * @param  object $resolve Создатель объектов
    * @return void
    */
   public static function register($name, Closure $resolve)
   {
      static::$registry[$name] = $resolve;
   }

   /**
    * Создаёт экземпляр объекта
    *
    * @param  string $name Идентификатор resolver'a
    * @return mixed
    */
   public static function resolve($name)
   {
      if ( static::registered($name) )
      {
         $name = static::$registry[$name];
         return $name();
      }
      throw new Exception('Нет у нас такого в наличии :(');
   }
   /**
    * Определяет, зарегистрирован ли resolver
    *
    * @param  string $name ID resolver'а
    * @return bool
    */
   public static function registered($name)
   {
      return array_key_exists($name, static::$registry);
   }
}

Данный класс предоставляет возможность регистрировать резолверы (функции, выполняющие непосредственную работу по созданию объектов и внедрению зависимостей) при помощи метода register() и позднее использовать метод resolve() для того, чтобы получать экземпляры объектов классов. Например, зарегистрируем резолвер для нашего класса Photo:

// Добавляем резолвер 'photo' в реестр
IoC::register('photo', function() {
   $photo = new Photo;
   $photo->setDB('...');
   $photo->setConfig('...');
   return $photo;
});

И теперь там, где нам нужен экземпляр объекта, получаем его простым и понятным способом:

// Получаем экземпляр Photo, полностью готовый к работе
$photo = IoC::resolve('photo');

Теперь оба зайца убиты: зависимости вынесены за пределы класса, при этом сохранилась простота работы с ним. Немножко добавилось писанины:

// До
$photo = new Photo;
// После
$photo = IoC::resolve('photo');

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

Магические методы

Если пойти ещё дальше, желая минимизировать и упростить сам класс-контейнер, можно задействовать магические методы __set() и __get():

 

class IoC {
   protected $registry = array();
   public function __set($name, $resolver)
   {
      $this->registry[$name] = $resolver;
   }
   public function __get($name)
   {
      return $this->registry[$name]();
   }
}

Теперь добавлять и использовать резолверы стало намного интуитивно-понятнее:

$c = new IoC;
$c->mailer = function() {
  $m = new Mailer;

  // Настраиваем объект, внедряем зависимости

  return $m;
};
$mailer = $c->mailer; // И получаем экземпляр

Источник




Что такое dependency injection: 10 комментариев

  1. В первой реализации вы слукавили и создали на самом деле статический класс с фабричным методом, а не контейнер. :)

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

    1. Благодарю за замечание! Статья переводная, а сам я, к сожалению, в шаблонах проектирования плаваю ужасно. Не посоветуете ли что лучше почитать по теме? Не обязательно применительно к PHP. Спасибо ещё раз.

      1. Ссылки на Озон, но в сети можно найти издания на русском и английском в электронном виде.

        www.ozon.ru/context/detail/id/1308678/ (советую прочитать для начала, даже перед шаблонами банды 4-х).

        www.ozon.ru/context/detail/id/4884925/

        www.ozon.ru/context/detail/id/3938906/

        www.ozon.ru/context/detail/id/5497184/ (для более подготовленных читателей)

        www.ozon.ru/context/detail/id/3105480/

        www.amazon.com/Software-D...es/dp/0135974445

        Много идей можно вытащить из устройства компонент Zend Framework-a, если вы пишите на PHP. Я застал ещё 1-ую версию, потом сменил язык. Сейчас вроде интересные идеи реализуются в Symphony2/Doctrine2.

        Кстати, уже реализованный контейнер: symfony-gu.ru/documentati...e_container.html

      1. Вторую не читал, а первая это можно сказать первоисточник по шаблонам. Правда если мало представления о C++, то будет плохо понятны приведённые примеры. Хотя в большей степени кросс-языковые описаны. Хотя есть такие, которые вы в других языках и почти не встретите или очень редко. Например, шаблон Мост (Bridge).

  2. Хорошая статья, очень подробно расписано про шаблон. Я долго не мог въехать в сути внедрения зависимостей, пока не попробовал Symfony2 в серьезном крупном проекте. Сейчас просто влюблен в этот паттерн, даже свою небольшую но удобную реализацию использую часто jakulov.ru/blog/2014/bun_dependency_injection

Комментарии запрещены.