Inversion of Control: Методы реализации с примерами на PHP

в 12:40, , рубрики: dependency injection, inversion of control, ioc, laravel, php, software architecture, symfony 2, yii 2, Программирование, Проектирование и рефакторинг

О боже, ещё один пост о Inversion of Control

Каждый более-менее опытный программист встречал в своей практике словосочетание Инверсия управления (Inversion of Control). Но зачастую не все до конца понимают, что оно значит, не говоря уже о том, как правильно это реализовать. Надеюсь, пост будет полезен тем, кто начинает знакомится с инверсией управления и несколько запутался.

Итак, согласно Википедии Inversion of Control — принцип объектно-ориентированного программирования, используемый для уменьшения связанности в компьютерных программах, основанный на следующих 2 принципах

  • Модули верхнего уровня не должны зависеть от модулей нижнего уровня. И те, и другие должны зависеть от абстракции.
  • Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

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

Рассмотрим пример.
Пусть у нас есть 2 класса — OrderModel и MySQLOrderRepository. OrderModel вызывает MySQLOrderRepository для получения данных из MySQL хранилища. Очевидно, что модуль более высокого уровня (OrderModel) зависит от относительного низкоуровневого MySQLOrderRepository.

Пример плохого кода приведён ниже.

<?php 

class OrderModel
{
   public function getOrder($orderID)
   {
      $orderRepository = new MySQLOrderRepository();
      $order = $orderRepository->load($orderID);
      return $this->prepareOrder($order);
   }
   
   private function prepareOrder($order)
   {
      //some order preparing
   }
}


class MySQLOrderRepository
{
   public function load($orderID)
   {
      // makes query to DB to fetch order row from table	
   }
   
}

В общем и целом этот код будет отлично работать, выполнять возложенные на него обязанности. Можно было и остановиться на этом. Но вдруг у Вашего заказчика появляется гениальная идея хранить заказы не в MySQL, а в 1С. И тут Вы сталкиваетесь с проблемой — Вам приходится изменять код, который отлично работал, да и ещё и изменения вносить в каждый метод, использующий MySQLOrderRepository.
К тому же, Вы и не писали тесты для OrderModel…

Таким образом, можно выделить следующие проблемы кода, приведенного ранее.

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

И что же со всем этим делать?

1. Фабричный метод / Абстрактная фабрика

Одним из самых простых способов реализации инверсии управления является фабричный метод (может использоваться и абстрактная фабрика)
Суть его заключается в том, что вместо непосредственного инстанцирования объекта класса через new, мы предоставляем классу-клиенту некоторый интерфейс для создания объектов. Поскольку такой интерфейс при правильном дизайне всегда может быть переопределён, мы получаем определённую гибкость при использовании низкоуровневых модулей в модулях высокого уровня.

Рассмотрим выше приведённый пример с заказами.
Вместо того, чтобы напрямую инстанцировать объект класса MySQLOrderRepository, мы вызовем фабричный метод build для класса OrderRepositoryFactory, который и будет решать, какой именно экземпляр и какого класса должен быть создан.

Реализация инверсии управления с помощью Factory Method

<?php 

class OrderModel
{
   public function getOrder($orderID)
   {
      $factory = new DBOrderRepositoryFactory();
      $orderRepository = $factory->build();
      $order = $orderRepository->load($orderID);
      return $this->prepareOrder($order);
   }
   
   private function prepareOrder($order)
   {
      //some order preparing
   }
}


abstract class OrderRepositoryFactory
{
  
  /**
   * @return IOrderRepository
   */
   abstract public function build();
}

class DBOrderRepositoryFactory extends OrderRepositoryFactory
{
   public function build()
   {
      return new MySQLOrderRepository();
   }
}


class RemoteOrderRepositoryFactory extends OrderRepositoryFactory
{
   public function build()
   {
      return new OneCOrderRepository();
   }
}

interface IOrderRepository
{
   public function load($orderID);
}

class MySQLOrderRepository implements IOrderRepository
{
   public function load($orderID)
   {
      // makes query to DB to fetch order row from table	
   }
   
}

class OneCOrderRepository implements IOrderRepository
{
   public function load($orderID)
   {
      // makes query to 1C to fetch order	
   }
   
}


Что нам даёт такая реализация?

  1. Нам предоставляется гибкость в создании объектов-репозиториев — инстанцируемый класс может быть заменён на любой, который мы сами пожелаем. Например, MySQLOrderRepository для DBOrderRepositoryfactory может быть заменён на OracleOrderRepository. И это будет сделано в одном месте
  2. Код становится более очевидным, поскольку объекты создаются в специализированных для этого классах
  3. Также имеется возможность добавить для выполнения какой-либо код при создании-объектов. Код будет добавлен только в 1 месте

Какие проблемы данная реализация не решает?

  1. Код перестал зависеть от низкоуровневых модулей, но тем не менее зависит от класса-фабрики, что всё равно несколько затрудняет тестирование

2. Service Locator

Основная идея паттерна Service Locator заключается в том, чтобы иметь объект, который знает, как получить все сервисы, которые, возможно, потребуются. Главное отличие от фабрик в том, что Service Locator не создаёт объекты, а знает как получить тот или иной объект. Т.е. фактически уже содержит в себе инстанцированные объекты.
Объекты в Service Locator могут быть добавлены напрямую, через конфигурационный файл, да и вообще любым удобным программисту способом.

Реализация инверсии управления с помощью Service Locator

<?php 

class OrderModel
{
   public function getOrder($orderID)
   {
     $orderRepository = ServiceLocator::getInstance()->get('orderRepository');
      $order = $orderRepository->load($orderID);
      return $this->prepareOrder($order);
   }
   
   private function prepareOrder($order)
   {
      //some order preparing
   }
}


class ServiceLocator
{
    private $services = array();
    private static $serviceLocatorInstance = null;
    private function __construct(){};   
  
    public static function getInstance()
    {
       if(is_null(self::$serviceLocatorInstance)){
          self::$serviceLocatorInstance = new ServiceLocator();
       }
      
       return self::$serviceLocatorInstance;        
    }
  
    public function loadService($name, $service)
    {
       $this->services[$name] = $service;   
    }
  
    public function getService($name) 
    {
       if(!isset($this->services[$name])){
          throw new InvalidArgumentException(); 
       }
       
       return $this->services[$name];
    }
}


interface IOrderRepository
{
   public function load($orderID);
}

class MySQLOrderRepository implements IOrderRepository
{
   public function load($orderID)
   {
      // makes query to DB to fetch order row from table	
   }
   
}

class OneCOrderRepository implements IOrderRepository
{
   public function load($orderID)
   {
      // makes query to 1C to fetch order	
   }
   
}



// somewhere at the entry point of application

ServiceLocator::getInstance()->loadService('orderRepository', new MySQLOrderRepository());

Что нам даёт такая реализация?

  1. Нам предоставляется гибкость в создании объектов-репозиториев. Мы можем привязать к именованному сервису любой класс который мы пожелаем сами.
  2. Появляется возможность конфигурирования сервисов через конфигурационный файл
  3. При тестировании сервисы могут быть заменены Mock-классами, что позволяет без проблем протестировать любой класс, использующий Service Locator

Какие проблемы данная реализация не решает?
В целом, спор о том, является Service Locator паттерном или анти-паттерны уже очень старый и избитый. На мой взгляд, главная проблема Service Locator

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

3. Dependency Injection

В целом, Dependency Injection — это предоставление внешнего сервиса какому-то классу путём его внедрения.
Таких пути бывает 3

  • Через метод класса (Setter injection)
  • Через конструктор (Constructor injection)
  • Через интерфейс внедрения (Interface injection)

Setter injection

При таком методе внедрения в классе, куда внедрятся зависимость, создаётся соответствутющий set-метод, который и устанавливает данную зависимость

Реализация инверсии управления с помощью Setter injection

<?php 

class OrderModel
{
   /**
    * @var IOrderRepository   
    */
   private $repository;
  
   public function getOrder($orderID)
   {
      $order = $this->repository->load($orderID);
      return $this->prepareOrder($order);
   }
   
   public function setRepository(IOrderRepository $repository)
   {
      $this->repository = $repository; 
   }  
  
   private function prepareOrder($order)
   {
      //some order preparing
   }
}



interface IOrderRepository
{
   public function load($orderID);
}

class MySQLOrderRepository implements IOrderRepository
{
   public function load($orderID)
   {
      // makes query to DB to fetch order row from table	
   }
   
}

class OneCOrderRepository implements IOrderRepository
{
   public function load($orderID)
   {
      // makes query to 1C to fetch order	
   }
   
}




$orderModel = new OrderModel();
$orderModel->setRepository(new MySQLOrderRepository());

Constructor injection

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

Реализация инверсии управления с помощью Constructor injection

<?php 

class OrderModel
{
   /**
    * @var IOrderRepository   
    */
   private $repository;
  
   public function __construct(IOrderRepository $repository)
   {
       $this->repository = $repository; 
   }
  
   public function getOrder($orderID)
   {
      $order = $this->repository->load($orderID);
      return $this->prepareOrder($order);
   }
     
   private function prepareOrder($order)
   {
      //some order preparing
   }
}



interface IOrderRepository
{
   public function load($orderID);
}

class MySQLOrderRepository implements IOrderRepository
{
   public function load($orderID)
   {
      // makes query to DB to fetch order row from table	
   }
   
}

class OneCOrderRepository implements IOrderRepository
{
   public function load($orderID)
   {
      // makes query to 1C to fetch order	
   }
   
}


$orderModel = new OrderModel(new MySQLOrderRepository());

Inteface injection

Такой метод внедрения зависимостей очень похож на Setter Injection, затем исключением, что при таком методе внедрения в класс, куда внедрятся зависимость, set-метод добавляется из интерфейса, который данный класс и реализует

Реализация инверсии управления с помощью Inteface injection

<?php 

class OrderModel implements IOrderRepositoryInject
{
   /**
    * @var IOrderRepository   
    */
   private $repository;
  
   public function getOrder($orderID)
   {
      $order = $this->repository->load($orderID);
      return $this->prepareOrder($order);
   }
   
   public function setRepository(IOrderRepository $repository)
   {
      $this->repository = $repository; 
   }  
  
   private function prepareOrder($order)
   {
      //some order preparing
   }
}

interface IOrderRepositoryInject
{
   public function setRepository(IOrderRepository $repository);
}

interface IOrderRepository
{
   public function load($orderID);
}

class MySQLOrderRepository implements IOrderRepository
{
   public function load($orderID)
   {
      // makes query to DB to fetch order row from table	
   }
   
}

class OneCOrderRepository implements IOrderRepository
{
   public function load($orderID)
   {
      // makes query to 1C to fetch order	
   }
   
}


$orderModel = new OrderModel();
$orderModel->setRepository(new MySQLOrderRepository());

Что нам даёт реализация с помощью Dependency Injection?

  1. Код классов теперь зависит только от интерфейсов, не абстракций. Конкретная реализация уточняется на этапе выполнения
  2. Такие классы очень легки в тестировании

Какие проблемы данная реализация не решает?
По правде говоря, я не вижу во внедрении зависимостей каких-то больших недостатков. Это хороший способ сделать класс гибким и максимально независимым от других классов. Возможно это ведёт к излишней абстракции, но это уже проблема конкретной реализации принципа программистом, а не самого принципа

4. IoC-контейнер

IoC-контейнер — это некий контейнер, который непосредственно занимается управлением зависимостями и их внедрениями (фактически реализует Dependency Injection)

IoC-контейнеры присутствует во многих современных PHP-фреймворках — Symfony 2, Yii 2, Laravel, даже в Joomla Framework :)
Главное его целью является автоматизация внедрения зарегистрированных зависимостей. Т.е. вам необходимо только лишь указать в конструкторе класса необходжимый интерфейс, зарегестрировать конкретную реализацию данного интерфейса и вуаля — зависимость внедрена в Ваш класс

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

Symfony 2 — symfony.com/doc/current/components/dependency_injection/introduction.html
Laravel — laravel.com/docs/4.2/ioc
Yii 2 — www.yiiframework.com/doc-2.0/guide-concept-di-container.html

Заключение

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

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

Автор: andrewnester

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js