Singleton Design Pattern
The Singleton pattern is classified as a creational design pattern because its primary purpose is to control the creation of objects. Specifically, the Singleton pattern ensures that a class has only one instance and provides a global point of access to that instance.
The Singleton pattern restricts the instantiation of a class to a single object. This is achieved by making the constructor private, thereby preventing external code from creating new instances of the class.
Singleton provides a globally accessible instance, typically through a static method. This method checks if the instance already exists; if not, it creates and returns it. Otherwise, it returns the existing instance.
Singleton manages the lifecycle of its instance. It decides when the instance is created (usually on first access) and ensures that it persists for the duration of the program (or as needed).
By ensuring that only one instance of the class exists, the Singleton pattern avoids issues related to the creation of multiple instances, such as inconsistent state, excessive resource usage, or conflicts in managing shared resources.
In the context of front-end web development, this pattern can be very useful for managing shared resources like application state, configurations, or service instances.
Practical Example: A Singleton Configuration Manager
Let's create a ConfigManager class that handles more complexity, such as managing different sets of URLs, Google Maps API keys, and potentially other configuration variables for three different environments: development, staging, and production.
Let’s say we are building a web application where various parts of the application need access to a shared configuration (e.g., API keys and URLs). We want to ensure that there is only one instance of this configuration throughout the application to avoid inconsistencies.
We can incorporate the Registry pattern to manage different environment configurations. This way, we can extend or modify the configurations without breaking the Open/Closed Principle (OCP). The Registry pattern complements Singleton by acting as a central storage for various objects or configurations, allowing them to be retrieved when needed. When combined, Singleton and Registry allow for the creation and management of unique instances of objects or configurations, each associated with a specific key or identifier.
Step 1: Create the interface
Step 2: Create the Configuration Classes
Each environment will have its own configuration class. These classes will hold the configuration details specific to that environment.
//Config.ts
export interface Config {
getApiUrl(): string;
getGoogleMapsApiKey(): string;
// Add other configuration methods as needed
}
//DevelopmentConfig.ts
import { Config } from "./Config";
export class DevelopmentConfig implements Config {
getApiUrl(): string {
return "https://dev.example.com/api";
}
getGoogleMapsApiKey(): string {
return "DEV_GOOGLE_MAPS_API_KEY";
}
}
//ProductionConfig.ts
export class ProductionConfig implements Config {
getApiUrl(): string {
return "https://prod.example.com/api";
}
getGoogleMapsApiKey(): string {
return "PROD_GOOGLE_MAPS_API_KEY";
}
}
//StagingConfig.ts
export class StagingConfig implements Config {
getApiUrl(): string {
return "https://staging.example.com/api";
}
getGoogleMapsApiKey(): string {
return "STAGING_GOOGLE_MAPS_API_KEY";
}
}
Step 3: Implement the Registry
The Registry pattern will act as a central place where different environment configurations are registered. It will provide a way to retrieve the appropriate configuration based on the environment.
export class ConfigRegistry {
private static instance: ConfigRegistry;
private static configs: { [key: string]: any } = {};
private constructor() {}
static getInstance(): ConfigRegistry {
if (!ConfigRegistry.instance) {
ConfigRegistry.instance = new ConfigRegistry();
}
return ConfigRegistry.instance;
}
registerConfig(env: string, config: any): void {
if (!ConfigRegistry.configs[env]) {
ConfigRegistry.configs[env] = config;
} else {
throw new Error(`Configuration for '${env}' is already registered.`);
}
}
getConfig(env: string): any {
const config = ConfigRegistry.configs[env];
if (!config) {
throw new Error(`No configuration registered for environment : ${env}`);
}
return config;
}
}
Step 4: Create the Singleton ConfigManager
The ConfigManager will use the ConfigRegistry to fetch the correct configuration based on the environment. It will be a singleton to ensure that there is only one instance managing the configuration.
import { ConfigRegistry } from "./ConfigRegistry";
export class ConfigManager {
private static instance: ConfigManager;
private config: any;
private constructor(environment: string) {
const registry = ConfigRegistry.getInstance();
this.config = registry.getConfig(environment);
}
static getInstance(environment: string): ConfigManager {
if (!ConfigManager.instance) {
ConfigManager.instance = new ConfigManager(environment);
}
return ConfigManager.instance;
}
getApiUrl(): string {
return this.config.getApiUrl();
}
getGoogleMapsApiKey(): string {
return this.config.getGoogleMapsApiKey();
}
}
Step 5: Register the Configurations
Step 6: Using the ConfigManager
Instantiate the ConfigManager with a specific environment key.
import { ConfigManager } from "./ConfigManager";
import { ConfigRegistry } from "./ConfigRegistry";
import { DevelopmentConfig } from "./DevelopmentConfig";
import { ProductionConfig } from "./ProductionConfig";
import { StagingConfig } from "./StagingConfig";
function main() {
const registry = ConfigRegistry.getInstance();
registry.registerConfig("development", new DevelopmentConfig());
registry.registerConfig("production", new ProductionConfig());
registry.registerConfig("staging", new StagingConfig());
//Instantiate the ConfigManager with a specific environment key
const environment = "production"; // Change this to 'development' or 'staging' as needed
const configManager = ConfigManager.getInstance(environment);
console.log(configManager.getApiUrl()); // Logs the API URL for the specified environment
console.log(configManager.getGoogleMapsApiKey()); // Logs the Google Maps API key for the specified environment
}
main();
Explanation:
Interface (Config): Defines the methods that must be implemented by all configuration classes, ensuring a consistent API.
Concrete Config Classes (DevelopmentConfig, ProductionConfig, StagingConfig): Implement the Config interface, providing specific configurations for each environment.
Registry (ConfigRegistry): Manages the registration and retrieval of configuration instances, ensuring that only one instance of each configuration is used.
Singleton Manager (ConfigManager): Retrieves the configuration based on the environment key and provides methods to access configuration values.
Conclusion:
Benefits of Singleton with Registry:
Different configurations or objects. This makes it easy to extend or change configurations without modifying the core logic.
Single Responsibility Principle (SRP): Each class has a single responsibility—ConfigRegistry manages registration and retrieval, while ConfigManager accesses configurations.
Open/Closed Principle (OCP): The design is open for extension. You can add new configurations without modifying existing classes, adhering to OCP.
Singleton Consistency: The Singleton pattern ensures that only one instance of both ConfigRegistry and ConfigManager exists, maintaining consistency across the application.
The GITHUB LINK for the sample code reference.