Every time when discussing software with other developers, the topic of singletons emerges, especially in the context of WordPress development. I often try to explain why they should be avoided, even if they are considered a standard template.
In this article I will try to reveal the topic of why singletons should never be used in code, and what alternatives are there to solve similar problems.
Singleton is a design pattern in software development, described in the book Design Patterns: Elements of Reusable Object-Oriented Software (by the Gang of Four ), thanks to which design patterns were spoken of as a software development tool.
The idea is that you may need to have only one instance of the class and that you provide a global single access point to it.
It’s actually just enough to explain and understand, and for many people, Singleton is an easy entry into the world of design patterns , making it the most popular pattern.
Singleton is popular; it was one of the first templates described and standardized in the book. How is it that some developers consider it anti-pattern? Can it really be so bad?
Yes.
Yes maybe.
I noticed that many people confuse two related concepts. When they say they need a singleton, they actually need to use one instance of the object in different instantiations . In general, when you create an instance, you create a new instance of this class. But for some objects you should always use the same shared instance of the object, regardless of where it is used.
But singleton is not the right solution for this.
Confusion caused by the fact that singleton combines two functions (responsibilities) in one object . Suppose there is a singleton to connect to the database. Let's call it (very ingeniously) DatabaseConnection
. Singleton now has two main functions:
DatabaseConnection
instance management.It is because of the second function that people choose singleton, but this task must be solved by another object.
There is nothing bad in the general instance . But the object that you want to use for this is not the place for such a restriction.
Below I will show a few alternatives. But first I want to tell you what problems Singleton can cause.
First of all, and this may seem more like a theoretical problem, Singlelton violates many of the principles of SOLID .
The singleton pattern violates four of the five principles of SOLID. He might have wanted to break the fifth one, if only he could have interfaces ...
It is easy to say that your code does not work only because of some theoretical principles. And although, according to my own experience, these principles are the most valuable and reliable guidance that can be relied upon when developing software, I understand that just the words “this is a fact” sound unconvincing to many. We must trace the influence of singleton on your daily practice.
Here are the shortcomings that can be encountered when dealing with a singleton:
You cannot pass / inject arguments to the constructor. Since the first call to a singleton actually executes only the constructor and you cannot know in advance which code will first access the singleton, all consuming code needs to use the same set of arguments to pass to the constructor, which is almost impossible in the first place generally meaningless. As a result, Singleton makes use of the main mechanism of instantiation in OOP languages.
You cannot mock away a singleton when testing components that use it. This makes it almost impossible to correctly unit test, because you will not achieve complete isolation of the code under test. The problem is not even caused by the logic that you want to test, but by an arbitrary instantiation constraint into which you wrap it.
Since singleton is a globally accessible construct that is used by your entire codebase, any encapsulation efforts are in the dust, which causes the same problems as in the case of global variables. That is, no matter how you try to isolate a singleton in the encapsulated part of the code, any other external code can lead to side effects and bugs in the singleton. And without proper encapsulation, the principles of the PLO are emasculated.
If you have ever had a website or application that has grown so large that the DatabaseConnection
singleton suddenly needed to connect to a second database different from the first one, it means you're in trouble. We'll have to re-examine the architecture itself and, possibly, completely rewrite a significant part of the code.
All tests that directly or indirectly use singleton cannot correctly switch from one to another. They always save state through a singleton, which can lead to unexpected behavior where your tests depend on the launch sequence, or the remaining state will hide real bugs from you.
I do not want to be the person who sees the bad in everything, but cannot offer a solution to the problem. Although I believe that you should evaluate the entire application architecture to decide how to avoid using singleton first, I suggest some of the most common ways in WordPress, when a singleton can be easily replaced by a mechanism that meets all requirements and is free from most of the drawbacks. But before I talk about this, I want to point out why all my proposals are just a compromise.
There is an “ideal structure” for application development. Theoretically, the best option is the only instantiating call in the boot code that creates the entire dependency tree of the application, from top to bottom. It will work like this:
App
(need Config
, Database
, Controller
).Config
for implementation in the App
.Database
for implementation in App
.Controller
for implementation in the App
(need Router
, Views
).Router
for implementation in Controller
(need HTTPMiddleware
).With one call, the entire application stack will be built from top to bottom with dependency injection as needed. The objectives of this approach are:
However, no matter how good it sounds, it’s not possible to do this in WordPress, since it does not provide a centralized container or implementation mechanism, all plugins / themes are loaded in isolation.
Keep this in mind as we discuss approaches. The ideal solution, in which the entire WordPress stack is instantiated through a centralized deployment mechanism, is not available to us, since it requires WordPress Core support. All the approaches described below are characterized by some common shortcomings such as hiding dependencies by addressing them directly from logic instead of introducing them.
Sample code using the singleton approach, which we will compare with others:
// . final class DatabaseConnection { private static $instance; private function __construct() {} // . public static function get_instance() { if ( ! isset( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } // . public function query( ...$args ) { // . } } // . $database = DatabaseConnection::get_instance(); $result = $database->query( $query );
I didn’t include here all the implementation details with which singletones are often loaded, because they are not important for theoretical discussion.
In most cases, the best way to get away from problems associated with a singleton is to use the “factory method” design pattern. A factory is an object whose only duty is to instantiate other objects. Instead of the DatabaseConnectionManager
, which makes its own instance using the get_instance()
method, you have a DatabaseConnectionFactory
that creates instances of the DatabaseConnection
object. In general, the factory will always produce new copies of the desired object. But based on the requested object and context, the factory can decide for itself whether to create a new instance or to always share one.
Given the name of the template, you might think that it looks more like Java code than PHP code, so feel free to deviate from too strict (and lazy) naming conventions and call the factory more creative.
An example of a factory method:
// . final class Database { public function get_connection(): DatabaseConnection { static $connection = null; if ( null === $connection ) { // , , . $connection = new MySQLDatabaseConnection(); } return $connection; } } // , (mock) . interface DatabaseConnection { public function query( ...$args ); } // . final class MySQLDatabaseConnection implements DatabaseConnection { public function query( ...$args ) { // . } } // . $database = ( new Database )->get_connection(); $result = $database->query( $query );
As you can see, the consuming code is not so large and simple, only there is one nuance. We decided to call the Database
factory instead of DatabaseConnection
, since this is part of the API that we provide, and we should always strive for a balance between logical accuracy and elegant brevity.
The above version of the factory is free from almost all the previously described shortcomings, with one exception.
DatabaseConnection
object, but instead created a new one with a factory. This is not problematic, because the factory is a pure abstraction, the likelihood that at some point you will need to move away from the concept of “instantiation” is very small. If this happens, then you may have to reconsider the entire paradigm of the PLO.You are probably starting to wonder that we can no longer forcibly confine ourselves to a single instantiation. Although we always give away a generic instance of the DatabaseConnection
implementation, anyone can still run new MySOLDatabaseConnection
and gain access to the additional instance. Yes, it is, and this is one of the reasons for the rejection of the singleton. But this does not always give advantages in real-life tasks, since it makes impossible the observance of basic requirements such as unit testing.
Static Proxy is another design pattern for which you can change a singleton. It implies an even closer connection than a factory, but this is at least a connection with abstraction, and not with a specific implementation. The idea is that you have a static mapping of the interface, and these static calls are redirected to the concrete implementation transparently. Thus, there is no direct connection with the actual implementation, and the static Deputy decides for himself how to choose the implementation to use.
// . final class Database { public static function get_connection(): DatabaseConnection { static $connection = null; if ( null === $connection ) { // You can have arbitrary logic in here to decide what // implementation to use. $connection = new MySQLDatabaseConnection(); } return $connection; } public static function query( ...$args ) { // Forward call to actual implementation. self::get_connection()->query( ...$args ); } } // , (mock) . interface DatabaseConnection { public function query( ...$args ); } // . final class MySQLDatabaseConnection implements DatabaseConnection { public function query( ...$args ) { // . } } // . $result = Database::query( $query );
As you can see, the static Deputy creates a very short and clean API. The disadvantages include the fact that there is a close connection between the code and the class signature. When used in the right place, this does not cause any special problems, since this is a connection with abstraction, which can be controlled directly, and not with a specific implementation. You can still replace the code of one database with the code of another, which you consider necessary, and the implementation is still a completely normal object that can be tested.
The WordPress Plugin API can replace singletones when they are used to enable global access via plugins. This is the cleanest solution given the limitations of WordPress, with the proviso that the entire infrastructure and architecture of your code is tied to the WordPress Plugin API. Do not use this method if you are going to re-use your code in different frameworks.
// , (mock) . interface DatabaseConnection { const FILTER = 'get_database_connection'; public function query( ...$args ); } // . class MySQLDatabaseConnection implements DatabaseConnection { public function query( ...$args ) { // . } } // . $database = new MySQLDatabaseConnection(); add_filter( DatabaseConnection::FILTER, function () use ( $database ) { return $database; } ); // . $database = apply_filters( DatabaseConnection::FILTER ); $result = $database->query( $query );
One of the main tradeoffs is that your architecture is directly tied to the WordPress Plugin API. If you plan to ever provide the functionality of a plugin for Drupal-sites, then the code will have to be completely rewritten.
Another possible problem is that you are now dependent on the timing of WordPress interceptors (hooks). This can lead to timing-related bugs, which are often difficult to reproduce and fix.
The service locator is one form of the Inversion of Control Container . Some sites describe the method as anti-pattern. On the one hand, this is true, but on the other, as we have already discussed above, all the recommendations proposed here can only be considered as compromises.
A service locator is a container that provides access to services implemented elsewhere. The container for the most part is a collection of instances associated with identifiers. More complex implementations of the service locator can introduce features such as lazy instantiation or generation of surrogates.
// , . interface Container { public function has( string $key ): bool; public function get( string $key ); } // . class ServiceLocator implements Container { protected $services = []; public function has( string $key ): bool { return array_key_exists( $key, $this->services ); } public function get( string $key ) { $service = $this->services[ $key ]; if ( is_callable( $service ) ) { $service = $service(); } return $service; } public function add( string $key, callable $service ) { $this->services[ $key ] = $service; } } // , (mock) . interface DatabaseConnection { public function query( ...$args ); } // . class MySQLDatabaseConnection implements DatabaseConnection { public function query( ...$args ) { // . } } // . $services = new ServiceLocator(); $services->add( 'Database', function () { return new MySQLDatabaseConnection(); } ); // . $result = $services->get( 'Database' )->query( $query );
As you might have guessed, the problem of getting a link to the $services
instance is not lost. It can be solved by combining this method with any of the previous three.
$result = ( new ServiceLocator() )->get( 'Database' )->query( $query );
$result = Services::get( 'Database' )->query( $query );
$services = apply_filters( 'get_service_locator' ); $result = $services->get( 'Database' )->query( $query );
However, there is still no answer to the question of whether a service locator should be used instead of a singleton anti-pattern ... There is a problem with the service locator : it “hides” dependencies. Imagine a code base that uses the correct implementation of the constructor. In this case, it is enough to look at the designer of a specific object, and you can immediately understand which object it depends on. If the object has access to the link to the service locator , then you can bypass this explicit dependency resolution and extract the link (and therefore start to depend) to any object from real logic. This is what they mean when they say that the service locator "hides" dependencies.
But, given the context of WordPress, we must accept the fact that from the very beginning we are not able to get the perfect solution. There is no technical ability to implement proper dependency injection across the entire code base. This means that in any case we will have to look for a compromise. The service locator is not an ideal solution, but this template fits well with the legacy context and at least allows you to collect all the “trade-offs” in one place, rather than scatter them around the code base.
If you work only in your own plugin and you do not need to provide access to your objects to other plugins, then you are lucky: you can use real dependency injection to avoid global access to dependencies.
// , (mock) . interface DatabaseConnection { public function query( ...$args ); } // . class MySQLDatabaseConnection implements DatabaseConnection { public function query( ...$args ) { // . } } // . class Plugin { private $database; public function __construct( DatabaseConnection $database ) { $this->database = $database; } public function run() { $consumer = new Consumer( $this->database ); return $consumer->do_query(); } } // . // . class Consumer { private $database; public function __construct( DatabaseConnection $database ) { $this->database = $database; } public function do_query() { // . // . return $this->database->query( $query ); } } // . $database = new MySQLDatabaseConnection(); $plugin = new Plugin( $database ); $result = $plugin->run();
, , , .
, , , .
, (wiring) . ( Dependency Injector ) ( ), .
, ( /, ):
// , (resolving) DatabaseConnection. $injector->alias( DatabaseConnection::class, MySQLDatabaseConnection::class ); // , DatabaseConnection . $injector->share( DatabaseConnection::class ); // Plugin, , . $plugin = $injector->make( Plugin::class );
, , , .
, , , :
. WordPress', .
, , , , .
, — , !
Source: https://habr.com/ru/post/334078/