DEV Community

Zane
Zane

Posted on

Singleton Pattern: The Lonely Chapter of Design Patterns

The Singleton pattern is like the control tower of an application, ensuring that all operations are conducted under a unified command, avoiding chaos and resource waste. It is the lone ranger in the family of creational patterns, focusing on building a unique and reliable instance, bringing order and efficiency to the complex software world.

Tower

Core Value and Definition of Singleton Pattern

The core of the Singleton pattern is to ensure that a class has only one instance and provides a global access point to this instance for the entire application. It's like the only post office in the city center, where all letters are sent and received, ensuring simple and efficient mail processing. Its existence not only saves resources but also ensures consistency in data and behavior.

Main Features of Singleton Pattern

  • Unique Instance: Ensures that a class has only one instance in the application, similar to the only moon on Earth, avoiding conflicts between instances.
  • Global Access Point: Provides a global access point, allowing access to this unique instance from anywhere in the application, like the moon clearly visible in the night sky.

Analogy of Singleton Pattern in Daily Life

Imagine a unique landmark in your city center, like the Eiffel Tower or the Statue of Liberty, which is unique and a shared memory. The Singleton pattern is similar; it is recognizable in every corner of the software because it is unique and shared. Just like every city has a central post office serving all residents, the Singleton pattern serves the entire application, ensuring system efficiency and order.

Structure and Composition of Singleton Pattern

In this chapter, we will explore the mystery of the Singleton pattern. Let's start with its construction, like the first step in solving an ancient puzzle.

Basic Structure of Singleton Pattern

The Singleton pattern typically involves the following components:

  1. Private Constructor: Ensures that the class cannot be instantiated from outside;
  2. A Private Static Variable (usually a private static property in TypeScript) to hold the unique instance of the class;
  3. A Public Static Method (usually named getInstance) to provide a global access point.

UML Class Diagram and Interpretation of Singleton Pattern

The UML class diagram of the Singleton pattern is like a book with only one page opened. On this page, it clearly records how to guard a treasure — that unique class instance.

Singleton

In this diagram, you will see:

  • A Class whose constructor is privatized, like a spell sealed with secret words in a book.
  • A Private Static Variable, like the treasure location silently guarded in the book, ensuring only one treasure exists.
  • A Public Static Method, like a spell to reveal the treasure. If the treasure is undiscovered, it will cast magic to summon it; if the treasure is already in the open, it will guide you directly to it.

Implementation of Singleton Pattern

Let's move from theory to practice. The following TypeScript code demonstrates how to implement a Singleton pattern:

function createSingleton<T extends new (...args: any[]) => any>(constructor: T): (...args: ConstructorParameters<T>) => InstanceType<T> {
  // Variable to store the instance
  let instance: InstanceType<T>;

  // Return a new function
  return function (...args: ConstructorParameters<T>): InstanceType<T> {
    // If the instance does not exist, create a new one
    if (!instance) {
      instance = new constructor(...args);
    }
    // Return the instance
    return instance;
  };
}
Enter fullscreen mode Exit fullscreen mode
  1. The createSingleton function takes a constructor as a parameter;
  2. It returns a new function. This new function checks if an instance already exists:
    • If it exists, it returns the existing instance;
    • If it does not exist, it creates a new instance and then returns it.

In this way, no matter how many times we try to create an instance, we always get the same instance, as promised by the Singleton pattern.

Practical Exercise of Singleton Pattern — Global State Manager

In our software world, state is like a ship sailing at sea, needing to navigate safely under orderly and clear guidance. But if every component tries to navigate independently, it's like every captain trying to control the lighthouse themselves — chaos ensues! At this point, we need a global state manager to act as the lighthouse for the entire application, guiding the safe flow of state.

Background Introduction of the Case

In complex front-end applications, component state management is like sailing at sea, where each component may have its own direction, and their states may need to be shared and synchronized. If each component tries to navigate independently, it quickly becomes uncontrollable. This is why a global state manager becomes an indispensable lighthouse in software navigation. It provides a central location for managing and synchronizing the state of the entire application, like a harbor lighthouse guiding the ships.

Application of Singleton Pattern in State Management

Here, the Singleton pattern is like the light in the lighthouse, unique and crucial. It ensures that our global state manager is unique, like the central lighthouse in the software sea. No matter where you are in the software, you manage the state through this unique lighthouse, ensuring that all state changes are orderly and predictable.

Code Demonstration

Next, we will demonstrate through code how to use the Singleton pattern to create and use a global state manager.

First, we define a Store class responsible for managing state, executing changes, and handling actions:

class Store<S extends object = State, M extends Mutations<S> = Mutations<S>, A extends Actions<S> = Actions<S>> {
  private state: S;
  private mutations: M;
  private actions: A;

  // In the constructor, initialize state, mutations, and actions.
  constructor(options: StoreOptions<S, M, A>) {
    this.state = reactive(options.state) as S;
    this.mutations = options.mutations;
    this.actions = options.actions;
  }

  // getState method returns the current state.
  getState(): S {
    return this.state;
  }

  // commit method is used to call a mutation. It takes a mutation type and a payload.
  commit(type: string, payload: any): void {
    const mutation = this.mutations[type];
    if (!mutation) {
      throw new Error(`Mutation "${type}" does not exist`);
    }
    mutation(this.state, payload);
  }

  // dispatch method is used to call an action. It takes an action type and a payload.
  dispatch(type: string, payload: any): void {
    const action = this.actions[type];
    if (!action) {
      throw new Error(`Action "${type}" does not exist`);
    }
    action({ state: this.state, commit: this.commit.bind(this) }, payload);
  }
}
Enter fullscreen mode Exit fullscreen mode

Then, we use the createSingleton function to ensure that our Store instance is unique:

// Create a singleton Store instance
const store = createSingleton(Store<State>)({
  // Definition of state
  state: {
    count: 0
  },
  // Definition of mutations
  mutations: {
    increment(state, payload) {
      state.count += payload;
    }
  },
  // Definition of actions
  actions: {
    incrementAsync({ commit }, payload) {
      commit('increment', payload);
    }
  },
});
Enter fullscreen mode Exit fullscreen mode

Finally, in our Vue component, we can use this global state manager as follows:

<script setup lang="ts">
import { computed } from 'vue';
import { store } from '@/stores'

// Define a computed property that returns the count state from the store
const count = computed(() => store.getState().count);

// Use store methods to change the state
const increment = () => {
  store.commit('increment', 1);
};

const decrement = () => {
  store.dispatch('incrementAsync', -1);
};

</script>

<template>
 <div class="flex items-center justify-center h-screen bg-gray-100">
    <div class="p-6 bg-white shadow-md rounded-md">
      <h2 class="text-2xl font-bold mb-4 text-gray-700">Counter</h2>
      <div class="flex items-center space-x-4">
        <button @click="decrement" class="px-4 py-2 bg-red-500 text-white rounded">-</button>
        <span class="text-xl font-bold text-gray-700">{{ count }}</span>
        <button @click="increment" class="px-4 py-2 bg-green-500 text-white rounded">+</button>
      </div>
    </div>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Through these codes, we demonstrated how to use the Singleton pattern to create an efficient and reliable global state manager. This state manager is like the central lighthouse of our software sea, ensuring that the flow of state is orderly and safe, guiding every state ship to sail safely.

Store

Case Study of Singleton Pattern in Practice

We have just designed a lighthouse (Singleton pattern), now let's see how it illuminates ships in actual navigation. We are not analyzing ordinary code, but the code that plays a key role in excellent frameworks. This time, our focus is on Vuex, a famous Vue.js state management library. Vuex uses the Singleton pattern to manage the application's state, ensuring that the state throughout the application is unified and predictable.

Application Analysis of Singleton Pattern in Vuex Library

In Vuex, the Singleton pattern is not just a pattern; it is the heart of state management. There is a central repository (Store), which is the only source of state for the entire application. Every component communicates through this central repository, regardless of their position in the application. Imagine this as the central control tower of a large airport, where all planes (components) follow its command.

Source Code Analysis

In Vuex, the Store class is the core of global state management. The constructor of Store is responsible for initializing the global state, registering mutations, actions, and getters, and connecting the state of the entire application.

Vuex

Below is the key part of the constructor:

function createStore (options) {
  return new Store(options)
}

class Store {
  constructor (options = {}) {  
    this._committing = false
    this._actions = Object.create(null)
    this._actionSubscribers = []
    this._mutations = Object.create(null)
    this._wrappedGetters = Object.create(null)
    this._modules = new ModuleCollection(options)
    this._modulesNamespaceMap = Object.create(null)
    this._subscribers = []
    this._makeLocalGettersCache = Object.create(null)

    // Bind commit and dispatch methods 
    const { dispatch, commit } = this
    this.dispatch = function boundDispatch (type, payload) {
      return dispatch.call(store, type, payload)
    }
    this.commit = function boundCommit (type, payload, options) {
      return commit.call(store, type, payload, options)
    }

    const state = this._modules.root.state

    // Initialize root module, recursively register all submodules, and collect all module getters
    installModule(this, state, [], this._modules.root)

    // Initialize store state, responsible for reactivity (also registers _wrappedGetters as computed properties)
    resetStoreState(this, state)

    // Apply plugins
    plugins.forEach(plugin => plugin(this))
  }

  install (app, injectKey) {
    // ... Code to install Vuex ...
  }

  // Other methods
  // ...
}
Enter fullscreen mode Exit fullscreen mode

In this code, we see the Store class of Vuex. This Store class is the central repository of the application. When you call the createStore method, Vuex ensures that a Store instance is created, which is the only source of state for the entire application.

  • this._actions, this._mutations, etc., are used to store the logic for state changes.
  • installModule is responsible for registering all modules, ensuring modular state management.
  • resetStoreState is responsible for initializing the state, ensuring state reactivity.
  • plugins.forEach(plugin => plugin(this)) allows developers to extend Vuex functionality through plugins.

Let's delve a little deeper and look at the commit and dispatch methods in the Store class. These two methods are the core of Vuex state management. commit is used for synchronous operations, while dispatch is used for asynchronous operations. But regardless of the operation type, they are coordinated through this unique Store instance.

Practical Application Scenarios of Singleton Pattern

The Singleton pattern has a wide range of applications in software development. By ensuring that a class has only one instance, it simplifies the management of global resources. Here are some typical applications in software development:

  • Configuration Manager: Ensures that there is only one configuration manager throughout the application, making it easy for various components to access and use configuration information.
  • Logger: Manages logs through a global logger, simplifying log maintenance and improving performance.
  • Thread Pool: Controls the number of threads, avoiding frequent creation and destruction of threads, improving system resource utilization and performance.
  • Cache Management: A global cache instance manages data caching, improving data access speed and reducing database pressure.
  • Hardware Interface Access: For accessing hardware resources like printers and serial ports, using a singleton ensures that there is only one access instance globally, avoiding conflicts.

How to Identify Scenarios Suitable for Singleton Pattern

To identify scenarios suitable for the Singleton pattern, the key is to find situations that require a globally unique instance. Here are some criteria:

  • The class controls a resource or service, and there should only be one "access point" throughout the application.
  • You are writing code to "ensure only one instance is created," which is often a sign of using the Singleton pattern.
  • Managing global state, such as state management in front-end frameworks.

Disadvantages of Singleton Pattern and Coping Strategies

Although the Singleton pattern is powerful, it also has limitations, such as global state management issues and difficulties in unit testing.

Coping Strategy — Dependency Injection

  • Control Singleton Creation: Strictly control the creation and access of singletons through private constructors and public static methods.
  • Use Dependency Injection: In unit testing, replace singleton instances with dependency injection to improve test convenience.
  • Limit Modifications to Global State: Modify the state through clearly defined methods, avoiding direct modifications to the global state.

In unit testing, using dependency injection to replace singleton instances is an effective strategy to solve testing problems. It not only improves the flexibility and testability of the code but also retains the core advantages of the Singleton pattern. See the following dependency injection example:

class DIContainer {
  private static instances: Record<string, any> = {};

  // Register method for registering singleton instances
  public static register<T>(key: string, creator: () => T): void {
    if (!DIContainer.instances[key]) {
      DIContainer.instances[key] = creator();
    }
  }

  // Resolve method for obtaining singleton instances
  public static resolve<T>(key: string): T {
    if (!DIContainer.instances[key]) {
      throw new Error(`No instance found for key: ${key}`);
    }
    return DIContainer.instances[key];
  }
}

// Example: Create Store instance and register
function createStoreInstance() {
  return new Store<State>({
    state: { count: 0 },
    // ... Other state and methods ...
  });
}

// Create Store instance and register to DIContainer
DIContainer.register<Store<State>>('Store', createStoreInstance);

// In Vue component, get Store instance through DIContainer
const store = DIContainer.resolve<Store<State>>('Store');
Enter fullscreen mode Exit fullscreen mode

In this example, DIContainer provides a flexible way to manage singletons. We can register and resolve different instances in development and testing environments as needed. This not only retains the advantages of the Singleton pattern but also enhances the flexibility and testability of the code.

Essence Review and Value Re-recognition of Singleton Pattern

The Singleton pattern ensures that a class has only one instance and provides a global access point in a unique way. It not only ensures the consistency of global state but also greatly reduces resource consumption. Through our global state manager and Vuex case, we have seen the powerful application of the Singleton pattern in actual projects. But at the same time, we have also recognized its challenges and how to overcome these challenges through strategies such as dependency injection.

Top comments (0)