DEV Community

Cover image for Singleton Pattern? No! Use this instead πŸ’‘
Gabriel Afonso
Gabriel Afonso

Posted on

Singleton Pattern? No! Use this instead πŸ’‘

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

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/

Top comments (4)

Collapse
 
peerreynders profile image
peerreynders • Edited

Another reference: JCO (Just Create One) Pattern (2003)

"Design Patterns: Elements of Reusable Object-Oriented Software"; 1994 p.18

  1. Clients remain unaware of the specific types of objects they use, as long as the objects adhere to the interface that clients expect.
  2. Clients remain unaware of the classes that implement these objects. Clients only know about the abstract class(es) defining the interface.

This so greatly reduces implementation dependencies between subsystems that it leads to the following principle of reusable object-oriented design:

Program to an interface, not an implementation.

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.

Collapse
 
gabrielprrd profile image
Gabriel Afonso

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!

Collapse
 
dagnelies profile image
Arnaud Dagnelies • Edited

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 call whatever.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 pattern Whatever.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.

The first problem

This implementation particularly is too simplistic for multi-threaded languages like Java because different threads could create different instances simultaneously...

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.

Our class is breaking the Single Responsibility Principle

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.

Dependency Injection bla bla

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.*

  • It may be costly to instanciate / initialize
  • It may "grab" a limited resource (like file locks, shared memory, hardware stuff, DB connections...)
  • It may have thread-safe methods

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.

Collapse
 
proteusiq profile image
Prayson Wilfred Daniel

In Python, were I to need shared state I will use Borg Pattern over Singleton 😊

from __future__ import annotations

class Borg:
    _shared_state: dict[str, str] = {}

    def __init__(self) -> None:
        self.__dict__ = self._shared_state


class Borgie(Borg):
    def __init__(self, state: str = None) -> None:
        super().__init__()
        if state:
            self.state = state

    def __repr__(self) -> str:
        return self.state

if __name__ == "__main__":
    u = Borgie("start")
    print(u) # start

    v = Borgie("stop")
    print(v) # stop
    print(u) # stop
Enter fullscreen mode Exit fullscreen mode

But you are correct, we should be mindful where we use them and understand implications. Thank you for awesome article.