📜 ⬆️ ⬇️

The problem of initializing objects in OOP applications in PHP. Finding a solution using the Registry, Factory Method, Service Locator and Dependency Injection templates

It so happened that programmers fix successful solutions in the form of design patterns. There are a lot of literature on patterns. The Gang of Four book Design Patterns by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides and, perhaps, the Patterns of Enterprise Application Architecture by Martin Fowler are definitely considered classics. The best I've read with examples in PHP - this is "PHP Objects, Patterns and Practice" by Matt Zandstra . It so happened that all this literature is quite difficult for people who have just started to master the OOP. Therefore, I had an idea to present some patterns that I consider most useful, In simplified form. In other words, this article is my first attempt to interpret the design patterns in KISS style.
Today we will talk about what problems may arise with the initialization of objects in OOP application and how you can use some popular design patterns to solve these problems.

Example


Modern OOP application works with dozens, hundreds, and sometimes thousands of objects. Well, let's take a close look at how these objects are initialized in our applications. The initialization of objects is the only aspect that interests us in this article, so I decided to omit all the “extra” implementation.
Suppose we have created a super-duper useful class that can send a GET request to a specific URI and return the HTML from the server response. So that our class does not seem too simple, let it also check the result and throw an exception in case of a “wrong” server response.

class Grabber { public function get($url) {/** returns HTML code or throws an exception */} } 


Let's create another class whose objects will be responsible for filtering the resulting HTML. The filter method accepts HTML code and a CSS selector as arguments, and it returns an array of the found elements according to the given selector.
')
 class HtmlExtractor { public function filter($html, $selector) {/** returns array of filtered elements */} } 


Now, imagine that we need to get Google search results for specific keywords. To do this, we introduce another class that will use the Grabber class to send a request, and the HtmlExtractor class to retrieve the necessary content. It will also contain the logic for building a URI, a selector for filtering the resulting HTML and processing the results.

 class GoogleFinder { private $grabber; private $filter; public function __construct() { $this->grabber = new Grabber(); $this->filter = new HtmlExtractor(); } public function find($searchString) { /** returns array of founded results */} } 


Have you noticed that the initialization of the Grabber and HtmlExtractor objects is in the constructor of the GoogleFinder class? Let's think about how successful this solution is.
Of course, hardcoding object creation in the constructor is not the best idea. And that's why. First, we cannot easily substitute the Grabber class in a test environment to avoid sending a real request. In fairness, it is worth saying that this can be done using the Reflection API . Those. a technical possibility exists, but this is far from the most convenient and obvious way.
Secondly, the same problem will arise if we want to reuse GoogleFinder logic with other implementations of Grabber and HtmlExtractor. Creating dependencies is hard coded in the class constructor. And in the best case, we can inherit GoogleFinder and override its constructor. And then, only if the scope of the grabber and filter properties is protected or public.
And the last moment, each time a new GoogleFinder object is created, a new pair of dependency objects will be created in memory, although we may well use one Grabber object and one HtmlExtractor object in several GoogleFinder objects.
I think that you already understood that dependency initialization needs to be moved out of class. We may require that already prepared dependencies are passed to the GoogleFinder class constructor.

 class GoogleFinder { private $grabber; private $filter; public function __construct(Grabber $grabber, HtmlExtractor $filter) { $this->grabber = $grabber; $this->filter = $filter; } public function find($searchString) { /** returns array of founded results */} } 


If we want to give other developers the ability to add and use their own Grabber and HtmlExtractor implementations, then we should consider introducing interfaces for them. In this case, it is not only useful, but necessary. I believe that if we use only one implementation in the project and do not assume the creation of new ones in the future, then we should abandon the creation of the interface. It is better to act on the situation and do simple refactoring when there is a real need for it.
Now we have all the necessary classes and we can use the GoogleFinder class in the controller.

 class Controller { public function action() { /* Some stuff */ $finder = new GoogleFinder(new Grabber(), new HtmlExtractor()); $results = $finder->find('search string'); /* Do something with results */ } } 


Let's summarize the subtotal. We wrote quite a bit of code, and at first glance, we didn’t do anything wrong. But ... and what if we need to use an object like GoogleFinder elsewhere? We will have to duplicate its creation. In our example, this is just one line and the problem is not so noticeable. In practice, the initialization of objects can be quite complicated and can take up to 10 lines, or even more. There are also other problems typical for code duplication. If in the process of refactoring you need to change the name of the used class or the logic of object initialization, you will have to manually change all the places. I think you know how it happens :)
Usually with a hardcode come simply. Duplicate values ​​are usually put into configuration. This allows you to centrally change values ​​in all places where they are used.

Registry template.


So, we decided to put the creation of objects in the configuration. Let's do that.

 $registry = new ArrayObject(); $registry['grabber'] = new Grabber(); $registry['filter'] = new HtmlExtractor(); $registry['google_finder'] = new GoogleFinder($registry['grabber'], $registry['filter']); 

It remains only to transfer our ArrayObject to the controller and the problem is solved.

 class Controller { private $registry; public function __construct(ArrayObject $registry) { $this->registry = $registry; } public function action() { /* Some stuff */ $results = $this->registry['google_finder']->find('search string'); /* Do something with results */ } } 


You can further develop the idea of ​​Registry. Inherit ArrayObject, encapsulate the creation of objects inside a new class, prohibit adding new objects after initialization, etc. But in my opinion, the above code fully makes it clear what the Registry template is. This template does not apply to generators, but it does allow us to solve our problems to some extent. Registry is just a container in which we can store objects and transfer them within the application. To make the objects available, we need to create them first and register them in this container. Let's analyze the advantages and disadvantages of this approach.
At first glance, we have achieved our goal. We stopped hardcoding class names and create objects in one place. We create objects in a single copy, which guarantees their reuse. If the logic of creating objects changes, then only one place in the application will need to be edited. As a bonus, we got the opportunity to centrally manage objects in the Registry. We can easily get a list of all available objects, and carry out any manipulations with them. Let's now see what might not suit us in this template.
First, we must create an object before registering it with the Registry. Accordingly, the probability of creating “unnecessary objects” is high, i.e. those that will be created in memory, but will not be used in the application. Yes, we can add objects to the registry dynamically, i.e. create only those objects that are needed to process a specific request. One way or another, we’ll have to control this manually. Accordingly, over time it will become very difficult to maintain it.
Secondly, we have a new dependency on the controller. Yes, we can receive objects through a static method in the Registry, so as not to transfer the Registry to the constructor. But in my opinion, do not do this. Static methods are even tougher than creating dependencies within an object, and testing difficulties (this is a good article on this topic).
Thirdly, the controller interface does not tell us what objects are used in it. We can get in the controller any object available in the registry. It will be hard for us to say exactly which objects the controller uses, until we check all its source code.

Factory Method


In the Registry, we are most uncomfortable with the fact that the object must first be initialized in order for it to become accessible. Instead of initializing an object in a configuration, we can select the logic for creating objects into another class, from which it will be possible to “ask” to build the object we need. Classes that are responsible for creating objects are called factories. And the design pattern is called Factory Method. Let's look at an example of a factory.

 class Factory { public function getGoogleFinder() { return new GoogleFinder($this->getGrabber(), $this->getHtmlExtractor()); } private function getGrabber() { return new Grabber(); } private function getHtmlExtractor() { return new HtmlFiletr(); } } 


As a rule, they make factories that are responsible for creating one type of objects. Sometimes a factory can create a group of related objects. We can use caching in a property to avoid re-creating objects.

 class Factory { private $finder; public function getGoogleFinder() { if (null === $this->finder) { $this->finder = new GoogleFinder($this->getGrabber(), $this->getHtmlExtractor()); } return $this->finder; } } 


We can parameterize the factory method and delegate initialization to other factories depending on the input parameter. This will be the Abstract Factory pattern.
If it becomes necessary to split the application into modules, we can demand that each module provide its own factories. We can further develop the theme of factories, but I think that the essence of this template is clear. Let's see how we will use the factory in the controller.

 class Controller { private $factory; public function __construct(Factory $factory) { $this->factory = $factory; } public function action() { /* Some stuff */ $results = $this->factory->getGoogleFinder()->find('search string'); /* Do something with results */ } } 


To the advantages of this approach, let's attribute it to simplicity. Our objects are created explicitly, and your IDE will easily lead you to the place where it occurs. We also solved the Registry problem and objects in memory will be created only when we “ask” the factory about it. But we have not yet decided how to supply the necessary factories to the controllers. There are several options. You can use static methods. You can let the controllers create the necessary factories themselves and negate all our attempts to get rid of copy-paste. You can create a factory of factories and transfer only it to the controller. But getting objects in the controller will be a little more difficult, and you will need to manage dependencies between the factories. In addition, it is not entirely clear what to do if we want to use modules in our application, how to register module factories, how to manage connections between factories from different modules. In general, we lost the main advantage of the factory - the explicit creation of objects. And while still not solved the problem of the "implicit" controller interface.

Service Locator


The Service Locator template allows you to solve the lack of fragmentation of factories and manage the creation of objects automatically and centrally. If we think about it, we can introduce an additional layer of abstraction, which will be responsible for creating objects in our application and manage the relations between these objects. In order for this layer to be able to create objects for us, we will need to give it the knowledge of how to do it.
Terms of Service Locator Template:
Any module can register their service descriptions. To get some service from the container we will need to request it by key. There are many options for implementing a Service Locator; in the simplest version, we can use ArrayObject as a container and a closure, as a description of services.

 class ServiceContainer extends ArrayObject { public function get($key) { if (is_callable($this[$key])) { return call_user_func($this[$key]); } throw new \RuntimeException("Can not find service definition under the key [ $key ]"); } } 


Then registration Definitions will look like this:

 $container = new ServiceContainer(); $container['grabber'] = function () { return new Grabber(); }; $container['html_filter'] = function () { return new HtmlExtractor(); }; $container['google_finder'] = function() use ($container) { return new GoogleFinder($container->get('grabber'), $container->get('html_filter')); }; 


And use, in the controller as follows:

 class Controller { private $container; public function __construct(ServiceContainer $container) { $this->container = $container; } public function action() { /* Some stuff */ $results = $this->container->get('google_finder')->find('search string'); /* Do something with results */ } } 


Service Container can be very simple, and can be very difficult. For example, Symfony Service Container provides a lot of possibilities: parameters (parameters), services scope (scopes), search for services by tags (tags), aliases (aliases), closed services (private services), the ability to make changes to the container after adding all services (compiller passes) and whatnot. DIExtraBundle further enhances the standard implementation.
But back to our example. As you can see, the Service Locator not only solves all the problems that the previous templates have, but also makes it easy to use modules with their own service definitions.
In addition, at the framework level, we received an additional level of abstraction. Namely, by changing the ServiceContainer :: get method we can, for example, replace an object with a proxy. And the scope of proxy objects is limited only by the imagination of the developer. Here you can implement the AOP paradigm, and LazyLoading, etc.
But, most developers still consider Service Locator an anti-pattern. Because, in theory, we can have any number of so-called. Container Aware classes (i.e. such classes that contain a reference to the container). For example, our Controller, within which we can get any service.
Let's see why this is bad.
First, again testing. Instead of creating mocks only for the classes used in tests, you will have to make the whole container or use a real container. The first option does not suit, because you have to write a lot of unnecessary code in the tests, the second, because it contradicts the principles of unit testing, and may lead to additional costs for test support.
Secondly, it will be difficult for us to refactor. By changing any service (or ServiceDefinition) in the container, we will have to check all dependent services as well. And this problem is not solved with the help of IDE. Finding such places throughout the application will not be so easy. In addition to the dependent services, it will also be necessary to check all the places where the refactored service is obtained from the container.
Well, the third reason is that uncontrolled tugging of services from a container will sooner or later lead to a mess in the code and unnecessary confusion. It is difficult to explain, you just need to spend more and more time to understand how a particular service works, in other words, you can fully understand what a class is doing or how it works by only reading all its source code.

Dependency Injection


What else can be done to limit the use of the container in the application? You can transfer to the framework the management of the creation of all user objects, including controllers. In other words, user code should not call the get method on the container. In our example, we can add a Definition for the controller to the container:

 $container['google_finder'] = function() use ($container) { return new Controller(Grabber $grabber); }; 


And get rid of the container in the controller:

 class Controller { private $finder; public function __construct(GoogleFinder $finder) { $this->finder = $finder; } public function action() { /* Some stuff */ $results = $this->finder->find('search string'); /* Do something with results */ } } 


Such an approach (when access to the Service Container is not provided to client classes) is called Dependency Injection. But this template has both advantages and disadvantages. As long as we observe the principle of sole responsibility, the code looks very nice. First of all, we got rid of the container in the client classes, thanks to which their code became much clearer and simpler. We can easily test the controller by replacing the necessary dependencies. We can create and test each class independently of others (including controller classes) using the TDD or BDD approach. When creating tests, we will be able to abstract away from the container, and later add a Definition when we need to use specific instances. All this will make our code easier and clearer, and testing is more transparent.
But, it is necessary to mention the reverse side of the coin. The point is that controllers are very specific classes. To begin with, the controller, as a rule, contains a set of actions, which means it violates the principle of sole responsibility. As a result, the controller class may have much more dependencies than is necessary to perform a particular action. The use of deferred initialization (the object is instantiated at the time of first use, and before that a lightweight proxy is used) to some extent solves the issue of performance. But from the point of view of architecture, creating many dependencies on a controller is also not entirely correct. In addition, testing of controllers is usually an unnecessary operation. Everything, of course, depends on how the testing is organized in your application and on how you yourself feel about it.
From the previous paragraph, you realized that using Dependency Injection does not completely eliminate the problems with the architecture. Therefore, think how it will be more convenient for you to store a link to the container in controllers or not. There is no one right decision. I believe that both approaches are good as long as the controller code remains simple. But, definitely, you should not create Conatiner Aware services besides controllers.

findings


Well, it's time to beat all of the above. And it was said a lot ... :)
So, to structure the work on the creation of objects, we can use the following patterns:
, PHP . Prototype, Reflection API, . , :)
, Dependency Injection , .
Dependency Injection, KISS , Pimple , . , Symfony Dependency Injection Component . , .
, .

Have fun!

PS , . ;)

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


All Articles