Docs

11.5-Class-Patterns

8.5 Class Patterns

Overview

Design patterns using ES6+ classes enable clean, maintainable, and scalable code. This section covers the most useful patterns implemented with modern JavaScript classes.

Factory Pattern

Creates objects without specifying exact class:

class UserFactory {
  static create(type, data) {
    switch (type) {
      case 'admin':
        return new Admin(data);
      case 'moderator':
        return new Moderator(data);
      default:
        return new User(data);
    }
  }
}

const admin = UserFactory.create('admin', { name: 'Alice' });
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│            Factory Pattern               │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│  Client → Factory.create(type)          │
│                ↓                         │
│         ā”Œā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”                   │
│         ↓     ↓     ↓                   │
│     TypeA  TypeB  TypeC                 │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Builder Pattern

Constructs complex objects step by step:

class QueryBuilder {
  #table = '';
  #conditions = [];
  #columns = ['*'];
  #limit = null;

  select(...columns) {
    this.#columns = columns.length ? columns : ['*'];
    return this;
  }

  from(table) {
    this.#table = table;
    return this;
  }

  where(condition) {
    this.#conditions.push(condition);
    return this;
  }

  limit(n) {
    this.#limit = n;
    return this;
  }

  build() {
    let query = `SELECT ${this.#columns.join(', ')} FROM ${this.#table}`;
    if (this.#conditions.length) {
      query += ` WHERE ${this.#conditions.join(' AND ')}`;
    }
    if (this.#limit) {
      query += ` LIMIT ${this.#limit}`;
    }
    return query;
  }
}

const query = new QueryBuilder()
  .select('name', 'email')
  .from('users')
  .where('active = true')
  .where('age > 18')
  .limit(10)
  .build();

Singleton Pattern

Ensures single instance across application:

class Database {
  static #instance = null;

  constructor() {
    if (Database.#instance) {
      return Database.#instance;
    }
    this.connection = null;
    Database.#instance = this;
  }

  static getInstance() {
    if (!Database.#instance) {
      Database.#instance = new Database();
    }
    return Database.#instance;
  }
}

const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1 === db2); // true

Mixin Pattern

Combines behaviors from multiple sources:

// Mixin factories
const Timestamped = (Base) =>
  class extends Base {
    constructor(...args) {
      super(...args);
      this.createdAt = new Date();
      this.updatedAt = new Date();
    }

    touch() {
      this.updatedAt = new Date();
    }
  };

const Serializable = (Base) =>
  class extends Base {
    toJSON() {
      return { ...this };
    }
  };

// Compose mixins
class User extends Serializable(Timestamped(BaseModel)) {
  constructor(name) {
    super();
    this.name = name;
  }
}
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│              Mixin Composition              │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│  class User extends                        │
│    Serializable(                           │
│      Timestamped(                          │
│        BaseModel                           │
│      )                                     │
│    )                                       │
│                                            │
│  Prototype Chain:                          │
│  User → Serializable → Timestamped → Base │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Observer Pattern

Subscribe to state changes:

class EventEmitter {
  #listeners = new Map();

  on(event, callback) {
    if (!this.#listeners.has(event)) {
      this.#listeners.set(event, new Set());
    }
    this.#listeners.get(event).add(callback);
    return () => this.off(event, callback); // Unsubscribe
  }

  emit(event, ...args) {
    const callbacks = this.#listeners.get(event);
    if (callbacks) {
      callbacks.forEach((cb) => cb(...args));
    }
  }
}

class Store extends EventEmitter {
  #state = {};

  setState(newState) {
    this.#state = { ...this.#state, ...newState };
    this.emit('change', this.#state);
  }
}

Strategy Pattern

Encapsulate interchangeable algorithms:

class PaymentContext {
  #strategy;

  setStrategy(strategy) {
    this.#strategy = strategy;
  }

  pay(amount) {
    return this.#strategy.execute(amount);
  }
}

class CreditCardStrategy {
  execute(amount) {
    return `Paid $${amount} via Credit Card`;
  }
}

class PayPalStrategy {
  execute(amount) {
    return `Paid $${amount} via PayPal`;
  }
}

// Usage
const payment = new PaymentContext();
payment.setStrategy(new CreditCardStrategy());
payment.pay(100);

Decorator Pattern

Add behavior dynamically:

// Base class
class Coffee {
  cost() {
    return 5;
  }
  description() {
    return 'Coffee';
  }
}

// Decorators
class MilkDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }
  cost() {
    return this.coffee.cost() + 2;
  }
  description() {
    return `${this.coffee.description()} + Milk`;
  }
}

class SugarDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }
  cost() {
    return this.coffee.cost() + 0.5;
  }
  description() {
    return `${this.coffee.description()} + Sugar`;
  }
}

// Usage
let order = new Coffee();
order = new MilkDecorator(order);
order = new SugarDecorator(order);
console.log(order.description()); // "Coffee + Milk + Sugar"
console.log(order.cost()); // 7.5

Command Pattern

Encapsulate actions as objects:

class Command {
  execute() {
    throw new Error('Must implement execute()');
  }
  undo() {
    throw new Error('Must implement undo()');
  }
}

class AddItemCommand extends Command {
  constructor(cart, item) {
    super();
    this.cart = cart;
    this.item = item;
  }

  execute() {
    this.cart.add(this.item);
  }

  undo() {
    this.cart.remove(this.item);
  }
}

class CommandHistory {
  #history = [];

  execute(command) {
    command.execute();
    this.#history.push(command);
  }

  undo() {
    const command = this.#history.pop();
    if (command) command.undo();
  }
}

Pattern Comparison

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ Pattern         │ Use Case                               │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ Factory         │ Create objects of varying types        │
│ Builder         │ Construct complex objects step-by-step │
│ Singleton       │ Single shared instance                 │
│ Mixin           │ Share behavior across hierarchies      │
│ Observer        │ React to state changes                 │
│ Strategy        │ Swap algorithms at runtime             │
│ Decorator       │ Add behavior dynamically               │
│ Command         │ Encapsulate actions for undo/redo      │
│ Repository      │ Abstract data access layer             │
│ Adapter         │ Convert interface to another           │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Repository Pattern

Abstract data access:

class Repository {
  findById(id) {
    throw new Error('Not implemented');
  }
  findAll() {
    throw new Error('Not implemented');
  }
  save(entity) {
    throw new Error('Not implemented');
  }
  delete(id) {
    throw new Error('Not implemented');
  }
}

class UserRepository extends Repository {
  #store = new Map();

  findById(id) {
    return this.#store.get(id) || null;
  }

  findAll() {
    return [...this.#store.values()];
  }

  save(user) {
    this.#store.set(user.id, user);
    return user;
  }

  delete(id) {
    return this.#store.delete(id);
  }
}

Dependency Injection

Inject dependencies for testability:

class UserService {
  constructor(userRepository, emailService) {
    this.repo = userRepository;
    this.email = emailService;
  }

  async createUser(data) {
    const user = await this.repo.save(data);
    await this.email.sendWelcome(user.email);
    return user;
  }
}

// Easy to test with mocks
const mockRepo = { save: jest.fn() };
const mockEmail = { sendWelcome: jest.fn() };
const service = new UserService(mockRepo, mockEmail);

Best Practices

Do āœ“Don't āœ—
Choose simplest pattern for the jobOver-engineer with unnecessary patterns
Combine patterns when beneficialCreate deep inheritance hierarchies
Use mixins for cross-cutting concernsUse inheritance for code reuse only
Prefer composition over inheritanceMake everything a singleton
Keep patterns focused and smallForget to document pattern usage

Key Takeaways

  1. •Factory - Object creation abstraction
  2. •Builder - Step-by-step complex construction
  3. •Singleton - Single shared instance
  4. •Mixin - Composable behaviors
  5. •Observer - Event-driven communication
  6. •Strategy - Interchangeable algorithms
  7. •Decorator - Dynamic behavior enhancement
  8. •Command - Action encapsulation with undo
  9. •Repository - Data access abstraction
  10. •DI - Loose coupling for testability
.5 Class Patterns - JavaScript Tutorial | DeepML