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 from table row 
}

}


У загальному і цілому цей код буде відмінно працювати, виконувати покладені на нього обов'язки. Можна було і зупинитися на цьому. Але раптом у Вашого замовника з'являється геніальна ідея зберігати замовлення не в 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 from table row 
}

}

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

}





Що нам дає така реалізація?
  1. Нам надається гнучкість у створенні об'єктів-репозиторіїв — инстанцируемый клас може бути замінений на будь-який ми самі захочемо. Наприклад, MySQLOrderRepository для DBOrderRepositoryfactory може бути замінений на OracleOrderRepository. І це буде зроблено в одному місці
  2. Код стає більш очевидним, оскільки об'єкти створюються в спеціалізованих для цього класах
  3. Також є можливість додати для виконання якої-небудь код при створенні об'єктів. Код буде додано лише в 1 місці


Які проблеми дана реалізація не вирішує?
  1. Код перестав залежати від низькорівневих модулів, але тим не менш залежить від класу-фабрики, що все одно дещо ускладнює тестування


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

Реалізація інверсії управління з допомогою Locator Service
<?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 from table row 
}

}

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

}



// somewhere at the entry point of application

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




Що нам дає така реалізація?
  1. Нам надається гнучкість у створенні об'єктів-репозиторіїв. Ми можемо прив'язати до іменованого сервісу будь-клас який ми обираємо самі.
  2. З'являється можливість конфігурування сервісів через конфігураційний файл
  3. При тестуванні сервіси можуть бути замінені Mock-класами, що дозволяє без проблем протестувати будь-який клас, використовує Locator Service


Які проблеми дана реалізація не вирішує?
Загалом, суперечка про те, є Locator Service паттерном або анти-патерни вже дуже старий і побитий. На мій погляд, головна проблема Locator Service
  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 from table row 
}

}

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

}




$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 from table row 
}

}

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

}


$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 from table row 
}

}

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

}


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

Сподіваюся стаття була комусь корисна і чийсь код завдяки цьому стане краще.
Пишіть свої зауваження, побажання, уточнення, питання — буду радий

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.