О боже, ещё один пост о 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, который и будет решать, какой именно экземпляр и какого класса должен быть создан.
<?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
}
}
Что нам даёт такая реализация?
- Нам предоставляется гибкость в создании объектов-репозиториев — инстанцируемый класс может быть заменён на любой, который мы сами пожелаем. Например, MySQLOrderRepository для DBOrderRepositoryfactory может быть заменён на OracleOrderRepository. И это будет сделано в одном месте
- Код становится более очевидным, поскольку объекты создаются в специализированных для этого классах
- Также имеется возможность добавить для выполнения какой-либо код при создании-объектов. Код будет добавлен только в 1 месте
Какие проблемы данная реализация не решает?
- Код перестал зависеть от низкоуровневых модулей, но тем не менее зависит от класса-фабрики, что всё равно несколько затрудняет тестирование
2. 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());
Что нам даёт такая реализация?
- Нам предоставляется гибкость в создании объектов-репозиториев. Мы можем привязать к именованному сервису любой класс который мы пожелаем сами.
- Появляется возможность конфигурирования сервисов через конфигурационный файл
- При тестировании сервисы могут быть заменены Mock-классами, что позволяет без проблем протестировать любой класс, использующий Service Locator
Какие проблемы данная реализация не решает?
В целом, спор о том, является Service Locator паттерном или анти-паттерны уже очень старый и избитый. На мой взгляд, главная проблема Service Locator
- Поскольку объект-локатор это глобальный объект, то он может быть доступен в любой части кода, что может привезти к его чрезмерному коду и соответственно свести на нет все попытки уменьшения связности модулей
3. Dependency Injection
В целом, Dependency Injection — это предоставление внешнего сервиса какому-то классу путём его внедрения.
Таких пути бывает 3
- Через метод класса (Setter injection)
- Через конструктор (Constructor injection)
- Через интерфейс внедрения (Interface injection)
Setter injection
При таком методе внедрения в классе, куда внедрятся зависимость, создаётся соответствутющий set-метод, который и устанавливает данную зависимость
<?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
При таком методе внедрения в конструкторе класса, куда внедрятся зависимость, добавляется новый аргумент, который и является устанавливаемой зависимостью
<?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-метод добавляется из интерфейса, который данный класс и реализует
<?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?
- Код классов теперь зависит только от интерфейсов, не абстракций. Конкретная реализация уточняется на этапе выполнения
- Такие классы очень легки в тестировании
Какие проблемы данная реализация не решает?
По правде говоря, я не вижу во внедрении зависимостей каких-то больших недостатков. Это хороший способ сделать класс гибким и максимально независимым от других классов. Возможно это ведёт к излишней абстракции, но это уже проблема конкретной реализации принципа программистом, а не самого принципа
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