Marcell Ciszek Druzynski

Singleton

The Singleton pattern ensures that only one instance of a class is created and provides a global point of access to the instance.

November 14, 2023

Singleton pattern

The singleton pattern is categorized under creational design patterns. This pattern restricts the instantiation of specific classes, ensuring that only one unique instance of the object can exist. Essentially, it functions as a global variable accessible from any part of the program.

Consider a scenario with a dog as a Singleton. When we refer to this Singleton, we are consistently pointing to the same instance. Any changes made, such as incrementing the dog's age in one module, will reflect universally across all instances where the dog Singleton is employed.

1let instance: Dog;
2class Dog {
3 private name: string;
4 private age: number;
5
6 constructor() {
7 if (instance) {
8 throw new TypeError("Cannot create multiple instances");
9 }
10 this.name = "Bobby";
11 this.age = 1;
12 instance = this;
13 }
14
15 getName(): string {
16 return this.name;
17 }
18 getAge(): number {
19 return this.age;
20 }
21 birthday() {
22 this.age += 1;
23 }
24}
25export default new Dog();
1let instance: Dog;
2class Dog {
3 private name: string;
4 private age: number;
5
6 constructor() {
7 if (instance) {
8 throw new TypeError("Cannot create multiple instances");
9 }
10 this.name = "Bobby";
11 this.age = 1;
12 instance = this;
13 }
14
15 getName(): string {
16 return this.name;
17 }
18 getAge(): number {
19 return this.age;
20 }
21 birthday() {
22 this.age += 1;
23 }
24}
25export default new Dog();

This singleton dog can be globally shared across multiple modules. When modules import the singleton object, they all reference the same instance. A single method, birthday(), is responsible for modifying the age property. Each time any module invokes the birthday() method, the age increases by one, and this change becomes visible throughout all modules utilizing the singleton.

It's important to note that creating a singleton doesn't necessarily require the use of es6 class syntax; a simple object literal suffices and works equally well.

1let age = 1;
2const name = "Bobby";
3
4const dog = {
5 getAge: () => age,
6 getName: () => name,
7 birthday: () => (age += 1),
8};
9
10export default dog;
1let age = 1;
2const name = "Bobby";
3
4const dog = {
5 getAge: () => age,
6 getName: () => name,
7 birthday: () => (age += 1),
8};
9
10export default dog;

The concept of a singleton is frequently employed in our applications, often unintentionally. When an object literal is exported from one module, it essentially becomes a singleton. If another module imports this object and makes modifications to its properties, these changes will have a widespread impact throughout the entire program.

1export const person = {
2 name: "Mike",
3 age: 22,
4};
1export const person = {
2 name: "Mike",
3 age: 22,
4};

To safeguard against unintentional property mutations, consider using Object.freeze:

1const person = {
2 name: "Mike",
3 age: 22,
4};
5export default Object.freeze(person);
1const person = {
2 name: "Mike",
3 age: 22,
4};
5export default Object.freeze(person);

Common Use Cases

Why opt for a single instance of an object?

The primary rationale is to regulate access to a shared resource, such as a database or file, to avoid potential issues. Having multiple instances connected to a database, for instance, could lead to chaos. Instead, employing a singleton ensures only one instance exists within our database.

1let instance: DbConnection;
2
3class DbConnection {
4 private uri: string;
5 private isConnected: boolean;
6
7 constructor(uri: string) {
8 if (instance) {
9 throw new Error("Instance already exists");
10 }
11 this.uri = uri;
12 this.isConnected = false;
13 instance = this;
14 }
15
16 connect() {
17 this.isConnected = true;
18 console.log(`DB Connection to ${this.uri}`);
19 }
20
21 disconnect() {
22 this.isConnected = false;
23 console.log(`DB Disconnected from ${this.uri}`);
24 }
25
26 getUri(): string {
27 return this.uri;
28 }
29
30 getIsConnected(): boolean {
31 return this.isConnected;
32 }
33}
34
35export default Object.freeze(
36 new DbConnection("postgres://postgres:postgres@localhost:5432/postgres"),
37);
1let instance: DbConnection;
2
3class DbConnection {
4 private uri: string;
5 private isConnected: boolean;
6
7 constructor(uri: string) {
8 if (instance) {
9 throw new Error("Instance already exists");
10 }
11 this.uri = uri;
12 this.isConnected = false;
13 instance = this;
14 }
15
16 connect() {
17 this.isConnected = true;
18 console.log(`DB Connection to ${this.uri}`);
19 }
20
21 disconnect() {
22 this.isConnected = false;
23 console.log(`DB Disconnected from ${this.uri}`);
24 }
25
26 getUri(): string {
27 return this.uri;
28 }
29
30 getIsConnected(): boolean {
31 return this.isConnected;
32 }
33}
34
35export default Object.freeze(
36 new DbConnection("postgres://postgres:postgres@localhost:5432/postgres"),
37);

This ensures a single, controlled instance of the DbConnection class with a designated URI for our PostgreSQL database.

Singletons in the Real World

Consider the analogy of a person having only one father. It's an inherent singleton scenario; an individual can't have multiple fathers. From this perspective, a father is essentially a singleton.

| Pros | Cons | | :--------------------------------- | :------------------------------------------------------------------------------- | | Single Instance, Memory Efficiency | Solves Multiple Concerns, Potentially Violates Single Responsibility Principle | | Global Accessibility | Increased Coupling, Components Know Too Much About Each Other |

Summary

  • Memory Efficiency: Singleton patterns contribute to memory optimization by allowing only one instance. This means memory is allocated once and persists throughout the entire program, preventing unnecessary duplication.

  • Default Singletons (Modules): Since ES2015, modules inherently function as singletons. There's often no need to explicitly create singletons, as the language incorporates this feature.

  • Exercise Caution in Value Modification: Altering a value in a singleton can have widespread consequences. Changes propagate across all program modules using the singleton, potentially leading to unexpected behaviors.

  • Testing Challenges: Singleton patterns can pose challenges in testing. With only one allowed instance, state changes in the singleton may impact tests, leading to failures.

  • Global Variable Analogy: Singletons are akin to global variables, accessible throughout the entire program. Care must be exercised to avoid unintended overwrites of values, as such changes can have a broad impact on the entire program.

References