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

Что тако внедрение зависимостей (Dependency Injection) ?

Dependency Injection – это программный шаблон, который реализует принцип объектно-ориентированного программирования "Инверсия управления (Inversion Of Control)". Реализация этого шаблона подразумевает снижения "связанности кода", соответственно, получается код, который легче использовать повторно и сопровождать, то есть, изменение компонент одной части приложения не вызывает ошибок в другой части или необходимости значительных каскадных изменений.

DI существет только в объектно-ориентированном мире. Рассмотрение будет проведено на примере языка PHP. Допустим, мы пишем приложение, логику мы помещаем в класс App. Наше приложение использует сторонние данные, логику получение данных помещаем в класс Service. Получается класс App зависит от класса Service. Как это реализуется? Например так:

class Service
{
  private $data;
  private $key;

  function __construct ($key = '123') 
  {
    $this->key = $key;
  }

  function get()
  {
    return $data();
  }

  // ...
}

class App
{
  protected $service;

  function __construct()
  {
    $this->service = new Service();
  }

  function getData()
  {
    return $this->service->get();
  }

  // ...
}

$app = new App();
$app_data = $app->getData();

Всё хорошо работает пока нам не нужно больше гибкости. Что делать, если надо изменить $key сервиса? Необходимо менять код класса, например, "вшить" ключ внутрь конструктора App.

class App
{
  protected $service;

  function __construct()
  {
    $this->service = new Service('456');
  }

  // ...
}

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

class App
{
  protected $service;

  function __construct($key)
  {
    $this->service = new Service($key);
  }

  // ...
}

$app = new App('789');

Всё рассмотренные альтернативы являются bad practice.

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

И вот на сцену выходит внедрение зависимостей. Вместо того, чтобы создавать Service объект внутри App класса, надо создать Service заранее и затем передать его в объект App как аргумент конструктора.

class App
{
  protected $service;

  function __construct(Service $service)
  {
    $this->service = $service;
  }

  // ...
}

$service = new Service('777');
$app = new App($service);

Всё. В этом вся суть внедрения зависимостей. Теперь можно настраивать сервис независимо от приложения, и легко подменить класс сервиса для тестирования.

Внедрение зависимостей не ограничивается инъекцией конструктора. Еще есть внедрение через setter метод и через свойство класса.

  • Внедрение через конструктор:
class App
{
  function __construct($service)
  {
    $this->service = $service;
  }

  // ...
}
  • Внедрение через setter метод:
class App
{
  function setService($service)
  {
    $this->service = $service;
  }

  // ...
}
  • Внедрение через свойство класса:
class App
{
   public $service;
}

$app->service = $service;

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

Большинство современных PHP фреймворков активно используют внедрение зависимостей.

Что тако сервис-контейнер (Service Container) ?

Следующая вещь, о которой хочется поговорить - контейнер внедрения зависимостей Injection Dependency Container. По-русски обычно назыается сервис-контейнер (Service Container или IoC Container или Injector) - объект, который знает, как создавать и настраивать объекты. Чтобы выполнять свою работу, он должен знать об аргументах конструктора и отношениях между объектами.

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

class Container
{
  public function getSomeService()
  {
    return new Service('option1','option2');
  }
}

$container = new Container();
$service = $container->getSomeService();

В случае чуть сложнее мы сделаем контейнер настраиваемым, пробросив параметры option1 и option2 через конструктор контейнера.

class Container
{
  protected $parameters = array();

  public function __construct(array $parameters = array())
  {
    $this->parameters = $parameters;
  }

  public function getSomeService()
  {
    return new Service(
      $this->parameters['option1'],
      $this->parameters['option2']
    );
  }  
}

$container = new Container(array(
  'option1' => 'foo',
  'option2' => 'bar'
);
$service = $container->getSomeService();

В случае еще чуть сложнее реализуется синглтон, чтобы getSomeService() возвращал всегда один и тот же экземпляр.

Автоматическое внедрение зависимостей

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

  • Представим, что сервис-контейнер - это рюкзак.
  • Внедряемые классы - вещи, лежащие в рюкзаке.
  • У вас есть возможность класть вещи в рюкзак, при этом вы подписываете название вещи (класса) и краткую инструкцию как ей пользоваться (параметры конструктора класса и зависимости).
  • Далее Вы указываете зависимости в своих классах
class A {
  private $b;
  private $c;

  public function __construct(thingB $dependencyB, thingC: $dependencyC) // вещи thingB и thingC
  {
    $this->b = $dependencyB;
    $this->c = $dependencyC;
  }

  // ...
}

// где-то в недрах фреймворка
$container = new Container;
$a = $container->get('A');

// или вот так в Laravel
$foo = App::make('A');

// или вот так в Angular
injector = ReflectiveInjector.resolveAndCreate([A, thingB, thingC]);
let a = injector.get(A);
  • Когда создается экземпляр класса А, фреймворк берет рюкзак и начинает перебирать вещи, пытаясь найти на них названия thingB и thingC.
  • Если находит, то создает экземпляры thingB и thingC используя краткую инструкцию. Иначе выбрасывает исключения.
  • Передает созданные экземпляры dthingB и thingC в конструктор класса А и нужный нам объект создается. Надо заметить, что помещать класс А в контейнер необязательно, сервис-контейнер фреймворка используя механизм рефлексии PHP сам может получить параметры конструктора и исползовать эту информацию чтобы создать экземпляр класса А.

Почитать в оригинале

Похожие записи

+function ($) { "use strict"; }(window.jQuery);

  • объявляется IIFE (немедленно выполняемая функция-выражение)
  • в функцию передаётся объект библиотеки jQuery, которая внутри будет доступна через переменную с именем $
  • включается «строгий режим»

Medium like image zooming

Полгода собирался в блог добавить zoom на изображения как на medium и вот "новый год" позволил мне это сделать.