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 runtimeCommand
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