DevToolBoxGRÁTIS
Blog

Software Design Patterns Guide: Creational, Structural & Behavioral Patterns

22 min readby DevToolBox Team

Software Design Patterns: A Comprehensive Guide with TypeScript & Python Examples

Master the 23 Gang of Four design patterns — Creational (Factory, Builder, Singleton), Structural (Adapter, Decorator, Proxy, Facade), and Behavioral (Observer, Strategy, Command, State) — with production-ready TypeScript and Python code examples.

TL;DR — Design Patterns in 60 Seconds
  • Factory: Delegate object creation to factory methods, decoupling clients from concrete classes
  • Builder: Construct complex objects step by step with fluent chaining and optional params
  • Singleton: Ensure a single global instance (use sparingly — prefer DI)
  • Adapter / Facade: Unify incompatible interfaces / provide a simple entry to complex subsystems
  • Decorator / Proxy: Add behavior dynamically / control object access (cache, logging, auth)
  • Observer: One-to-many notification driving event systems and reactive programming
  • Strategy / Command / State: Swappable algorithms / encapsulate requests / state-driven behavior
Key Takeaways
  • Favor composition over inheritance — most patterns embody this principle
  • Program to interfaces, not implementations — reduce coupling
  • Open/Closed Principle (OCP): Open for extension, closed for modification
  • Do not use patterns for their own sake — introduce them only when genuinely needed
  • TypeScript interfaces and Python Protocols are keystones for type-safe patterns
  • Modern frameworks embed many patterns (React Context=Observer, Express middleware=Chain of Responsibility)

Design Patterns Overview

Design patterns fall into three categories. The table below lists the 11 most commonly used patterns covered in this guide along with their core intent.

CategoryPatternCore Intent
CreationalFactoryDelegate object instantiation to subclasses or factory methods
CreationalBuilderConstruct complex objects step by step
CreationalSingletonEnsure a class has only one instance
StructuralAdapterConvert incompatible interfaces into compatible ones
StructuralDecoratorDynamically attach new behavior to objects
StructuralProxyProvide a surrogate to control access to an object
StructuralFacadeProvide a simplified interface to a complex subsystem
BehavioralObserverDefine one-to-many dependency with automatic change notification
BehavioralStrategyDefine a family of interchangeable algorithms
BehavioralCommandEncapsulate a request as an object, enabling undo and queuing
BehavioralStateAlter object behavior when its internal state changes

1. Creational Patterns

1.1 Factory Method Pattern

The Factory Method defines an interface for creating an object but lets subclasses decide which class to instantiate. It defers the new operation to subclasses, keeping code open for extension and closed for modification.

TypeScript

// Factory Method — TypeScript
interface Notification {
  send(message: string): void;
}

class EmailNotification implements Notification {
  send(message: string): void {
    console.log("Email: " + message);
  }
}

class SMSNotification implements Notification {
  send(message: string): void {
    console.log("SMS: " + message);
  }
}

class PushNotification implements Notification {
  send(message: string): void {
    console.log("Push: " + message);
  }
}

type Channel = "email" | "sms" | "push";

function createNotification(channel: Channel): Notification {
  const map: Record<Channel, () => Notification> = {
    email: () => new EmailNotification(),
    sms:   () => new SMSNotification(),
    push:  () => new PushNotification(),
  };
  return map[channel]();
}

// Usage
const notifier = createNotification("email");
notifier.send("Welcome aboard!");  // Email: Welcome aboard!

Python

# Factory Method — Python
from abc import ABC, abstractmethod

class Notification(ABC):
    @abstractmethod
    def send(self, message: str) -> None: ...

class EmailNotification(Notification):
    def send(self, message: str) -> None:
        print(f"Email: {message}")

class SMSNotification(Notification):
    def send(self, message: str) -> None:
        print(f"SMS: {message}")

class PushNotification(Notification):
    def send(self, message: str) -> None:
        print(f"Push: {message}")

def create_notification(channel: str) -> Notification:
    factories = {
        "email": EmailNotification,
        "sms":   SMSNotification,
        "push":  PushNotification,
    }
    if channel not in factories:
        raise ValueError(f"Unknown channel: {channel}")
    return factories[channel]()

# Usage
notifier = create_notification("sms")
notifier.send("Your code is 1234")  # SMS: Your code is 1234

1.2 Builder Pattern

The Builder pattern separates the construction of a complex object from its representation. When an object has many optional parameters or requires a specific build order, Builder is more elegant than constructor overloading.

TypeScript

// Builder — TypeScript
interface QueryConfig {
  table: string;
  fields: string[];
  conditions: string[];
  orderBy?: string;
  limit?: number;
}

class QueryBuilder {
  private config: QueryConfig;

  constructor(table: string) {
    this.config = { table, fields: ["*"], conditions: [] };
  }

  select(...fields: string[]): this {
    this.config.fields = fields;
    return this;
  }

  where(condition: string): this {
    this.config.conditions.push(condition);
    return this;
  }

  orderBy(field: string): this {
    this.config.orderBy = field;
    return this;
  }

  limit(n: number): this {
    this.config.limit = n;
    return this;
  }

  build(): string {
    let sql = "SELECT " + this.config.fields.join(", ");
    sql += " FROM " + this.config.table;
    if (this.config.conditions.length > 0) {
      sql += " WHERE " + this.config.conditions.join(" AND ");
    }
    if (this.config.orderBy) {
      sql += " ORDER BY " + this.config.orderBy;
    }
    if (this.config.limit !== undefined) {
      sql += " LIMIT " + this.config.limit;
    }
    return sql;
  }
}

// Usage — fluent chaining
const query = new QueryBuilder("users")
  .select("id", "name", "email")
  .where("active = true")
  .where("age > 18")
  .orderBy("name")
  .limit(50)
  .build();

console.log(query);
// SELECT id, name, email FROM users WHERE active = true AND age > 18 ORDER BY name LIMIT 50

Python

# Builder — Python
from dataclasses import dataclass, field

@dataclass
class QueryConfig:
    table: str
    fields: list[str] = field(default_factory=lambda: ["*"])
    conditions: list[str] = field(default_factory=list)
    order_by: str | None = None
    limit: int | None = None

class QueryBuilder:
    def __init__(self, table: str):
        self._config = QueryConfig(table=table)

    def select(self, *fields: str) -> "QueryBuilder":
        self._config.fields = list(fields)
        return self

    def where(self, condition: str) -> "QueryBuilder":
        self._config.conditions.append(condition)
        return self

    def order_by(self, col: str) -> "QueryBuilder":
        self._config.order_by = col
        return self

    def limit(self, n: int) -> "QueryBuilder":
        self._config.limit = n
        return self

    def build(self) -> str:
        sql = f"SELECT {', '.join(self._config.fields)} FROM {self._config.table}"
        if self._config.conditions:
            sql += f" WHERE {' AND '.join(self._config.conditions)}"
        if self._config.order_by:
            sql += f" ORDER BY {self._config.order_by}"
        if self._config.limit is not None:
            sql += f" LIMIT {self._config.limit}"
        return sql

# Usage
query = (
    QueryBuilder("users")
    .select("id", "name", "email")
    .where("active = true")
    .order_by("name")
    .limit(50)
    .build()
)
print(query)

1.3 Singleton Pattern

Singleton ensures a class has exactly one instance with a global access point. In modern development, dependency injection is often preferred, but Singleton remains useful for managing configuration, logging, and connection pools.

TypeScript

// Singleton — TypeScript
class Logger {
  private static instance: Logger;
  private logs: string[] = [];

  private constructor() {} // prevent external instantiation

  static getInstance(): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }

  log(message: string): void {
    const entry = "[" + new Date().toISOString() + "] " + message;
    this.logs.push(entry);
    console.log(entry);
  }

  getHistory(): string[] {
    return [...this.logs];
  }
}

// Usage — same instance everywhere
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();
console.log(logger1 === logger2);  // true

logger1.log("App started");
logger2.log("User logged in");
console.log(logger1.getHistory().length);  // 2

Python

# Singleton — Python (using __new__)
class Logger:
    _instance = None
    _logs: list[str] = []

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def log(self, message: str) -> None:
        from datetime import datetime
        entry = f"[{datetime.now().isoformat()}] {message}"
        self._logs.append(entry)
        print(entry)

    def get_history(self) -> list[str]:
        return list(self._logs)

# Usage
logger1 = Logger()
logger2 = Logger()
print(logger1 is logger2)  # True

# Alternative: module-level singleton (Pythonic approach)
# config.py
class _Config:
    def __init__(self):
        self.debug = False
        self.db_url = "sqlite:///app.db"

config = _Config()  # module-level instance acts as singleton

2. Structural Patterns

2.1 Adapter Pattern

The Adapter pattern converts the interface of a class into another interface clients expect. It allows classes with incompatible interfaces to work together, commonly used for integrating third-party libraries or legacy systems.

TypeScript

// Adapter — TypeScript
// Target interface our app expects
interface PaymentProcessor {
  charge(amount: number, currency: string): Promise<{ id: string; status: string }>;
}

// Legacy third-party SDK with incompatible interface
class LegacyPaymentSDK {
  makePayment(cents: number, curr: string, callback: (err: Error | null, txId: string) => void): void {
    setTimeout(() => callback(null, "txn_" + Math.random().toString(36).slice(2)), 100);
  }
}

// Adapter wraps the legacy SDK to match our interface
class LegacyPaymentAdapter implements PaymentProcessor {
  constructor(private sdk: LegacyPaymentSDK) {}

  charge(amount: number, currency: string): Promise<{ id: string; status: string }> {
    return new Promise((resolve, reject) => {
      const cents = Math.round(amount * 100);
      this.sdk.makePayment(cents, currency, (err, txId) => {
        if (err) reject(err);
        else resolve({ id: txId, status: "success" });
      });
    });
  }
}

// Usage — client only knows PaymentProcessor
async function checkout(processor: PaymentProcessor) {
  const result = await processor.charge(29.99, "USD");
  console.log("Payment " + result.id + ": " + result.status);
}

const adapter = new LegacyPaymentAdapter(new LegacyPaymentSDK());
checkout(adapter);

Python

# Adapter — Python
from typing import Protocol

class PaymentProcessor(Protocol):
    def charge(self, amount: float, currency: str) -> dict: ...

class LegacyPaymentSDK:
    """Third-party SDK with incompatible interface"""
    def make_payment(self, cents: int, curr: str) -> str:
        import uuid
        return f"txn_{uuid.uuid4().hex[:8]}"

class LegacyPaymentAdapter:
    """Adapter wraps legacy SDK to match PaymentProcessor protocol"""
    def __init__(self, sdk: LegacyPaymentSDK):
        self._sdk = sdk

    def charge(self, amount: float, currency: str) -> dict:
        cents = round(amount * 100)
        tx_id = self._sdk.make_payment(cents, currency)
        return {"id": tx_id, "status": "success"}

# Usage
adapter = LegacyPaymentAdapter(LegacyPaymentSDK())
result = adapter.charge(29.99, "USD")
print(f"Payment {result['id']}: {result['status']}")

2.2 Decorator Pattern

The Decorator pattern dynamically attaches additional responsibilities to an object without modifying the original class. It is more flexible than inheritance and allows combining behaviors freely. Python's @decorator syntax and TypeScript Stage 3 decorators are both influenced by this pattern.

TypeScript

// Decorator — TypeScript
interface HttpClient {
  request(url: string, options?: RequestInit): Promise<Response>;
}

// Base implementation
class FetchClient implements HttpClient {
  async request(url: string, options?: RequestInit): Promise<Response> {
    return fetch(url, options);
  }
}

// Decorator: adds logging
class LoggingDecorator implements HttpClient {
  constructor(private client: HttpClient) {}

  async request(url: string, options?: RequestInit): Promise<Response> {
    console.log("[LOG] Request: " + url);
    const start = Date.now();
    const response = await this.client.request(url, options);
    console.log("[LOG] " + response.status + " in " + (Date.now() - start) + "ms");
    return response;
  }
}

// Decorator: adds retry logic
class RetryDecorator implements HttpClient {
  constructor(private client: HttpClient, private maxRetries = 3) {}

  async request(url: string, options?: RequestInit): Promise<Response> {
    let lastError: Error | null = null;
    for (let i = 0; i <= this.maxRetries; i++) {
      try {
        return await this.client.request(url, options);
      } catch (err) {
        lastError = err as Error;
        console.log("[RETRY] Attempt " + (i + 1) + " failed");
      }
    }
    throw lastError;
  }
}

// Usage: compose decorators
const client: HttpClient = new LoggingDecorator(
  new RetryDecorator(
    new FetchClient(), 3
  )
);
// client.request("https://api.example.com/data");

Python

# Decorator — Python (both class-based and @decorator syntax)
import functools
import time
from typing import Callable, Any

# Function decorator for timing
def timer(func: Callable) -> Callable:
    @functools.wraps(func)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.4f}s")
        return result
    return wrapper

# Function decorator for retry
def retry(max_attempts: int = 3):
    def decorator(func: Callable) -> Callable:
        @functools.wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"Attempt {attempt} failed: {e}")
                    if attempt == max_attempts:
                        raise
        return wrapper
    return decorator

# Usage — stack decorators
@timer
@retry(max_attempts=3)
def fetch_data(url: str) -> str:
    import urllib.request
    return urllib.request.urlopen(url).read().decode()

# fetch_data("https://api.example.com/data")

2.3 Proxy Pattern

The Proxy pattern provides a surrogate or placeholder for another object to control access. Common use cases include lazy initialization (virtual proxy), access control (protection proxy), and caching (smart proxy).

TypeScript

// Proxy — TypeScript (Caching Proxy)
interface DataService {
  fetchData(key: string): Promise<string>;
}

class ApiService implements DataService {
  async fetchData(key: string): Promise<string> {
    console.log("[API] Fetching " + key + " from server...");
    // Simulate network request
    return "data_for_" + key;
  }
}

class CachingProxy implements DataService {
  private cache = new Map<string, { value: string; expiry: number }>();

  constructor(private service: DataService, private ttlMs = 60000) {}

  async fetchData(key: string): Promise<string> {
    const cached = this.cache.get(key);
    if (cached && cached.expiry > Date.now()) {
      console.log("[CACHE] Hit for " + key);
      return cached.value;
    }
    console.log("[CACHE] Miss for " + key);
    const value = await this.service.fetchData(key);
    this.cache.set(key, { value, expiry: Date.now() + this.ttlMs });
    return value;
  }
}

// Usage
const service: DataService = new CachingProxy(new ApiService(), 5000);
// First call: cache miss, hits API
// Second call: cache hit, returns instantly

Python

# Proxy — Python (Caching Proxy)
import time
from typing import Protocol

class DataService(Protocol):
    def fetch_data(self, key: str) -> str: ...

class ApiService:
    def fetch_data(self, key: str) -> str:
        print(f"[API] Fetching {key} from server...")
        return f"data_for_{key}"

class CachingProxy:
    def __init__(self, service: DataService, ttl: float = 60.0):
        self._service = service
        self._ttl = ttl
        self._cache: dict[str, tuple[str, float]] = {}

    def fetch_data(self, key: str) -> str:
        if key in self._cache:
            value, expiry = self._cache[key]
            if time.time() < expiry:
                print(f"[CACHE] Hit for {key}")
                return value
        print(f"[CACHE] Miss for {key}")
        value = self._service.fetch_data(key)
        self._cache[key] = (value, time.time() + self._ttl)
        return value

# Usage
service = CachingProxy(ApiService(), ttl=5.0)
print(service.fetch_data("users"))   # [API] Fetching...
print(service.fetch_data("users"))   # [CACHE] Hit

2.4 Facade Pattern

The Facade pattern provides a simplified, unified interface to a complex subsystem. It does not add new functionality but wraps multiple complex operations behind a high-level API, reducing usage complexity for clients.

TypeScript

// Facade — TypeScript
// Complex subsystems
class UserService {
  createUser(email: string) { return { id: 1, email }; }
}

class EmailService {
  sendWelcome(email: string) { console.log("Welcome email sent to " + email); }
}

class AnalyticsService {
  trackSignup(userId: number) { console.log("Signup tracked for user " + userId); }
}

class BillingService {
  createTrialSubscription(userId: number) {
    console.log("14-day trial started for user " + userId);
  }
}

// Facade — single method coordinates all subsystems
class RegistrationFacade {
  private userService = new UserService();
  private emailService = new EmailService();
  private analytics = new AnalyticsService();
  private billing = new BillingService();

  async registerUser(email: string) {
    const user = this.userService.createUser(email);
    this.emailService.sendWelcome(email);
    this.analytics.trackSignup(user.id);
    this.billing.createTrialSubscription(user.id);
    return user;
  }
}

// Usage — client only interacts with the facade
const registration = new RegistrationFacade();
registration.registerUser("alice@example.com");

3. Behavioral Patterns

3.1 Observer Pattern

The Observer pattern defines a one-to-many dependency between objects: when one object (Subject) changes state, all dependents (Observers) are notified and updated automatically. Event buses, pub/sub systems, and React state management all embody this pattern.

TypeScript

// Observer — TypeScript (Type-safe Event Emitter)
type EventMap = {
  "user:login":    { userId: string; timestamp: number };
  "user:logout":   { userId: string };
  "cart:update":   { items: number; total: number };
};

class EventBus<T extends Record<string, unknown>> {
  private listeners = new Map<keyof T, Set<(data: any) => void>>();

  on<K extends keyof T>(event: K, callback: (data: T[K]) => void): () => void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(callback);
    // Return unsubscribe function
    return () => { this.listeners.get(event)?.delete(callback); };
  }

  emit<K extends keyof T>(event: K, data: T[K]): void {
    this.listeners.get(event)?.forEach((cb) => cb(data));
  }
}

// Usage
const bus = new EventBus<EventMap>();

const unsub = bus.on("user:login", (data) => {
  console.log("User " + data.userId + " logged in at " + data.timestamp);
});

bus.on("cart:update", (data) => {
  console.log("Cart: " + data.items + " items, $" + data.total);
});

bus.emit("user:login", { userId: "u_123", timestamp: Date.now() });
bus.emit("cart:update", { items: 3, total: 59.97 });

unsub(); // unsubscribe from user:login

Python

# Observer — Python
from typing import Callable, Any
from collections import defaultdict

class EventBus:
    def __init__(self):
        self._listeners: dict[str, list[Callable]] = defaultdict(list)

    def on(self, event: str, callback: Callable) -> Callable:
        self._listeners[event].append(callback)
        def unsubscribe():
            self._listeners[event].remove(callback)
        return unsubscribe

    def emit(self, event: str, **data: Any) -> None:
        for callback in self._listeners.get(event, []):
            callback(**data)

# Usage
bus = EventBus()

def on_login(user_id: str, timestamp: float):
    print(f"User {user_id} logged in at {timestamp}")

unsub = bus.on("user:login", on_login)
bus.emit("user:login", user_id="u_123", timestamp=1709000000)
unsub()  # unsubscribe

3.2 Strategy Pattern

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Clients can select different strategies at runtime without modifying the code that uses them.

TypeScript

// Strategy — TypeScript
interface CompressionStrategy {
  compress(data: string): string;
  name: string;
}

class GzipStrategy implements CompressionStrategy {
  name = "gzip";
  compress(data: string): string {
    // Simulate gzip compression
    return "[gzip:" + data.length + "]" + data.slice(0, 10) + "...";
  }
}

class BrotliStrategy implements CompressionStrategy {
  name = "brotli";
  compress(data: string): string {
    // Simulate brotli compression (better ratio)
    return "[br:" + Math.floor(data.length * 0.8) + "]" + data.slice(0, 8) + "...";
  }
}

class NoCompression implements CompressionStrategy {
  name = "none";
  compress(data: string): string {
    return data;
  }
}

class FileCompressor {
  constructor(private strategy: CompressionStrategy) {}

  setStrategy(strategy: CompressionStrategy): void {
    this.strategy = strategy;
  }

  compressFile(data: string): string {
    console.log("Compressing with " + this.strategy.name);
    return this.strategy.compress(data);
  }
}

// Usage — switch strategies at runtime
const compressor = new FileCompressor(new GzipStrategy());
console.log(compressor.compressFile("Hello world repeated many times"));

compressor.setStrategy(new BrotliStrategy());
console.log(compressor.compressFile("Hello world repeated many times"));

Python

# Strategy — Python (using Protocols and first-class functions)
from typing import Protocol, Callable

# Class-based approach
class CompressionStrategy(Protocol):
    name: str
    def compress(self, data: str) -> str: ...

class GzipStrategy:
    name = "gzip"
    def compress(self, data: str) -> str:
        return f"[gzip:{len(data)}]{data[:10]}..."

class BrotliStrategy:
    name = "brotli"
    def compress(self, data: str) -> str:
        return f"[br:{int(len(data)*0.8)}]{data[:8]}..."

class FileCompressor:
    def __init__(self, strategy: CompressionStrategy):
        self._strategy = strategy

    def set_strategy(self, strategy: CompressionStrategy) -> None:
        self._strategy = strategy

    def compress_file(self, data: str) -> str:
        print(f"Compressing with {self._strategy.name}")
        return self._strategy.compress(data)

# Functional alternative — even simpler
CompressFn = Callable[[str], str]

def gzip_compress(data: str) -> str:
    return f"[gzip:{len(data)}]{data[:10]}..."

def compress_file(data: str, strategy: CompressFn = gzip_compress) -> str:
    return strategy(data)

print(compress_file("test data", gzip_compress))

3.3 Command Pattern

The Command pattern encapsulates a request as a standalone object containing all information needed to perform the action. It supports parameterizing operations, queuing execution, logging, and undoable operations, forming the basis for undo/redo systems.

TypeScript

// Command — TypeScript (Text Editor with Undo/Redo)
interface Command {
  execute(): void;
  undo(): void;
  description: string;
}

class TextEditor {
  content = "";
  cursorPos = 0;
}

class InsertTextCommand implements Command {
  description: string;
  private previousContent = "";

  constructor(private editor: TextEditor, private text: string, private pos: number) {
    this.description = "Insert \"" + text + "\" at pos " + pos;
  }

  execute(): void {
    this.previousContent = this.editor.content;
    const before = this.editor.content.slice(0, this.pos);
    const after = this.editor.content.slice(this.pos);
    this.editor.content = before + this.text + after;
    this.editor.cursorPos = this.pos + this.text.length;
  }

  undo(): void {
    this.editor.content = this.previousContent;
    this.editor.cursorPos = this.pos;
  }
}

class DeleteTextCommand implements Command {
  description: string;
  private deletedText = "";

  constructor(private editor: TextEditor, private start: number, private end: number) {
    this.description = "Delete chars " + start + "-" + end;
  }

  execute(): void {
    this.deletedText = this.editor.content.slice(this.start, this.end);
    this.editor.content = this.editor.content.slice(0, this.start) + this.editor.content.slice(this.end);
    this.editor.cursorPos = this.start;
  }

  undo(): void {
    const before = this.editor.content.slice(0, this.start);
    const after = this.editor.content.slice(this.start);
    this.editor.content = before + this.deletedText + after;
    this.editor.cursorPos = this.start + this.deletedText.length;
  }
}

class CommandHistory {
  private undoStack: Command[] = [];
  private redoStack: Command[] = [];

  execute(command: Command): void {
    command.execute();
    this.undoStack.push(command);
    this.redoStack = []; // clear redo stack on new action
  }

  undo(): void {
    const command = this.undoStack.pop();
    if (command) {
      command.undo();
      this.redoStack.push(command);
    }
  }

  redo(): void {
    const command = this.redoStack.pop();
    if (command) {
      command.execute();
      this.undoStack.push(command);
    }
  }
}

// Usage
const editor = new TextEditor();
const history = new CommandHistory();

history.execute(new InsertTextCommand(editor, "Hello ", 0));
history.execute(new InsertTextCommand(editor, "World", 6));
console.log(editor.content); // "Hello World"

history.undo();
console.log(editor.content); // "Hello "

history.redo();
console.log(editor.content); // "Hello World"

Python

# Command — Python
from abc import ABC, abstractmethod

class Command(ABC):
    @abstractmethod
    def execute(self) -> None: ...
    @abstractmethod
    def undo(self) -> None: ...

class TextEditor:
    def __init__(self):
        self.content = ""

class InsertTextCommand(Command):
    def __init__(self, editor: TextEditor, text: str, pos: int):
        self._editor = editor
        self._text = text
        self._pos = pos
        self._prev = ""

    def execute(self) -> None:
        self._prev = self._editor.content
        before = self._editor.content[:self._pos]
        after = self._editor.content[self._pos:]
        self._editor.content = before + self._text + after

    def undo(self) -> None:
        self._editor.content = self._prev

class CommandHistory:
    def __init__(self):
        self._undo_stack: list[Command] = []
        self._redo_stack: list[Command] = []

    def execute(self, cmd: Command) -> None:
        cmd.execute()
        self._undo_stack.append(cmd)
        self._redo_stack.clear()

    def undo(self) -> None:
        if self._undo_stack:
            cmd = self._undo_stack.pop()
            cmd.undo()
            self._redo_stack.append(cmd)

# Usage
editor = TextEditor()
history = CommandHistory()
history.execute(InsertTextCommand(editor, "Hello ", 0))
history.execute(InsertTextCommand(editor, "World", 6))
print(editor.content)  # Hello World
history.undo()
print(editor.content)  # Hello 

3.4 State Pattern

The State pattern allows an object to alter its behavior when its internal state changes, appearing to modify its class. It encapsulates state-specific behavior in separate state objects, eliminating large conditional statements.

TypeScript

// State — TypeScript (Order Processing)
interface OrderState {
  name: string;
  next(order: Order): void;
  cancel(order: Order): void;
}

class Order {
  state: OrderState;
  id: string;

  constructor(id: string) {
    this.id = id;
    this.state = new PendingState();
    console.log("Order " + id + " created [" + this.state.name + "]");
  }

  next(): void { this.state.next(this); }
  cancel(): void { this.state.cancel(this); }

  transitionTo(state: OrderState): void {
    console.log("Order " + this.id + ": " + this.state.name + " -> " + state.name);
    this.state = state;
  }
}

class PendingState implements OrderState {
  name = "Pending";
  next(order: Order): void {
    order.transitionTo(new PaidState());
  }
  cancel(order: Order): void {
    order.transitionTo(new CancelledState());
  }
}

class PaidState implements OrderState {
  name = "Paid";
  next(order: Order): void {
    order.transitionTo(new ShippedState());
  }
  cancel(order: Order): void {
    console.log("Order " + order.id + ": refund initiated");
    order.transitionTo(new CancelledState());
  }
}

class ShippedState implements OrderState {
  name = "Shipped";
  next(order: Order): void {
    order.transitionTo(new DeliveredState());
  }
  cancel(order: Order): void {
    console.log("Order " + order.id + ": cannot cancel — already shipped");
  }
}

class DeliveredState implements OrderState {
  name = "Delivered";
  next(order: Order): void {
    console.log("Order " + order.id + ": already delivered — no further transitions");
  }
  cancel(order: Order): void {
    console.log("Order " + order.id + ": cannot cancel — already delivered");
  }
}

class CancelledState implements OrderState {
  name = "Cancelled";
  next(order: Order): void {
    console.log("Order " + order.id + ": cancelled — no transitions allowed");
  }
  cancel(order: Order): void {
    console.log("Order " + order.id + ": already cancelled");
  }
}

// Usage
const order = new Order("ORD-001");
order.next();   // Pending -> Paid
order.next();   // Paid -> Shipped
order.cancel(); // cannot cancel — already shipped
order.next();   // Shipped -> Delivered

Python

# State — Python (Order Processing)
from abc import ABC, abstractmethod

class OrderState(ABC):
    name: str
    @abstractmethod
    def next(self, order: "Order") -> None: ...
    @abstractmethod
    def cancel(self, order: "Order") -> None: ...

class Order:
    def __init__(self, order_id: str):
        self.id = order_id
        self.state: OrderState = PendingState()

    def next(self) -> None:
        self.state.next(self)

    def cancel(self) -> None:
        self.state.cancel(self)

    def transition_to(self, state: OrderState) -> None:
        print(f"Order {self.id}: {self.state.name} -> {state.name}")
        self.state = state

class PendingState(OrderState):
    name = "Pending"
    def next(self, order: Order) -> None:
        order.transition_to(PaidState())
    def cancel(self, order: Order) -> None:
        order.transition_to(CancelledState())

class PaidState(OrderState):
    name = "Paid"
    def next(self, order: Order) -> None:
        order.transition_to(ShippedState())
    def cancel(self, order: Order) -> None:
        print(f"Order {order.id}: refund initiated")
        order.transition_to(CancelledState())

class ShippedState(OrderState):
    name = "Shipped"
    def next(self, order: Order) -> None:
        order.transition_to(DeliveredState())
    def cancel(self, order: Order) -> None:
        print(f"Order {order.id}: cannot cancel — already shipped")

class DeliveredState(OrderState):
    name = "Delivered"
    def next(self, order: Order) -> None:
        print(f"Order {order.id}: already delivered")
    def cancel(self, order: Order) -> None:
        print(f"Order {order.id}: cannot cancel — delivered")

class CancelledState(OrderState):
    name = "Cancelled"
    def next(self, order: Order) -> None:
        print(f"Order {order.id}: cancelled — no transitions")
    def cancel(self, order: Order) -> None:
        print(f"Order {order.id}: already cancelled")

# Usage
order = Order("ORD-001")
order.next()    # Pending -> Paid
order.next()    # Paid -> Shipped
order.next()    # Shipped -> Delivered

4. Pattern Selection Guide

Choosing the right design pattern is as important as implementing it correctly. The table below helps you make quick decisions based on common scenarios.

ScenarioRecommended PatternWhy
Create objects based on configFactoryDecouple creation logic, easy to extend
Build objects with many optional paramsBuilderReadable, avoids constructor bloat
Integrate legacy or third-party libsAdapterConvert interfaces without modifying existing code
Combine behaviors dynamically (log+retry+cache)DecoratorStack features at runtime, more flexible than inheritance
Control access to expensive resourcesProxyLazy loading, caching, permission checks
Simplify complex APIs or subsystemsFacadeProvide clean entry point, reduce coupling
Event-driven notificationObserverDecouple publishers and subscribers
Switch algorithms at runtimeStrategyEliminate conditional branches, easy to add new algorithms
Undo/redo or task queuesCommandObjectify requests, support logging and rollback
Object behavior varies with stateStateEliminate if/switch chains for state checks

5. Design Patterns and SOLID Principles

Design patterns do not exist in a vacuum — they are closely tied to the SOLID principles. Understanding this relationship helps you choose the right pattern at the right time.

SOLID PrincipleRelated PatternsConnection
SRPCommand, Strategy, StateSeparate responsibilities into individual classes
OCPDecorator, Strategy, ObserverExtend behavior without modifying existing code
LSPFactory, AdapterSubclasses/adapters can replace parent/target interfaces
ISPAdapter, FacadeProvide lean interfaces without forcing unneeded methods
DIPFactory, Strategy, ObserverDepend on abstractions, not concretions

6. Common Anti-Patterns and Pitfalls

Using design patterns correctly requires judgment. Here are common mistakes developers make.

  • Over-engineering: Do not use Factory when there is only one implementation, or Command when undo is not needed
  • Singleton abuse: Global state makes testing hard; prefer dependency injection
  • Pattern stacking: Do not apply three or four patterns to the same problem; keep it simple
  • Ignoring language features: Python first-class functions can replace many Strategy/Command classes
  • Premature abstraction: Do not introduce patterns before seeing duplication; follow the Rule of Three

Conclusion

Design patterns are time-tested solutions, but they are not silver bullets. The key is understanding what problem each pattern solves, when it applies, and what trade-offs it introduces. In TypeScript, interfaces and the type system provide compile-time guarantees for patterns. In Python, duck typing and Protocols make pattern implementations more concise. Regardless of language, remember: code is written for humans to read, and the greatest value of patterns lies in communication and maintainability.

Frequently Asked Questions

What are design patterns and why should I learn them?

Design patterns are reusable solutions to common software design problems. They provide a shared vocabulary for developers, improve code maintainability, and help you avoid reinventing the wheel. Learning them accelerates your ability to architect flexible, scalable systems and makes you a more effective collaborator.

What is the difference between Creational, Structural, and Behavioral patterns?

Creational patterns (Factory, Builder, Singleton) deal with object creation mechanisms. Structural patterns (Adapter, Decorator, Proxy, Facade) concern class and object composition. Behavioral patterns (Observer, Strategy, Command, State) focus on communication and responsibility between objects.

When should I use the Factory pattern vs the Builder pattern?

Use Factory when you need to create objects without specifying the exact class, typically when you have a family of related objects. Use Builder when constructing complex objects step by step, especially when the object has many optional parameters or requires a specific construction order.

Is Singleton an anti-pattern?

Singleton is sometimes considered an anti-pattern because it introduces global state, makes unit testing harder, and violates the Single Responsibility Principle. However, it is appropriate for genuinely shared resources like configuration managers, connection pools, or logging systems. In modern code, dependency injection often replaces Singleton.

How does the Decorator pattern differ from inheritance?

Decorator adds behavior to individual objects at runtime without affecting other instances, while inheritance extends entire classes at compile time. Decorator follows the Open/Closed Principle by allowing new behavior without modifying existing code. It also supports combining multiple behaviors dynamically, which is not possible with single inheritance.

What is the Strategy pattern and when should I use it?

The Strategy pattern defines a family of interchangeable algorithms, encapsulates each one, and makes them interchangeable at runtime. Use it when you have multiple ways to perform an operation (sorting, validation, pricing) and want to switch between them without conditional logic or modifying client code.

How do design patterns apply to TypeScript and Python differently?

TypeScript leverages interfaces, generics, and type guards to enforce pattern contracts at compile time. Python uses duck typing, protocols (PEP 544), ABC classes, and decorators. Python first-class functions often simplify patterns like Strategy and Command. TypeScript access modifiers (private, protected) enable stricter encapsulation.

Which design patterns are most commonly used in modern web development?

Observer (event systems, pub/sub, React state), Strategy (middleware, validation), Factory (API clients, component creation), Decorator (Python decorators, TypeScript decorators, higher-order components), Proxy (API gateways, caching layers), and Command (undo/redo, task queues) are the most prevalent in modern web and backend development.

𝕏 Twitterin LinkedIn
Isso foi útil?

Fique atualizado

Receba dicas de dev e novos ferramentas semanalmente.

Sem spam. Cancele a qualquer momento.

Try These Related Tools

{ }JSON Formatter.*Regex TesterB→Base64 Encoder

Related Articles

Advanced TypeScript Guide: Generics, Conditional Types, Mapped Types, Decorators, and Type Narrowing

Master advanced TypeScript patterns. Covers generic constraints, conditional types with infer, mapped types (Partial/Pick/Omit), template literal types, discriminated unions, utility types deep dive, decorators, module augmentation, type narrowing, covariance/contravariance, and satisfies operator.

System Design Guide: Scalability, Load Balancers, Caching, CAP Theorem, and Interview Prep

Master system design for interviews and real-world applications. Covers horizontal/vertical scaling, load balancers, caching (CDN, Redis), database sharding, CAP theorem, message queues, rate limiting, URL shortener design, social media feed, and back-of-envelope calculations.

React Testing Guide: React Testing Library, Jest, Vitest, MSW, Playwright, and Code Coverage

Master React testing from unit to e2e. Covers React Testing Library queries, userEvent, renderHook, jest.mock(), Mock Service Worker (MSW), Vitest, async testing, snapshot tests, Redux/Zustand testing, Playwright vs Cypress, and code coverage with Istanbul.