📜 ⬆️ ⬇️

Inversion of Control: Implementation Methods with PHP Examples

Oh god, another post about Inversion of Control


Each more or less experienced programmer has encountered the phrase Inversion of Control in his practice. But often not everyone fully understands what it means, not to mention how to properly implement it. I hope the post will be useful to those who are starting to get acquainted with the inversion of control and somewhat confused.


')
So, according to Wikipedia, Inversion of Control is an object-oriented programming principle used to reduce connectivity in computer programs, based on the following 2 principles


In other words, we can say that all dependencies of modules should be built on the abstractions of these modules, and not on their specific implementations.

Consider an example.
Suppose we have 2 classes - OrderModel and MySQLOrderRepository. OrderModel calls MySQLOrderRepository to get data from the MySQL repository. Obviously, a higher level module (OrderModel) depends on the relative low-level MySQLOrderRepository.

An example of bad code is shown below.
<?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 } } 


In general, this code will work fine, perform the duties assigned to it. It was possible to stop at this. But suddenly your customer has a brilliant idea to store orders not in MySQL, but in 1C. And here you encounter a problem - you have to change the code that worked perfectly, and also make changes to every method that uses MySQLOrderRepository.
In addition, you did not write tests for OrderModel ...

Thus, the following problems of the code given earlier can be highlighted.


And what to do with all this?

1. Factory Method / Abstract Factory


One of the easiest ways to implement control inversion is a factory method (an abstract factory can also be used)
Its essence lies in the fact that instead of directly instantiating a class object through new, we provide a certain interface for the client class to create objects. Since such an interface can always be redefined with the right design, we get some flexibility when using low-level modules in high-level modules.

Consider the above example with orders.
Instead of directly instantiating an object of the MySQLOrderRepository class, we will call the factory build method for the OrderRepositoryFactory class, which will decide which instance and which class should be created.

Implementing inversion control with 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 } } 



What does such an implementation give us?
  1. We are given the flexibility to create repository objects — the class to be instantiated can be replaced by whatever we want. For example, the MySQLOrderRepository for DBOrderRepositoryfactory can be replaced with OracleOrderRepository. And it will be done in one place.
  2. The code becomes more obvious as objects are created in specialized classes.
  3. It is also possible to add some code to execute when creating-objects. The code will be added only in 1 place


What problems does this implementation solve?
  1. The code has ceased to depend on low-level modules, but nonetheless depends on the factory class, which still makes testing somewhat difficult


2. Service Locator


The main idea of ​​the Service Locator pattern is to have an object that knows how to get all the services that may be needed. The main difference from factories is that Service Locator does not create objects, but knows how to get one or another object. Those. actually already contains instantiated objects.
Objects in the Service Locator can be added directly, through the configuration file, and indeed in any way convenient to the programmer.

Implementing Inversion Control with 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()); 



What does such an implementation give us?
  1. We are given the flexibility to create repository objects. We can bind to a named service any class that we wish.
  2. It is possible to configure services through the configuration file
  3. When testing, services can be replaced by Mock-classes, which allows you to easily test any class that uses the Service Locator


What problems does this implementation solve?
In general, the debate about whether the Service Locator is a pattern or an anti-pattern is already very old and beaten. In my opinion, the main problem of Service Locator
  1. Since the locator object is a global object, it can be available in any part of the code, which can lead to its excessive code and, accordingly, nullify all attempts to reduce the connectivity of modules.


3. Dependency Injection


In general, Dependency Injection is the provision of an external service to some class through its implementation.
There are 3 such ways


Setter injection


With this method of implementation in the class where dependencies are introduced, a corresponding set-method is created, which sets this dependency.

Implementing inversion control with 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


With this method of implementation, a new argument is added to the constructor of the class where dependencies are introduced, which is the dependency to be set.
Implementing inversion control with 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


This method of implementing dependencies is very similar to Setter Injection, then the exception is that with this method of implementation, the class where dependencies are introduced is inherited from the interface, which requires the class to implement this set method.

Implementing inversion control using 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()); 



What does the implementation with Dependency Injection give us?
  1. Class code now depends only on interfaces, not abstractions. The specific implementation is specified at runtime.
  2. Such classes are very easy to test.


What problems does this implementation solve?
In truth, I do not see any major flaws in dependency injection. This is a good way to make a class flexible and as independent as possible from other classes. Perhaps this leads to unnecessary abstraction, but this is already a problem of the concrete implementation of the principle by the programmer, and not the principle itself.

4. IoC container


An IoC container is a container that is directly involved in managing dependencies and their implementations (in fact, it implements Dependency Injection)

IoC containers are present in many modern PHP frameworks - Symfony 2, Yii 2, Laravel, even in the Joomla Framework :)
Its main goal is to automate the introduction of registered dependencies. Those. you only need to specify the necessary interface in the class constructor, register the concrete implementation of this interface and voila - the dependency is implemented in your class

The work of such containers is somewhat different in different frameworks, so I give you links to the official framework resources, which describes how their containers work.

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

Conclusion


The topic of control inversion has been raised millions of times, hundreds of posts and thousands of comments on this topic. But nevertheless, I also meet people all, I see the code and I understand that this topic is not very popular in PHP, despite the presence of excellent frameworks and libraries that allow writing beautiful, clean, readable, flexible code.

I hope the article was useful to someone and someone's code thanks to this will be better.
Write your comments, suggestions, clarifications, questions - I will be glad

Source: https://habr.com/ru/post/244517/


All Articles