Common OOP Design Patterns
Design patterns are reusable solutions to common problems in software design. They represent best practices evolved over time.
Singleton Pattern
Ensures a class has only one instance and provides a global access point to it.
class Database {
private static instance: Database;
private connection: any;
// Private constructor prevents external instantiation
private constructor() {
this.connection = this.createConnection();
console.log("Database instance created");
}
public static getInstance(): Database {
if (!Database.instance) {
Database.instance = new Database();
}
return Database.instance;
}
private createConnection(): any {
return { connected: true, host: "localhost" };
}
public query(sql: string): any {
console.log(`Executing: ${sql}`);
return [];
}
}
// Usage
const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1 === db2); // true - same instance
// const db3 = new Database(); // ERROR: Constructor is privateFactory Pattern
Creates objects without specifying the exact class of object that will be created.
interface Notification {
send(message: string): void;
}
class EmailNotification implements Notification {
constructor(private recipient: string) {}
send(message: string): void {
console.log(`Email to ${this.recipient}: ${message}`);
}
}
class SMSNotification implements Notification {
constructor(private phoneNumber: string) {}
send(message: string): void {
console.log(`SMS to ${this.phoneNumber}: ${message}`);
}
}
class PushNotification implements Notification {
constructor(private deviceId: string) {}
send(message: string): void {
console.log(`Push to ${this.deviceId}: ${message}`);
}
}
// Factory
class NotificationFactory {
static create(type: string, recipient: string): Notification {
switch (type) {
case "email":
return new EmailNotification(recipient);
case "sms":
return new SMSNotification(recipient);
case "push":
return new PushNotification(recipient);
default:
throw new Error(`Unknown notification type: ${type}`);
}
}
}
// Usage
const notifications = [
NotificationFactory.create("email", "user@example.com"),
NotificationFactory.create("sms", "+1234567890"),
NotificationFactory.create("push", "device-123")
];
notifications.forEach(notification => {
notification.send("Hello!");
});Observer Pattern
Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified.
interface Observer {
update(data: any): void;
}
class Subject {
private observers: Observer[] = [];
private state: any;
attach(observer: Observer): void {
this.observers.push(observer);
}
detach(observer: Observer): void {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
notify(): void {
for (const observer of this.observers) {
observer.update(this.state);
}
}
setState(state: any): void {
this.state = state;
this.notify();
}
}
class StockMarket extends Subject {
private stockPrices: Map<string, number> = new Map();
updatePrice(symbol: string, price: number): void {
this.stockPrices.set(symbol, price);
this.setState({ symbol, price });
}
getPrice(symbol: string): number | undefined {
return this.stockPrices.get(symbol);
}
}
class Investor implements Observer {
constructor(private name: string) {}
update(data: { symbol: string; price: number }): void {
console.log(`${this.name} notified: ${data.symbol} = $${data.price}`);
}
}
class TradingBot implements Observer {
constructor(private strategy: string) {}
update(data: { symbol: string; price: number }): void {
console.log(`Bot (${this.strategy}) analyzing: ${data.symbol} = $${data.price}`);
if (data.price < 100) {
console.log(` → Executing BUY order`);
}
}
}
// Usage
const market = new StockMarket();
const investor1 = new Investor("Alice");
const investor2 = new Investor("Bob");
const bot = new TradingBot("Value Investing");
market.attach(investor1);
market.attach(investor2);
market.attach(bot);
market.updatePrice("AAPL", 150);
market.updatePrice("GOOGL", 95);Strategy Pattern
Defines a family of algorithms, encapsulates each one, and makes them interchangeable.
interface SortStrategy {
sort(data: number[]): number[];
}
class BubbleSort implements SortStrategy {
sort(data: number[]): number[] {
console.log("Using Bubble Sort");
const arr = [...data];
for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
return arr;
}
}
class QuickSort implements SortStrategy {
sort(data: number[]): number[] {
console.log("Using Quick Sort");
if (data.length <= 1) return data;
const pivot = data[Math.floor(data.length / 2)];
const left = data.filter(x => x < pivot);
const middle = data.filter(x => x === pivot);
const right = data.filter(x => x > pivot);
return [...this.sort(left), ...middle, ...this.sort(right)];
}
}
class MergeSort implements SortStrategy {
sort(data: number[]): number[] {
console.log("Using Merge Sort");
if (data.length <= 1) return data;
const mid = Math.floor(data.length / 2);
const left = this.sort(data.slice(0, mid));
const right = this.sort(data.slice(mid));
return this.merge(left, right);
}
private merge(left: number[], right: number[]): number[] {
const result: number[] = [];
let i = 0, j = 0;
while (i < left.length && j < right.length) {
if (left[i] < right[j]) {
result.push(left[i++]);
} else {
result.push(right[j++]);
}
}
return result.concat(left.slice(i)).concat(right.slice(j));
}
}
class DataSorter {
private strategy: SortStrategy;
constructor(strategy: SortStrategy) {
this.strategy = strategy;
}
setStrategy(strategy: SortStrategy): void {
this.strategy = strategy;
}
sort(data: number[]): number[] {
return this.strategy.sort(data);
}
}
// Usage
const data = [64, 34, 25, 12, 22, 11, 90];
const sorter = new DataSorter(new BubbleSort());
console.log("Result:", sorter.sort(data));
// Change strategy at runtime
sorter.setStrategy(new QuickSort());
console.log("Result:", sorter.sort(data));
sorter.setStrategy(new MergeSort());
console.log("Result:", sorter.sort(data));Decorator Pattern
Attaches additional responsibilities to an object dynamically, providing a flexible alternative to subclassing.
interface Coffee {
cost(): number;
description(): string;
}
class SimpleCoffee implements Coffee {
cost(): number {
return 2;
}
description(): string {
return "Simple coffee";
}
}
// Decorator base class
abstract class CoffeeDecorator implements Coffee {
constructor(protected coffee: Coffee) {}
abstract cost(): number;
abstract description(): string;
}
class MilkDecorator extends CoffeeDecorator {
cost(): number {
return this.coffee.cost() + 0.5;
}
description(): string {
return this.coffee.description() + ", milk";
}
}
class SugarDecorator extends CoffeeDecorator {
cost(): number {
return this.coffee.cost() + 0.2;
}
description(): string {
return this.coffee.description() + ", sugar";
}
}
class WhippedCreamDecorator extends CoffeeDecorator {
cost(): number {
return this.coffee.cost() + 0.7;
}
description(): string {
return this.coffee.description() + ", whipped cream";
}
}
// Usage - stack decorators
let coffee: Coffee = new SimpleCoffee();
console.log(`${coffee.description()} = $${coffee.cost()}`);
coffee = new MilkDecorator(coffee);
console.log(`${coffee.description()} = $${coffee.cost()}`);
coffee = new SugarDecorator(coffee);
console.log(`${coffee.description()} = $${coffee.cost()}`);
coffee = new WhippedCreamDecorator(coffee);
console.log(`${coffee.description()} = $${coffee.cost()}`);
// Or all at once
const fancyCoffee = new WhippedCreamDecorator(
new SugarDecorator(
new MilkDecorator(
new SimpleCoffee()
)
)
);
console.log(`${fancyCoffee.description()} = $${fancyCoffee.cost()}`);Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free