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;
Enter fullscreen mode Exit fullscreen mode
- Second, we make the constructor private, so our client can’t instantiate it whenever it pleases.
private Singleton(String value) {
this.value = value;
}
Enter fullscreen mode Exit fullscreen mode
- 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;
}
Enter fullscreen mode Exit fullscreen mode
- 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;
}
Enter fullscreen mode Exit fullscreen mode
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;
}
}
Enter fullscreen mode Exit fullscreen mode
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");
}
Enter fullscreen mode Exit fullscreen mode
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);
}
Enter fullscreen mode Exit fullscreen mode
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;
}
}
Enter fullscreen mode Exit fullscreen mode
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);
}
Enter fullscreen mode Exit fullscreen mode
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/
暂无评论内容