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

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

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

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 32

    • UFO just landed and posted this here
        +5
        тут БД не главное, это всего лишь пример такой :)
          +7
          При чём тут БД вообще?
            –3
            Это статья о создании такой архитектуры кода, чтобы потом этот код легче было адаптировать под изменения или масштабировать горизонтально. Что очень полезно при разработке ПО.
              +3
              А какая связь между горизонтальным масшабированием и IoC?
            –2
            Наиболее правильный подход это задавать зависимости в конфигурационном файле. А у вас по сути, чтобы изменить зависимость, все равно приходится по коду бегать и искать что поменять. И фабрика (она же контейнер) тогда в этом случае подходит очень хорошо.
            Вот простой контейнер: bitbucket.org/stp008/customioc/src.
              +2
              И чем этот подход «правильный»?

              (а чтобы не надо было бегать по коду, нужно зависимости описывать строго в одном месте — composition root)
                +1
                Дополню — иными словами, разделять конструирование и использование, т.е. соблюдать SRP.
                  –2
                  Тем, что в больших крупных компаниях существуют специальные люди, называемые инженерами по настройке и конфигурированию систем. И исходя из названия их должности они занимаются конфигурацией и поддержкой этих систем. И они очень часто не знают хорошо ЯП. И еще при использовании конфига из файла отпадает необходимость в перекомпиляции кода. Для больших систем этот процесс может занимать очень много времени. Я понимаю, что php эта проблема никогда не коснется ввиду специфики использования этого языка, но тема вашего поста это рассказ про инверсию контроля, а не про особенности ее использования на php. И инверсия контроля впервые изначально появилась в больших энтерпрайз приложениях, потому что там особенно остро стояла проблема зависимостей и конфигурирования.
                    +2
                    описание зависимостей в конфигурационном файле не является целью инверсии управления

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

                    как они будут конфигурироваться — конфиг-файл, параметры из администраторской панели, данные с какого-либо устройства, да что угодно — это специфика приложения
                      –2
                      Я написал как правильнее будет задавать зависимости. Меня спросили почему и я ответил. Про цели инверсии вообще ни слова не было сказано. Вы же статью написали с целью помочь людям разобраться. Так и показывайте более правильные реализации. Вы же взяли не конкретное уже реализованное приложение, а написали свой код для демонстрации принципов. А конфигурация параметров в вашем случае происходит прямо в коде. Поэтому я и написал как более правильно будет сделать и объяснил зачем. Ваш код не использует конф файла, панель и не сканирует среду на поиск файлов с совпадающим с параметрами названием.
                      +1
                      Вы про devops не слышали? Это к разговору о «специальных людях».

                      А вообще, то, что вы озвучиваете — это типичная позиция «давайте мы сделаем конфигурируемую систему, в которой все будет делаться через конфиг». Позиция хорошая, но в реальности выживающая исключительно редко, поскольку все комбинации зависимостей, которые можно указать в конфиге, нужно тестировать в разработке. Дело это весьма трудозатратное, а выигрыш от него, скажем так… не очень большой. Поэтому в реальности в конфигурационном файле задаются только те зависимости, которые в системе реально есть смысл конфигурировать (например, адаптер БД или конкретный механизм логирования), а не все, и делается это чаще всего не через конфигурацию контейнера как такового, а через специализированную конфигурацию приложения.
                    +2
                    при описании сервис локатора я писал
                    Объекты в Service Locator могут быть добавлены напрямую, через конфигурационный файл, да и вообще любым удобным программисту способом.

                    это применимо и к контейнерам. для того же Symfony 2 сервисы описываются в основном в конфиг файлах
                    но это уже имеет не столько отношение к инверсии зависимости, сколько к организации контейнера и его использованию

                    а что, если у Вас предусмотрена возможность выбора сервиса в администраторской панели приложения? Например, выбор payment processor. Тут уже конфигурационный файл не поможет, а зависимость в контейнере конфигурируется на основе данных в БД

                    но как я сказал, это уже специфика приложений, и имеет меньшее отношение к инверсии управления
                    0
                    Как по мне то тру IoC контейнеры есть только в PHP DI и Magento2. Все остольное это сервис локатор сервис локаторов ;)
                      0
                      с Magento честно говоря не работал, но PHP DI действительно отличный контейнер, который удобно добавлять в существующий проект либо при написании проекта на фреймворке, в котором проблема внедрения зависимости не решается из коробки.
                      в Symfony 2 мне прекрасно хватает их коробочного решения :)
                      –1
                      Кстати, есть довольно минималистичный контейнер для зависимостей в меньше чем 10 строчек.

                      class DI
                      {
                      protected $storage = array();

                      function __set($key, $value)
                      {
                      $this->storage[$key] = $value;
                      }

                      function __get($key)
                      {
                      return $this->storage[$key]($this);
                      }
                      }

                      Спойлер не работает :(
                        +1
                        Вы, вроде, Registry изобрели сейчас :)
                        +1
                        10 строк это замечательно, но отсутствие какого-либо исключения при обращении к несуществующему сервису не очень-то хорошо.
                        насколько я понял в качестве $value должна передаваться анонимная функция, котораю будет возвращать экземпляр сервиса, верно?

                        и спасибо за уточнение по Service Locator, действительно добавил лишню точку с запятой после private function __construct(){}
                        0
                        Спасибо за статью. Эта тема действительно много раз поднималась и все равно радует, когда люди несут свет в наше разношерстное PHP комьюнити.
                        Только одно небольшое «фэ»:
                        set-метод добавляется из интерфейса
                        — скорей интерфейс обязует класс реализовать метод.
                          0
                          спасибо большое, действительно не очень грамотно написал, исправил :)
                          +1
                          Вы забыли самый популярный подход — через глобальные переменные.
                            +1
                            По мне так это все тот же Service Locator
                            0
                            Спасибо за статью, очень интересно и актуально будет всегда. Мне нравится ещё использовать для этого observer, странно что его не упомянули. В этом случае в теле класса может вообще не быть упоминания о других классах и их методах, а сделать это в одном связующем объекте. Тогда все связи также будут в одном месте.
                              0
                              наверное все таки правильнее сказать не observer, а Publish–subscribe pattern. Создаем класс Event, и будем использовать его для отправки сообщений из одного метода класса1 и подписки на него другого метода класса2. Код тяжело отлаживать, но зависимость минимальная.
                                0
                                Во-первых, pub-sub — это не IoC (а еще точнее, можно сделать pub-sub с IoC и без него).
                                А ао-вторых, вы просто заменяете прямые зависимости на косвенные. А зачем? Снижение уровня связности — не самоцель же.
                                  0
                                  я соглашусь с Вами, что такой вариант сделает 2 модуля действительно не зависимыми друг от друга, но как Вы сами сказали — отлаживать код тяжело, да и понятность кода резко упадёт.

                                  я бы observer (не считая классического случая использования) использовал в случае реализации Event Sourcing — тогда действительно это имело бы место и смотрелось гармонично, остальные варианты мне кажется не очень себя оправдывают
                                0
                                Еще бы PHPStorm научился грамотно определять все подключенные компоненты в IoC.
                                Для того же Yii2 приходится хаком класс Yii переопределять
                                  0
                                  для Yii не смотрел честно говоря, но для Symfony 2 есть плагин, который распознает все сервисы, прописанные в конфигах, даёт ьыстрый способ навигации по сервисам и естественно type hinting отлично работает.
                                  думаю в ближайшее время, что-нибудь такое должно и для Yii 2 появится
                                  +2
                                  На мой взгяд, крайне неудачный пример Service Locator-а.
                                  $orderModel->setRepository(new MySQLOrderRepository());
                                  
                                  Создаёт инстанс класса MySQLOrderRepository сразу. Исользуя lazy-loading используя лямбду будет более экономичным решением, если учесть, что в проекте таких вызовов могут быть тонны. Всё равно каждый раз надо грузить в сценарий лишь используемые классы.
                                  Примера ради:
                                  $orderModel->setRepository(function () { return new MySQLOrderRepository()});
                                  

                                  Далее не буду описывать описывать механизмы проверки, загружен ли уже сервис и т.д.
                                  Думаю, алгоритм понятен.
                                    0
                                    соглашусь, так будет экономичнее, особенно если много сервисов
                                    0
                                    Скажите пожалуйста, разве приведенное в начале статьи определение не является определением Dependency Inversion Principle?

                                    Only users with full accounts can post comments. Log in, please.