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

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

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

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

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

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

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

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 модуля, когда они настолько сильно связаны Такой код плохо расширяется. Как показал пример выше, для изменения хранилища заказов, пришлось изменять и модель, обрабатывающую заказы И что же со всем этим делать?

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

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

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 месте Какие проблемы данная реализация не решает?

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

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

Поскольку объект-локатор это глобальный объект, то он может быть доступен в любой части кода, что может привезти к его чрезмерному коду и соответственно свести на нет все попытки уменьшения связности модулей В целом, Dependency Injection — это предоставление внешнего сервиса какому-то классу путём его внедрения.Таких пути бывает 3Через метод класса (Setter injection) Через конструктор (Constructor injection) Через интерфейс внедрения (Interface injection) Setter injection При таком методе внедрения в классе, куда внедрятся зависимость, создаётся соответствутющий set-метод, который и устанавливает данную зависимостьРеализация инверсии управления с помощью Setter injection

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

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

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?

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

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

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

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

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

© Habrahabr.ru