Docs
README
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 job | Over-engineer with unnecessary patterns |
| Combine patterns when beneficial | Create deep inheritance hierarchies |
| Use mixins for cross-cutting concerns | Use inheritance for code reuse only |
| Prefer composition over inheritance | Make everything a singleton |
| Keep patterns focused and small | Forget to document pattern usage |
Key Takeaways
- ā¢Factory - Object creation abstraction
- ā¢Builder - Step-by-step complex construction
- ā¢Singleton - Single shared instance
- ā¢Mixin - Composable behaviors
- ā¢Observer - Event-driven communication
- ā¢Strategy - Interchangeable algorithms
- ā¢Decorator - Dynamic behavior enhancement
- ā¢Command - Action encapsulation with undo
- ā¢Repository - Data access abstraction
- ā¢DI - Loose coupling for testability