The Singleton is one of the most common design patterns you will see in code. The pattern alone can provide an easy and straightforward solution to handle the global cross-cutting state. Despite its popularity, it has become one of the most notorious design patterns. Perhaps even one has caused some developers to curse loudly or lose sleep at night from large refactors removing them. It is best to use the Singleton with caution, but without a doubt, this design pattern still provides value when applied with knowledge of its negative effects.
A traditional example of a Singleton
class Singleton {
private static _instance?: Singleton;
private _config: any;
constructor() {
if (!Singleton._instance) {
Singleton._instance = new Singleton();
Singleton._instance.initialize();
}
return Singleton._instance;
}
private initialize() {
// do some one time initialization, this will not happen again until the app reloads!
this._config = {};
}
/** This is was initialized just once and is accessible globally now! */
public get config() {
return this._config;
}
}
The key is to use a global variable (in this case, a static “_instance”). The pattern guarantees you that you can only create the Singleton once for the lifetime of the program. It is constructed with new Singleton()
which returns a prior instance or creates a new one if it has not done so yet.
What is it?
The Singleton itself is “lazy loaded” and initialized on-demand. This makes it beneficial when you need state or functionaltiy around state on a global level and created just once. However, it becomes problematic once it is not needed and remains on the memory heap, hogging some chunk of memory until the program is closed. The Singleton design pattern effectively hides the class constructor and initialization flow from the caller.
Singleton’s inherent problem is that when you instantiate the object using the design pattern, it becomes “global.” It is accessible anywhere and everywhere and, by nature, becomes a cross-cutting concern. Also, because you did not explicitly create it in line with the natural “lifecycle” of your application, it will make it harder to mold the application architecture over time to a multiple instanced architecture (multi-user, for example). Thus the need to segment a Singleton(s) becomes an expensive and heavy refactor. Singletons are notoriously difficult to make tests against since their state is not directly in control from the beginning to end of a single test suite. This is because the Singleton cannot be deallocated and initialized on a natural flow.
Also, Singleton’s often don’t do well with asynchronous code. Callers can initialize the Singleton from anywhere. This can create a complex dependency tree, especially around initialization and certain systems in the program that expects the Singleton to be in a specific state.
You might think this is enough to say “never” use Singletons. To that, I say nay. As with any tool in the tool chest, weigh the pros and cons of your design and ensure it’s the right fit for your architecture.
Pros and cons to Singleton usage
Pros
- Make specific state or functionality global and accessible from anywhere
- Hides the constructor and some internals from callers
- Does not take up memory until the first request for the instance is made
Cons
- Singleton pattern causes additional complexities to handle asynchronous initialization that will be a headache
- Lifecycle management of the class contents is difficult; very little control on when or how it is used (or even created)
- Singletons are not extensible (does not allow inheritance)
- Remain in memory for the lifetime of the app once created
- Very difficult to test since they remain in memory
- Using Singletons for business logic will tightly couple variouns systems to that logic
The instanced Singleton
With the above in consideration, I would like to present to you the idea of an “Instanced” Singleton. A Singleton by nature under a specific umbrella “instance,” object. There can only be one Singleton in that specific instance and multiple instances per program. It still satisfies Singleton’s goals with the additional benefits of eventual deallocation with its’ parent. This counterbalances the cons of the “one singleton in memory” theme while still providing the same benefits.
Example
class InstancedSingleton {
private _config: any;
private async initialize() {
// do some one time initialization, this will only happen when a new parent instance is created!
this._config = {};
}
/** This is was initialized just once and is accessible globally now! */
public get config() {
return this._config;
}
}
/** The application will initialize a "core" instance to keep its
* global state and make the core "globally" available to all of its
* children. It in and of itself becomes a global instance although isn't
* quite a Singleton. Since it can be made "globally" available a new core
* could be created for your needs of multi-instanced programming
* (multi-user or multi-tenant application for example).
*/
class InstancedGlobalCore {
private _instancedSingleton: InstancedSingleton;
public async initialize() {
this._instancedSingleton = new InstancedSingleton();
await this._instancedSingleton.initialize();
}
public get singleton() {
return this._instancedSingleton;
}
}