The Singleton Pattern is extremely popular among beginners in Object-Oriented Programming due to its ease of implementation and promise of global state handling, but is it worth it?
Table of Contents
- What is the Singleton Pattern
- The first problem
- Singleton breaks a SOLID principle
- Dependency Injection
- A better alternative: Monostate
- Be aware that global state can be unpredictable
- Conclusion
- References
What is the Singleton Pattern
Simply put, the Singleton Pattern ensures that a class has only one instance while providing a global access point to that instance. You can imagine that it could be useful to share a configuration object across an application, for example.
To create a singleton, we will need to add a couple of things to our class:
- First, we add a private static field for storing the singleton instance.
private static Singleton instance;
- Second, we make the constructor private, so our client can't instantiate it whenever it pleases.
private Singleton(String value) {
this.value = value;
}
- If we can't instantiate it outside of itself, we need to create a public static method for getting the instance.
public static Singleton getInstance(String value) {
instance = new Singleton(value);
return instance;
}
- We now instantiate a new object only on its first call and attribute it to our static field, so the method will always return the same instance, no matter how many calls are made. So the previous code become:
public static Singleton getInstance(String value) {
if (instance == null) {
instance = new Singleton(value);
}
return instance;
}
Let's check the full code in Java to visualize it better:
public final class Singleton {
private static Singleton instance;
public String value;
private Singleton(String value) {
this.value = value;
}
public static Singleton getInstance(String value) {
if (instance == null) {
instance = new Singleton(value);
}
return instance;
}
}
We can check if it is working with the following:
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance("PIPIPI");
Singleton singleton2 = Singleton.getInstance("POPOPO");
}
Note that we didn't directly instantiate it.
Here, both singleton1.value
and singleton2.value
would have the same value "PIPIPI"
because the object was created on the first call only, and the second one can only access the cached object.
That's it! Well... it depends.
The first problem
This implementation particularly is too simplistic for multi-threaded languages like Java because different threads could create different instances simultaneously, so it would not be a Singleton anymore, right? If we need some sort of slow initialization, we will run into problems.
To prevent race conditions like that we need to synchronize threads when instantiating the singleton. I won't get into details here because the solution requires double-checked locking and that's another topic. Also, Java handles it in its very own way and it requires some extra knowledge.
Refactor Guru provides a very interesting implementation of a thread-safe singleton and explains pretty well the strategy behind double-checked locking. Check it out for further discussion.
Singleton breaks a SOLID principle
Our class is breaking the Single Responsibility Principle by trying to solve two problems at the same time: Ensure that it is instantiated only once and provide a global access point to that instance.
A class exists to serve as a blueprint of an object, and not to also instantiate the object itself.
Dependency Injection
Since we use Singletons to expose global state, it makes no sense to inject them into other objects, right? But with this approach you end up hiding the dependencies of your application in your code, instead of exposing them through interfaces, and we should code against interfaces, not implementations.
Without dependency injection, our singletons and the classes that need him are tight-coupled, creating a huge problem when unit testing.
A better alternative: Monostate
Also known as BorgIdiom (because the Borgs in the Star Trek series share a common memory), the Monostate Pattern allows the creation of multiple objects that share the same static attributes instead of guaranteeing that only a single instance of a class exists.
public static void main(String[] args) {
Monostate monostate1 = new Monostate();
Monostate monostate2 = new Monostate();
monostate1.setValue("Apple");
monostate2.setValue("Banana");
System.out.println(monostate1.getValue());
System.out.println(monostate2.getValue());
System.out.println(monostate1 == monostate2);
}
Both values will be "Banana" but monostate1 == monostate2
will be false, because they are not the same object.
Monostate has one major advantage over singleton: The subclasses might decorate the shared state as they wish and hence can provide dynamically different behavior than the base class.
Users of a monostate do not behave differently than users of a regular object. The users do not need to know that the object is monostate, and that's one of the most important characteristics.
Be aware that global state can be unpredictable
Sharing the same state across an application and expose an API to modify it, can very easily lead to confusion since it gets almost impossible to know what is the current value once the setter is called in different places.
Let's re-create the first Singleton example with a setter, so we can assign a new value
not only in the constructor but anytime.
public final class Singleton {
private static Singleton instance;
public static String value;
private Singleton(String value) {
this.value = value;
}
public static Singleton getInstance(String value) {
if (instance == null) {
instance = new Singleton(value);
}
return instance;
}
public static void setValue(String newValue) {
value = newValue;
}
}
For the sake of simplicity, the example is all in the entry point of the app:
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance("PIPIPI");
Singleton singleton2 = Singleton.getInstance("LALALA");
singleton2.setValue("POPOPO");
System.out.println(singleton1.value);
System.out.println(singleton2.value);
}
Both print "POPOPO"
because there is only one instance! So if we have a singleton3
, singleton4
, and so on (or simply Singleton. You don't need to assign a static class to a new variable) anywhere in our app, it's really difficult to keep track of our current state value.
It is even worse if the value type is a reference type like an Array, ArrayList, or any Iterable! Just think about the number of nullPointerExceptions
that it could create, or the unexpected elements inside.
On that matter, I honestly believe functional programming brings a better approach with the use of pure functions, but that's a discussion for the future.
Conclusion
We cannot use a design pattern blindly. It can solve a problem while creating others if we don't have an understanding of the pros and cons.
Also, SOLID principles are not sacred but they've been tested over decades and developed conventions proven to create better maintainability for huge codebases, so I particularly tend to respect them a lot for the code quality they brought to my career.
Thank you so much for reading.
References
https://refactoring.guru/design-patterns/singleton
https://www.freecodecamp.org/news/solid-principles-explained-in-plain-english/
https://www.youtube.com/watch?v=yimeXZ1twWs
https://stackoverflow.com/questions/137975/what-are-drawbacks-or-disadvantages-of-singleton-pattern
https://jorudolph.wordpress.com/2009/11/22/singleton-considerations/
https://betterprogramming.pub/code-against-interfaces-not-implementations-37b30e7ab992
https://www.simplethread.com/the-monostate-pattern/
Top comments (5)
Another reference: JCO (Just Create One) Pattern (2003)
"Design Patterns: Elements of Reusable Object-Oriented Software"; 1994 p.18
So the trade off of the Monostate pattern is that you still have to have access to the class implementation via the global namespace thereby directly coupling the dependants implementation to the Monostate class.
With JCO they simply have to depend on an (client-oriented) interface that only represents capabilities they need, not aware of the full interface of the implementing class thus reducing inter-class coupling.
I gotta admit that I've never heard about that one, but I'll definitely read about it and maybe add it to the article for future readers if you don't mind. Thank you for your contribution!
Honestly, I find this article bad advise.
The main reason this is ill-advised is that your result is counter intuitive..
If I instanciate an object
new Whatever()
and callwhatever.setValue("x")
then I intuitively expect it to be an instance value. It is counter-intuitive to have a "normal" object share state "behind the scenes".Either use
static
methods directly, or use singleton patternWhatever.getInstance()
to clearly communicate it's a "shared" object. This is about clearly communicating the intend to developers.Also, the so-called "problems" are non-existant.
Just declare the method
synchronized
. That's it. Done. Threading issue solved.However, most of the time, your singletons will be instanciated during the initialization phase of the software, typically in a single thread, so that you won't ever need it.
Being a singleton or not has nothing to do with the "Single Responsibility Principle". The "responsibility" is the task/role/functionality it has to provide.
Nonsense. It works wonderfully hand in hand. They are so well suited for each other that it's the foundation of many frameworks like Spring.
Lastly, there is one big thing you ommited in your argument. It's reasons why singletons *are singletons.*
These are a few of the reasons that pop out of my head, why instanciating just one singleton might make sense.
So, well, although the idea was nice, I'll pass on the "
Monostate
" which looks like a normal object but behaves counter-intuitively and inferiror in many aspects to usual singletons.In Python, were I to need shared state I will use Borg Pattern over Singleton π
But you are correct, we should be mindful where we use them and understand implications. Thank you for awesome article.
Excellent insight. I think singleton breaks indirectly another solid principle, open-closed principle, because it prevents extension of the singleton class.
I'm avoiding singleton usage wherever I can, by either using factories, dependency injection or some other mechanisms. Sometimes when I need to implement simple things in php, I'm using some sort of singleton functions with static blocks. In the end every language can offer some features that could help.