Lesson 10 | Singleton: related patterns |
Objective | How other Patterns work with or substitute for Singleton Patterns |
How other Patterns work with or substitute for Singleton Patterns?
The Singleton pattern plays a unique role in software design, but it's not always the best solution, and other patterns can sometimes work with it or substitute for it. The Singleton's purpose is to control access to a single, shared instance, but in many cases, there are more flexible or robust design patterns that can be used instead.
- Factory Pattern: If the goal is to control the creation of objects, the Factory pattern can often be a better choice. The Factory pattern encapsulates the logic of creating complex objects, allowing the client code to stay independent of the complexities of object creation. This can substitute a Singleton when the control of object creation is the main concern, and not necessarily the uniqueness of the instance.
- Dependency Injection (DI): DI is a technique rather than a design pattern, but it can substitute for a Singleton in many cases. By injecting dependencies rather than accessing them globally, code can be made more modular, more testable, and less tightly coupled. A Singleton can be converted into a single instance that is passed to all objects that need it. DI containers often have a mechanism for defining services that should only be instantiated once, providing Singleton-like behavior.
- Prototype Pattern: This pattern creates a new object by copying an existing object (prototype). It can coexist with Singleton pattern when a system needs copies of the Singleton object. Each copy can change independently, but if you want all instances to change simultaneously, consider using Singleton.
- Monostate/Borg Pattern: This pattern lets multiple instances share the same state. From the client's perspective, these instances appear to be a Singleton, but they are not. This pattern provides the same functionality as Singleton but uses a different approach, which can make it more suitable in some contexts.
- Module Pattern: In languages that support modules, such as JavaScript (Node.js), the Module pattern can be used as an alternative to the Singleton pattern. Each module in Node.js, for example, is a Singleton by default because the instance created by the module is cached and reused whenever the module is required.
- Observer Pattern: This pattern allows an object (subject) to notify other objects (observers) when its state changes. A Singleton object could be an Observer, Subject, or both in a system using the Observer pattern. For instance, a Singleton Logger could observe various subjects and log any state changes.
Remember, while the Singleton pattern has its uses, it's often considered an anti-pattern because it can introduce global state into an application, leading to code that is tightly coupled and hard to test. It's essential to thoroughly understand your system's requirements and the implications of different design patterns before choosing one to use.
Patterns do not exist in a vacuum any more than classes or objects do. Most significant
object-oriented systems designed with patterns use more than one.
The Singleton pattern prevents objects from being created, specifically objects of its class other than the one unique instance.
Most other creational patterns actually create many different objects of some class. You can think of these creational patterns as machines cranking out objects on an assembly line.
However, the analogy only stretches so far. In particular, two machines do not create objects any faster than one machine. Therefore, it is common (though not required) to implement various creational patterns with Singleton patterns.
In particular, the
- Abstract Factory,
- Builder, and
- Prototype patterns
are often implemented with Singleton classes.
In the course project you will also encounter an example of part of a behavioral pattern, Observer, implemented as a Singleton.
Basic C++ Idioms Supporting Singletons
Most often, singletons are implemented in C++ by using some variation of the following idiom:
// Header file Singleton.h
class Singleton{
public:
// Unique point of access
static Singleton* Instance(){
if (!pInstance_)
pInstance_ = new Singleton;
return pInstance_;
}
// ----- operations -----
private:
Singleton(); // Prevent clients from creating a new Singleton
Singleton(const Singleton&);
/* Prevent clients from creating a copy of the Singleton */
static Singleton* pInstance_; // The one and only instance
};
// Implementation file Singleton.cpp
Singleton* Singleton::pInstance_ = 0;
Because all the constructors are private, user code cannot create Singletons. However, Singleton's own member functions are allowed to create objects. Therefore, the uniqueness of the Singleton object is enforced at compile time. This is the essence of implementing theSingleton design pattern in C++.
If it's never used (no call to Instance occurs), the Singleton object is not created. The cost of this
optimization is the (usually negligible) test incurred at the beginning of Instance. The advantage of the
build-on-first-request solution becomes significant if Singleton is expensive to create and seldom used.
An ill-fated temptation is to simplify things by replacing the pointer pInstance_ in the previous example
with a full Singleton object.
// Header file Singleton.h
class Singleton{
public:
// Unique point of access
static Singleton* Instance() {
return &instance_;
}
int DoSomething();
private:
static Singleton instance_;
};
// Implementation file Singleton.cpp
Singleton Singleton::instance_;
This is not a good solution. Although instance_ is a static member of Singleton (just as
pInstance_ was in the previous example), there is an important difference between the two versions.
instance_ is initialized dynamically (by calling Singleton's constructor at runtime), whereas
pInstance_ benefits from static initialization (it is a type without a constructor initialized with a
compile-time constant).
The compiler performs static initialization before the very first assembly statement of the program gets
executed. (Usually, static initializers are right in the file containing the executable program, so loading is
initializing.) On the other hand, C++ does not define the order of initialization for dynamically initialized
objects found in different translation units, which is a major source of trouble. (A translation unit is,
roughly speaking, a com-pilable C++ source file.) Consider this code:
// SomeFile.cpp
#include "Singleton.h"
int global = Singleton::Instance()->DoSomething();
Depending on the order of initialization that the compiler chooses for instance_ and global, the call to
Singleton::Instance
may return an object that has not been constructed yet. This means that you
cannot count on
instance_
being initialized if other external objects are using it.