Allowed global variables and supposed memory savings.
For 20 years I have been teaching software at the University of Buenos Aires. In the software engineering course we teach design patterns and the same “scheme” is always repeated almost like a type of déjà vu, the same sequence that I had the opportunity to witness in several of my works and in the free software that I use:
The ‘magical’ appearance of the Singleton pattern.
The pattern has been used in the industry for decades. Its popularity is attributed to the excellent book Design Patterns. There are numerous software frameworks that use it, and we rarely find literature that discourages its use.
Despite this, in the corresponding Wikipedia entry we can read a Dantesque warning:
Critics consider the singleton to be an anti-pattern in that it is frequently used in scenarios where it is not beneficial.
Introduces unnecessary restrictions in situations where a sole instance of a class is not actually required, and introduces global state into an application.
Let’s be pragmatic as always, and look at the arguments for and against its use:
1. Violates the bijection principle
As we saw in previous articles, every object in our computable model has to be mapped on a 1 to 1 relationship with a real-world entity.
Singletons are often linked to objects that need to be unique. As usual, we will have to distinguish among the objects that are essentially unique (for problem domain drivers) and differentiate them from the accidentally unique ones regarding implementation reasons, efficiency, resource consumption, global access, etc.
Most accidentally unique objects are not present in the real world, and we will see later on that the presumably essentially unique ones may not be so if we consider different contexts, environments, or situations.
It is a global reference. Again according to Wikipedia:
An implementation of the singleton pattern must provide global access to that instance.
What a priori appears as a benefit for preventing us from having to pass context information, generates coupling. The reference to the singleton cannot be changed according to the environment (development, production), nor can dynamic strategy changes related to the current load be made, it cannot be replaced by a double test and it prevents us from making changes due to the possible ripple effect.
3. It says a lot about (accidental) implementation and little about his (essential) responsibilities
By focusing early on implementation issues (the Singleton is an implementation pattern) we orient ourselves according to accidentality (how) and underestimate the most important thing of an object: the responsibilities it has (what).
When carrying out premature optimization in our designs, we usually award a concept that we have just discovered as Singleton.
The aforementioned coupling has as a corollary; the impossibility of having full control over the side effects of a test to guarantee its determinism.
We must depend on the global state referenced by the Singleton.
The argument used to propose its use is to avoid the construction of multiple volatile objects. This supposed advantage is not real in virtual machines with efficient garbage collection mechanisms.
In such virtual machines, used by most modern languages, keeping objects in a memory area whose Garbage Collector algorithm is a double pass (mark & sweep) is much more expensive than creating volatile objects and quickly removing them.
As good solid design advocates, we favor inversion of control through dependency injection to avoid coupling.
The service provider (formerly a hardcoded Singleton) is decoupled from the service itself, replacing it with an injectable dependency that meets the defined requirements, coupling us to what and not how.
When we ask a class to create a new instance we expect the contract to be honored and give us a fresh new instance. However, many Singleton implementations hide the creation omission silently, rather than failing quickly to indicate that there is a business rule that instances should not be arbitrarily created.
A better answer would be to show with an exception it is not valid to create new instances in this execution context.
This will force us to have a private constructor to use it internally. Thus violating the contract that all classes can create instances. Another code smell.
When invoking a class to use it (again, to use its what), we will have to couple with the fact that it is accidentally a Singleton (its how), generating a relation that, when trying to break it, would produce the much-feared ripple effect.
If we use the TDD development technique, objects are defined purely and exclusively based on their behavior. Therefore, in no case, the construction of software using TDD will arise the Singleton concept.
If business rules state that there must be a single provider of a certain service, this will be modeled through a controlled access point (which should not be a global class, much less a Singleton).
Trying to create unit tests in an existing system coupled to a Singleton can be an almost impossible task.
When the pattern is stated it is usually accompanied by some idea that in the real world seems rather unique. For example, if we want to model the behavior of God according to the vision of Christianity, there could not be more than one God. But these rules are relative to the context and subjective vision of each religion. Various belief systems may coexist in the same world with their own gods (some monotheistic and other polytheistic beliefs).
Pattern structure according to the design pattern book
The class (and all the metamodel) is not present in the bijection. Any relationship linked to the class will be invalid
Pattern implementation can be tricky in programs with multiple threads. If two execution threads try to create the instance at the same time and it does not exist yet, only one of them should succeed in creating the object. The classic solution to this problem is to use mutual exclusion in the class creation method that implements the pattern, to make sure it is reentrant.
Singletons are references attached to classes, just as classes are global references these are not reached by the garbage collector. In case the Singleton is a complex object, this entire object, in addition to the transitive closure of all its references, will stay in memory throughout the execution.
The persistent state is the enemy of unit tests. One of the things that makes unit tests effective is that each test must be independent of all the others. If this is not true, then the order in which the tests are run may affect the test results and the tests become non-deterministic. This can lead to cases where tests fail when they shouldn’t, and worse, can lead to tests that pass only in the order they were performed. This can hide mistakes and is very bad.
Avoiding static variables is a good way to prevent the state from being preserved between tests. Singletons, by their very nature, depend on an instance that is kept in a static variable. This is an invitation for the dependency test.
The single responsibility of a class is to create instances.
Adding any other responsibility to any class implies violating the single responsibility principle (the S for Solid). A class should not worry about being or not being a Singleton. They should only be responsible for their commitments to business rules. In case of needing the uniqueness of these instances, this would be the responsibility of a third object in the middle such as a Factory or a Builder.
Singletons are frequently used to provide a global access point to some service. What ends up happening is design dependencies are hidden within the code and are not visible when examining the interfaces of their classes and methods.
The need to create something global to avoid passing it explicitly is a code smell. There are always better solutions and alternatives to using a global reference that do not require passing all collaborators between methods.
Many singletons are themselves abused as a global reference repository.
The temptation to use the singleton as an entry point for new references is huge.
There are many examples where a Singleton is used as a quick-reach reference container.
As if it was not enough to be the root of all evil he is also the easy friend of the party. In large projects, it just accumulates garbage to get out of trouble.
Since it does not have a corresponding entity on the bijection, adding responsibilities that do not correspond to it, is like adding one more stain to the tiger. Apparently without doing damage but generating ripple effect when wishing to do a healthy decoupling.
There are several Root Cause Analysis and post mortems on production defects correlated to systems and the lack of testing it hinders.
Having stated the arguments against Singleton let’s try to see the possible benefits:
This argument is fallacious according to the current state of the art of languages with a decent virtual machine and garbage collector. It is enough to carry out a benchmark and look for evidence to convince us.
The Singleton can be used to guarantee the uniqueness of a concept. But it is not the only way or the best.
Let’s rewrite the previous example:
Access and creation of the single instance are not coupled. Creation unicity is done in a single controlled point and direct references to classes are decoupled.
There are objects that require a certain cost of resources to create. If this cost is large, we will not be able to generate them constantly. One possible solution is to use a Singleton and have it available all time. As always we will focus on what and we will look for some other hows generating less coupling.
If we need a single control point or a cache we will have to access a known object related to a certain context (and easily replaceable according to the environment, the test setup, etc.). Certainly a Singleton will not be our first choice.
There are multiple techniques to gradually remove the (ab)use of Singletons. In this article we list some of them:
The disadvantages listed in this article are much greater than the advantages, and the evidence from the examples in the industry should be a strong indicator of the non-use of the evil pattern in any case. As our profession matures, we will leave behind these kinds of bad solutions.
Part of the objective of this series of articles is to generate spaces for debate and discussion on software design.
We look forward to comments and suggestions on this article.
If you liked this post, you can follow me on Twitter where I share daily tips about coding and software design.