Skip to main content
โšก Calmops

Creational Design Patterns in JavaScript

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

  1. Use Factory for object creation:

    // โœ… Good
    const obj = Factory.create(type);
    
    // โŒ Bad
    const obj = new ConcreteClass();
    
  2. Use Builder for complex objects:

    // โœ… Good
    const obj = new Builder()
      .setProperty1(value1)
      .setProperty2(value2)
      .build();
    
    // โŒ Bad
    const obj = new ComplexClass(value1, value2, value3, ...);
    
  3. Use Singleton for shared resources:

    // โœ… Good
    const db = Database.getInstance();
    
    // โŒ Bad
    const db = new Database();
    

Common Mistakes

  1. Overusing Singleton:

    // โŒ Bad - everything as Singleton
    class Service { }
    Service.instance = new Service();
    
    // โœ… Good - use when necessary
    class Database { }
    Database.instance = new Database();
    
  2. 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

Next Steps

Comments