Design Patterns
02 / 03

Behavioral Patterns

Behavioral Patterns

Observer

Define a one-to-many dependency so when one object changes state, all dependents are notified.

interface Observer<T> {
  update(data: T): void;
}

class EventEmitter<T> {
  private observers: Observer<T>[] = [];
  subscribe(obs: Observer<T>) { this.observers.push(obs); }
  unsubscribe(obs: Observer<T>) {
    this.observers = this.observers.filter(o => o !== obs);
  }
  emit(data: T) { this.observers.forEach(o => o.update(data)); }
}

// Usage
const orderEvents = new EventEmitter<{ orderId: string; status: string }>();
orderEvents.subscribe({ update: ({ orderId, status }) => sendEmail(orderId, status) });
orderEvents.subscribe({ update: ({ orderId }) => updateAnalytics(orderId) });

orderEvents.emit({ orderId: '123', status: 'shipped' });

Strategy

Define a family of algorithms, encapsulate each one, and make them interchangeable.

type SortStrategy<T> = (arr: T[]) => T[];

const bubbleSort: SortStrategy<number> = (arr) => { /* ... */ return arr; };
const quickSort: SortStrategy<number> = (arr) => { /* ... */ return arr; };
const mergeSort: SortStrategy<number> = (arr) => { /* ... */ return arr; };

class Sorter<T> {
  constructor(private strategy: SortStrategy<T>) {}
  setStrategy(s: SortStrategy<T>) { this.strategy = s; }
  sort(arr: T[]) { return this.strategy([...arr]); }
}

const sorter = new Sorter<number>(quickSort);
sorter.sort([3, 1, 4, 1, 5]);
sorter.setStrategy(mergeSort);  // swap algorithm at runtime

Command

Encapsulate a request as an object, enabling undo/redo, queuing, and logging.

interface Command {
  execute(): void;
  undo(): void;
}

class TextEditor {
  private history: Command[] = [];
  private text = '';

  executeCommand(cmd: Command) {
    cmd.execute();
    this.history.push(cmd);
  }

  undoLast() { this.history.pop()?.undo(); }
  getText() { return this.text; }

  createInsertCommand(text: string, position: number): Command {
    return {
      execute: () => {
        this.text = this.text.slice(0, position) + text + this.text.slice(position);
      },
      undo: () => {
        this.text = this.text.slice(0, position) + this.text.slice(position + text.length);
      },
    };
  }
}

Repository Pattern

Abstract data access logic behind an interface, decoupling business logic from storage details.

interface UserRepository {
  findById(id: string): Promise<User | null>;
  findAll(filters?: UserFilters): Promise<User[]>;
  create(data: CreateUserDto): Promise<User>;
  update(id: string, data: Partial<User>): Promise<User>;
  delete(id: string): Promise<void>;
}

// PostgreSQL implementation
class PgUserRepository implements UserRepository {
  async findById(id: string) {
    return db.query('SELECT * FROM users WHERE id = $1', [id]);
  }
  // ...
}

// In-memory implementation (for tests)
class InMemoryUserRepository implements UserRepository {
  private users = new Map<string, User>();
  async findById(id: string) { return this.users.get(id) ?? null; }
  // ...
}

// Service only depends on interface, not implementation
class UserService {
  constructor(private repo: UserRepository) {}
  async getUser(id: string) { return this.repo.findById(id); }
}

// Swap implementations easily
const service = new UserService(new PgUserRepository());
const testService = new UserService(new InMemoryUserRepository());

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free