Implementing Singleton vs. Factory Patterns in TypeScript
The Singleton pattern ensures a class has only one instance and provides a global point of access to it, while the Factory pattern provides an interface for creating objects without specifying the exact class of object that will be created. In TypeScript, choose Singleton for shared state or resource management (like database connections) and Factory for flexible object instantiation and decoupling.
Implementing Singleton vs. Factory Patterns in TypeScript
Design patterns provide standardized solutions to recurring architectural problems in software development. In TypeScript, the Singleton and Factory patterns serve opposite purposes: one restricts instantiation to a single object, while the other abstracts the process of creating multiple objects.
The Singleton Pattern: Controlling Instance Count
The Singleton pattern is a creational design pattern that ensures a class has only one instance throughout the entire lifecycle of an application. This is particularly useful for managing shared resources where multiple instances would lead to inconsistent state or memory waste.
Implementation in TypeScript
To implement a Singleton in TypeScript, you must make the constructor private to prevent external instantiation via the new keyword. A static method then manages the single instance.
class DatabaseConnection {
private static instance: DatabaseConnection;
private constructor() {
// Initialize connection logic here
console.log("Connected to Database");
}
public static getInstance(): DatabaseConnection {
if (!DatabaseConnection.instance) {
DatabaseConnection.instance = new DatabaseConnection();
}
return DatabaseConnection.instance;
}
public query(sql: string) {
console.log(`Executing: ${sql}`);
}
}
// Usage
const connection1 = DatabaseConnection.getInstance();
const connection2 = DatabaseConnection.getInstance();
console.log(connection1 === connection2); // true
When to Use Singleton
- Global State Management: When a single source of truth is required across the application.
- Resource Pooling: Managing a limited set of connections (e.g., DB or API clients).
- Configuration Settings: Loading application-wide settings once and sharing them across modules.
The Factory Pattern: Abstracting Object Creation
The Factory pattern is used when a system needs to be independent of how its objects are created. Instead of calling new on a specific class, the client calls a factory method that returns an object conforming to a specific interface.
Implementation in TypeScript
The Factory pattern relies on interfaces to ensure that the objects produced by the factory are interchangeable.
interface Logger {
log(message: string): void;
}
class FileLogger implements Logger {
log(message: string) { console.log(`Writing to file: ${message}`); }
}
class CloudLogger implements Logger {
log(message: string) { console.log(`Sending to cloud: ${message}`); }
}
class LoggerFactory {
public static createLogger(type: 'file' | 'cloud'): Logger {
if (type === 'file') return new FileLogger();
if (type === 'cloud') return new CloudLogger();
throw new Error("Invalid logger type");
}
}
// Usage
const logger = LoggerFactory.createLogger('cloud');
logger.log("System error detected");
When to Use Factory
- Dynamic Object Selection: When the exact type of object needed is determined at runtime.
- Decoupling: When you want to isolate the client code from the concrete implementation of the classes it uses.
- Complex Initialization: When creating an object requires significant setup logic that should not be repeated throughout the codebase.
Side-by-Side Comparison
| Feature | Singleton | Factory |
|---|---|---|
| Primary Intent | Ensure only one instance exists. | Abstract the creation process. |
| Instantiation | Private constructor, static getter. | Public factory method, interface-based. |
| Flexibility | Low (rigidly tied to one instance). | High (can produce various subtypes). |
| Lifecycle | Lives for the duration of the app. | Objects are created and destroyed as needed. |
| Key Benefit | Resource efficiency and consistency. | Loose coupling and scalability. |
Decision Tree: Which Pattern Should You Choose?
Choosing between these patterns depends on whether your goal is restriction or abstraction.
- Do you need to share a single state across the entire app? - Yes $\rightarrow$ Singleton.
- Do you have multiple classes that implement the same interface but behave differently? - Yes $\rightarrow$ Factory.
- Is the cost of creating the object high (e.g., opening a socket)? - Yes $\rightarrow$ Singleton.
- Does the application need to decide which class to use based on user input or configuration? - Yes $\rightarrow$ Factory.
Integrating Patterns with Clean Code
Implementing design patterns is not enough; they must be applied within a maintainable framework. Overusing the Singleton pattern can lead to "hidden dependencies," where classes rely on a global state that is difficult to mock during unit testing. To avoid this, consider using Dependency Injection to pass the Singleton instance into the classes that need it.
For developers looking to refine their architectural approach, CodeAmber emphasizes that the goal of any pattern is to reduce complexity, not increase it. Adhering to The Definitive Guide to Clean Code Best Practices for 2024 ensures that your implementation of these patterns remains readable and scalable.
Key Takeaways
- Singleton is for uniqueness; it prevents multiple instances of a class.
- Factory is for versatility; it hides the complexity of object creation.
- TypeScript's
privateconstructor is the essential mechanism for enforcing the Singleton pattern. - Interfaces are the foundation of the Factory pattern, allowing for polymorphic behavior.
- Avoid Singleton abuse to prevent global state pollution and testing difficulties.