Creational Design Patterns in JavaScript
Creational patterns focus on object creation mechanisms. This article covers Singleton, Factory, Abstract Factory, Builder, and Prototype patterns.
Introduction
Creational patterns provide:
- Flexible object creation
- Encapsulation of creation logic
- Reduced coupling
- Reusable creation mechanisms
- Controlled instantiation
Understanding these patterns helps you:
- Create objects efficiently
- Manage object creation
- Implement flexible systems
- Reduce code duplication
- Improve maintainability
Singleton Pattern
Basic Singleton
// โ
Good: Basic Singleton
class Database {
constructor() {
if (Database.instance) {
return Database.instance;
}
this.connection = null;
Database.instance = this;
}
connect() {
this.connection = 'Connected to database';
console.log(this.connection);
}
}
const db1 = new Database();
const db2 = new Database();
console.log(db1 === db2); // true (same instance)
db1.connect(); // Connected to database
Lazy Singleton
// โ
Good: Lazy initialization
class Logger {
static getInstance() {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
log(message) {
console.log(`[LOG] ${message}`);
}
}
// Instance created only when needed
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();
console.log(logger1 === logger2); // true
logger1.log('Hello'); // [LOG] Hello
Module Pattern (Singleton)
// โ
Good: Module pattern for Singleton
const ConfigManager = (() => {
let instance;
function createInstance() {
return {
config: {},
get(key) {
return this.config[key];
},
set(key, value) {
this.config[key] = value;
}
};
}
return {
getInstance() {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
const config1 = ConfigManager.getInstance();
const config2 = ConfigManager.getInstance();
console.log(config1 === config2); // true
config1.set('apiUrl', 'https://api.example.com');
console.log(config2.get('apiUrl')); // https://api.example.com
Factory Pattern
Simple Factory
// โ
Good: Simple Factory
class AnimalFactory {
static createAnimal(type) {
switch (type) {
case 'dog':
return new Dog();
case 'cat':
return new Cat();
case 'bird':
return new Bird();
default:
throw new Error(`Unknown animal type: ${type}`);
}
}
}
class Dog {
speak() {
return 'Woof!';
}
}
class Cat {
speak() {
return 'Meow!';
}
}
class Bird {
speak() {
return 'Tweet!';
}
}
// Usage
const dog = AnimalFactory.createAnimal('dog');
console.log(dog.speak()); // Woof!
const cat = AnimalFactory.createAnimal('cat');
console.log(cat.speak()); // Meow!
Factory Method
// โ
Good: Factory Method pattern
class Creator {
createProduct() {
throw new Error('Must implement createProduct');
}
operation() {
const product = this.createProduct();
return product.operation();
}
}
class ConcreteCreatorA extends Creator {
createProduct() {
return new ConcreteProductA();
}
}
class ConcreteCreatorB extends Creator {
createProduct() {
return new ConcreteProductB();
}
}
class ConcreteProductA {
operation() {
return 'Result from Product A';
}
}
class ConcreteProductB {
operation() {
return 'Result from Product B';
}
}
// Usage
const creatorA = new ConcreteCreatorA();
console.log(creatorA.operation()); // Result from Product A
const creatorB = new ConcreteCreatorB();
console.log(creatorB.operation()); // Result from Product B
Dynamic Factory
// โ
Good: Dynamic factory with registration
class DynamicFactory {
constructor() {
this.creators = {};
}
register(type, creator) {
this.creators[type] = creator;
}
create(type, ...args) {
const creator = this.creators[type];
if (!creator) {
throw new Error(`Unknown type: ${type}`);
}
return new creator(...args);
}
}
class User {
constructor(name) {
this.name = name;
}
}
class Admin {
constructor(name) {
this.name = name;
this.role = 'admin';
}
}
// Usage
const factory = new DynamicFactory();
factory.register('user', User);
factory.register('admin', Admin);
const user = factory.create('user', 'John');
const admin = factory.create('admin', 'Jane');
console.log(user); // User { name: 'John' }
console.log(admin); // Admin { name: 'Jane', role: 'admin' }
Abstract Factory Pattern
// โ
Good: Abstract Factory
class UIFactory {
createButton() {
throw new Error('Must implement createButton');
}
createCheckbox() {
throw new Error('Must implement createCheckbox');
}
}
class WindowsUIFactory extends UIFactory {
createButton() {
return new WindowsButton();
}
createCheckbox() {
return new WindowsCheckbox();
}
}
class MacUIFactory extends UIFactory {
createButton() {
return new MacButton();
}
createCheckbox() {
return new MacCheckbox();
}
}
class WindowsButton {
render() {
return 'Windows Button';
}
}
class MacButton {
render() {
return 'Mac Button';
}
}
class WindowsCheckbox {
render() {
return 'Windows Checkbox';
}
}
class MacCheckbox {
render() {
return 'Mac Checkbox';
}
}
// Usage
function createUI(factory) {
const button = factory.createButton();
const checkbox = factory.createCheckbox();
return { button, checkbox };
}
const windowsUI = createUI(new WindowsUIFactory());
console.log(windowsUI.button.render()); // Windows Button
console.log(windowsUI.checkbox.render()); // Windows Checkbox
const macUI = createUI(new MacUIFactory());
console.log(macUI.button.render()); // Mac Button
console.log(macUI.checkbox.render()); // Mac Checkbox
Builder Pattern
Basic Builder
// โ
Good: Builder pattern
class User {
constructor(builder) {
this.name = builder.name;
this.email = builder.email;
this.age = builder.age;
this.role = builder.role;
}
}
class UserBuilder {
constructor(name) {
this.name = name;
}
setEmail(email) {
this.email = email;
return this;
}
setAge(age) {
this.age = age;
return this;
}
setRole(role) {
this.role = role;
return this;
}
build() {
return new User(this);
}
}
// Usage
const user = new UserBuilder('John')
.setEmail('[email protected]')
.setAge(30)
.setRole('admin')
.build();
console.log(user);
// User {
// name: 'John',
// email: '[email protected]',
// age: 30,
// role: 'admin'
// }
Complex Builder
// โ
Good: Complex builder for configuration
class DatabaseConfig {
constructor(builder) {
this.host = builder.host;
this.port = builder.port;
this.database = builder.database;
this.username = builder.username;
this.password = builder.password;
this.ssl = builder.ssl;
this.timeout = builder.timeout;
this.pool = builder.pool;
}
}
class DatabaseConfigBuilder {
constructor(host) {
this.host = host;
this.port = 5432;
this.ssl = false;
this.timeout = 5000;
this.pool = 10;
}
setPort(port) {
this.port = port;
return this;
}
setDatabase(database) {
this.database = database;
return this;
}
setCredentials(username, password) {
this.username = username;
this.password = password;
return this;
}
enableSSL() {
this.ssl = true;
return this;
}
setTimeout(timeout) {
this.timeout = timeout;
return this;
}
setPoolSize(pool) {
this.pool = pool;
return this;
}
build() {
return new DatabaseConfig(this);
}
}
// Usage
const config = new DatabaseConfigBuilder('localhost')
.setPort(5432)
.setDatabase('myapp')
.setCredentials('user', 'password')
.enableSSL()
.setTimeout(10000)
.setPoolSize(20)
.build();
console.log(config);
Prototype Pattern
Basic Prototype
// โ
Good: Prototype pattern
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
clone() {
return new User(this.name, this.email);
}
getInfo() {
return `${this.name} (${this.email})`;
}
}
// Usage
const user1 = new User('John', '[email protected]');
const user2 = user1.clone();
user2.name = 'Jane';
console.log(user1.getInfo()); // John ([email protected])
console.log(user2.getInfo()); // Jane ([email protected])
Deep Clone Prototype
// โ
Good: Deep clone prototype
class ComplexObject {
constructor(data) {
this.data = data;
}
clone() {
return new ComplexObject(JSON.parse(JSON.stringify(this.data)));
}
}
// Usage
const original = new ComplexObject({
name: 'John',
hobbies: ['reading', 'coding'],
address: { city: 'New York', zip: '10001' }
});
const cloned = original.clone();
cloned.data.hobbies.push('gaming');
cloned.data.address.city = 'Boston';
console.log(original.data);
// { name: 'John', hobbies: ['reading', 'coding'], address: { city: 'New York', zip: '10001' } }
console.log(cloned.data);
// { name: 'John', hobbies: ['reading', 'coding', 'gaming'], address: { city: 'Boston', zip: '10001' } }
Practical Examples
Logger Factory
// โ
Good: Logger factory
class Logger {
log(message) {
throw new Error('Must implement log');
}
}
class ConsoleLogger extends Logger {
log(message) {
console.log(`[CONSOLE] ${message}`);
}
}
class FileLogger extends Logger {
log(message) {
console.log(`[FILE] ${message}`);
}
}
class LoggerFactory {
static createLogger(type) {
switch (type) {
case 'console':
return new ConsoleLogger();
case 'file':
return new FileLogger();
default:
throw new Error(`Unknown logger type: ${type}`);
}
}
}
// Usage
const consoleLogger = LoggerFactory.createLogger('console');
consoleLogger.log('Hello'); // [CONSOLE] Hello
const fileLogger = LoggerFactory.createLogger('file');
fileLogger.log('Hello'); // [FILE] Hello
HTTP Client Builder
// โ
Good: HTTP client builder
class HttpClient {
constructor(builder) {
this.baseUrl = builder.baseUrl;
this.timeout = builder.timeout;
this.headers = builder.headers;
this.interceptors = builder.interceptors;
}
async get(url) {
console.log(`GET ${this.baseUrl}${url}`);
}
}
class HttpClientBuilder {
constructor(baseUrl) {
this.baseUrl = baseUrl;
this.timeout = 5000;
this.headers = {};
this.interceptors = [];
}
setTimeout(timeout) {
this.timeout = timeout;
return this;
}
addHeader(key, value) {
this.headers[key] = value;
return this;
}
addInterceptor(interceptor) {
this.interceptors.push(interceptor);
return this;
}
build() {
return new HttpClient(this);
}
}
// Usage
const client = new HttpClientBuilder('https://api.example.com')
.setTimeout(10000)
.addHeader('Authorization', 'Bearer token')
.addHeader('Content-Type', 'application/json')
.addInterceptor((request) => console.log('Request:', request))
.build();
client.get('/users');
Best Practices
-
Use Factory for object creation:
// โ Good const obj = Factory.create(type); // โ Bad const obj = new ConcreteClass(); -
Use Builder for complex objects:
// โ Good const obj = new Builder() .setProperty1(value1) .setProperty2(value2) .build(); // โ Bad const obj = new ComplexClass(value1, value2, value3, ...); -
Use Singleton for shared resources:
// โ Good const db = Database.getInstance(); // โ Bad const db = new Database();
Common Mistakes
-
Overusing Singleton:
// โ Bad - everything as Singleton class Service { } Service.instance = new Service(); // โ Good - use when necessary class Database { } Database.instance = new Database(); -
Builder without fluent interface:
// โ Bad const builder = new Builder(); builder.setName('John'); builder.setEmail('[email protected]'); // โ Good const obj = new Builder() .setName('John') .setEmail('[email protected]') .build();
Summary
Creational patterns manage object creation. Key takeaways:
- Singleton: Single instance
- Factory: Encapsulate creation
- Abstract Factory: Family of objects
- Builder: Complex object construction
- Prototype: Clone objects
- Improves flexibility
- Reduces coupling
- Encapsulates creation logic
Related Resources
- Creational Patterns - Refactoring Guru
- Factory Pattern - MDN
- Builder Pattern - Wikipedia
- Singleton Pattern - Wikipedia
- Design Patterns in JavaScript
Next Steps
- Learn about Structural Design Patterns
- Explore Behavioral Design Patterns
- Study SOLID Principles
- Practice factory patterns
- Build reusable builders
Comments