Pull to refresh

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

PHPProgrammingDesigning and refactoring
О боже, ещё один пост о 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());




Interface injection


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

Реализация инверсии управления с помощью Interface 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, несмотря на наличие отличных фреймворков, библиотек, позволяющих писать красивый, чистый, читаемый, гибкий код.

Надеюсь статья была кому-то полезная и чей-то код благодаря этому станет лучше.
Пишите свои замечания, пожелания, уточнения, вопросы — буду рад
Tags:phpiocinversion of controldependency injectionинверсия зависимостейsoftware architecturesymfony 2yii 2laravelDI
Hubs: PHP Programming Designing and refactoring
Rating +24
Views 40.1k Add to bookmarks 262
Comments
Comments 32

Popular right now

PHP разработчик с framework Symfony , Yii, Laravel, etc…
from 150,000 ₽Страховая Компания СогласиеМосква
Backend PHP developer (Laravel)
from 130,000 to 180,000 ₽GeeckoRemote job
Backend Php Developer
to 220,000 ₽GOSOBLAKORemote job
PHP developer (laravel)
from 2,000 to 3,000 $Cool.ClubRemote job
Middle PHP Developer
from 1,800 $Spiral ScoutRemote job

Top of the last 24 hours