Files
skills/solid/references/design-patterns.md
Mathias d6a71e370e
Some checks failed
release / tag (push) Has been cancelled
chore: bootstrap skills library — 19 skills + installer + CI auto-tag
Phase 1 of mathias/skills extraction (infra#62 Track D — homelab
next-step plan addendum). Imports ~/dev/.skills/ verbatim (19 skill
dirs + SKILLS_INDEX.md) and adds the installation surface:

- Taskfile.yml — install / update / list / release / check targets
- install.sh — bootstrap installer for hosts without Task. Idempotent
  symlink wirer; default checkout at ~/.local/share/skills/ on every
  host; SKILLS_REF env var pins a tag (default: main).
- .gitea/workflows/release.yml — auto-tag every push to main by
  Bump-Type footer (major/minor/patch, default patch). Skipped when
  commit contains [skip-release].
- README — usage, versioning, contribution flow, secret-hygiene rule.

Phase 1 wires Claude Code only (~/.claude/skills/<name> global +
<repo>/.claude/skills/<name> per-repo). Phase 2 adds Crush, opencode,
antigravity, and gitea-resident agents (cobalt-dingo, agentsquad)
once their skill conventions are researched.

Public repo, markdown-only — no secrets, no client names. Verified
via pre-push grep before initial push.

[skip-release]
2026-05-24 14:59:54 +02:00

10 KiB

Design Patterns

What Are Design Patterns?

Reusable solutions to common design problems. A shared vocabulary for discussing design.

WARNING: Don't Force Patterns

"Let patterns emerge from refactoring, don't force them upfront."

Patterns should solve problems you HAVE, not problems you MIGHT have.

When to Use Patterns

  1. You recognize the problem - You've seen it before
  2. The pattern fits - Not forcing it
  3. It simplifies - Doesn't add unnecessary complexity
  4. Team understands it - Shared knowledge

Creational Patterns

Singleton

Purpose: Ensure only one instance exists.

When to use: Global configuration, connection pools, logging.

Warning: Often overused. Consider dependency injection instead.

class Logger {
  private static instance: Logger;

  private constructor() {}

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

  log(message: string): void { ... }
}

Factory

Purpose: Create objects without specifying exact class.

When to use: Object creation logic is complex, or varies by type.

interface Notification {
  send(message: string): void;
}

class EmailNotification implements Notification { ... }
class SMSNotification implements Notification { ... }
class PushNotification implements Notification { ... }

class NotificationFactory {
  create(type: 'email' | 'sms' | 'push'): Notification {
    switch (type) {
      case 'email': return new EmailNotification();
      case 'sms': return new SMSNotification();
      case 'push': return new PushNotification();
    }
  }
}

Builder

Purpose: Construct complex objects step by step.

When to use: Objects with many optional parameters, test data creation.

class UserBuilder {
  private user: Partial<User> = {};

  withName(name: string): UserBuilder {
    this.user.name = name;
    return this;
  }

  withEmail(email: string): UserBuilder {
    this.user.email = email;
    return this;
  }

  withAge(age: number): UserBuilder {
    this.user.age = age;
    return this;
  }

  build(): User {
    return new User(
      this.user.name!,
      this.user.email!,
      this.user.age
    );
  }
}

// Usage
const user = new UserBuilder()
  .withName('Alice')
  .withEmail('alice@example.com')
  .build();

Prototype

Purpose: Create new objects by cloning existing ones.

When to use: Object creation is expensive, or you need copies with slight variations.

interface Prototype {
  clone(): Prototype;
}

class Document implements Prototype {
  constructor(
    public title: string,
    public content: string,
    public metadata: Metadata
  ) {}

  clone(): Document {
    return new Document(
      this.title,
      this.content,
      { ...this.metadata }
    );
  }
}

Structural Patterns

Adapter

Purpose: Make incompatible interfaces work together.

When to use: Integrating third-party libraries, legacy code.

// Third-party library with different interface
class OldPaymentAPI {
  makePayment(cents: number): boolean { ... }
}

// Our interface
interface PaymentGateway {
  charge(amount: Money): ChargeResult;
}

// Adapter
class OldPaymentAdapter implements PaymentGateway {
  constructor(private oldAPI: OldPaymentAPI) {}

  charge(amount: Money): ChargeResult {
    const cents = amount.toCents();
    const success = this.oldAPI.makePayment(cents);
    return success ? ChargeResult.success() : ChargeResult.failed();
  }
}

Decorator

Purpose: Add behavior to objects dynamically.

When to use: Adding features without modifying existing code.

interface Notifier {
  send(message: string): void;
}

class EmailNotifier implements Notifier {
  send(message: string): void {
    console.log(`Email: ${message}`);
  }
}

// Decorators
class SMSDecorator implements Notifier {
  constructor(private wrapped: Notifier) {}

  send(message: string): void {
    this.wrapped.send(message);
    console.log(`SMS: ${message}`);
  }
}

class SlackDecorator implements Notifier {
  constructor(private wrapped: Notifier) {}

  send(message: string): void {
    this.wrapped.send(message);
    console.log(`Slack: ${message}`);
  }
}

// Usage - compose behaviors
const notifier = new SlackDecorator(
  new SMSDecorator(
    new EmailNotifier()
  )
);
notifier.send('Alert!'); // Sends to all three

Proxy

Purpose: Control access to an object.

When to use: Lazy loading, access control, logging, caching.

interface Image {
  display(): void;
}

class RealImage implements Image {
  constructor(private filename: string) {
    this.loadFromDisk(); // Expensive
  }

  private loadFromDisk(): void { ... }

  display(): void { ... }
}

// Lazy loading proxy
class ImageProxy implements Image {
  private realImage: RealImage | null = null;

  constructor(private filename: string) {}

  display(): void {
    if (!this.realImage) {
      this.realImage = new RealImage(this.filename);
    }
    this.realImage.display();
  }
}

Composite

Purpose: Treat individual objects and compositions uniformly.

When to use: Tree structures, hierarchies (files/folders, UI components).

interface Component {
  getPrice(): number;
}

class Product implements Component {
  constructor(private price: number) {}

  getPrice(): number {
    return this.price;
  }
}

class Box implements Component {
  private children: Component[] = [];

  add(component: Component): void {
    this.children.push(component);
  }

  getPrice(): number {
    return this.children.reduce(
      (sum, child) => sum + child.getPrice(),
      0
    );
  }
}

// Usage
const smallBox = new Box();
smallBox.add(new Product(10));
smallBox.add(new Product(20));

const bigBox = new Box();
bigBox.add(smallBox);
bigBox.add(new Product(50));

console.log(bigBox.getPrice()); // 80

Behavioral Patterns

Strategy

Purpose: Define a family of algorithms, make them interchangeable.

When to use: Multiple ways to do something, switchable at runtime.

interface PricingStrategy {
  calculate(basePrice: number): number;
}

class RegularPricing implements PricingStrategy {
  calculate(basePrice: number): number {
    return basePrice;
  }
}

class PremiumDiscount implements PricingStrategy {
  calculate(basePrice: number): number {
    return basePrice * 0.8; // 20% off
  }
}

class BlackFriday implements PricingStrategy {
  calculate(basePrice: number): number {
    return basePrice * 0.5; // 50% off
  }
}

class ShoppingCart {
  constructor(private pricing: PricingStrategy) {}

  calculateTotal(items: Item[]): number {
    const base = items.reduce((sum, i) => sum + i.price, 0);
    return this.pricing.calculate(base);
  }
}

Observer

Purpose: Notify multiple objects about state changes.

When to use: Event systems, pub/sub, reactive updates.

interface Observer {
  update(event: Event): void;
}

class EventEmitter {
  private observers: Observer[] = [];

  subscribe(observer: Observer): void {
    this.observers.push(observer);
  }

  unsubscribe(observer: Observer): void {
    this.observers = this.observers.filter(o => o !== observer);
  }

  notify(event: Event): void {
    this.observers.forEach(o => o.update(event));
  }
}

// Usage
class OrderService extends EventEmitter {
  placeOrder(order: Order): void {
    // Process order...
    this.notify({ type: 'ORDER_PLACED', order });
  }
}

class EmailService implements Observer {
  update(event: Event): void {
    if (event.type === 'ORDER_PLACED') {
      this.sendConfirmation(event.order);
    }
  }
}

Template Method

Purpose: Define algorithm skeleton, let subclasses override steps.

When to use: Common algorithm with varying steps.

abstract class DataExporter {
  // Template method - defines the algorithm
  export(data: Data[]): void {
    this.validate(data);
    const formatted = this.format(data);
    this.write(formatted);
    this.notify();
  }

  // Common steps
  private validate(data: Data[]): void { ... }
  private notify(): void { ... }

  // Steps to override
  protected abstract format(data: Data[]): string;
  protected abstract write(content: string): void;
}

class CSVExporter extends DataExporter {
  protected format(data: Data[]): string {
    return data.map(d => d.toCSV()).join('\n');
  }

  protected write(content: string): void {
    fs.writeFileSync('export.csv', content);
  }
}

class JSONExporter extends DataExporter {
  protected format(data: Data[]): string {
    return JSON.stringify(data);
  }

  protected write(content: string): void {
    fs.writeFileSync('export.json', content);
  }
}

Command

Purpose: Encapsulate a request as an object.

When to use: Undo/redo, queuing, logging actions.

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

class AddItemCommand implements Command {
  constructor(
    private cart: Cart,
    private item: Item
  ) {}

  execute(): void {
    this.cart.add(this.item);
  }

  undo(): void {
    this.cart.remove(this.item);
  }
}

class CommandHistory {
  private history: Command[] = [];

  execute(command: Command): void {
    command.execute();
    this.history.push(command);
  }

  undo(): void {
    const command = this.history.pop();
    command?.undo();
  }
}

Pattern Awareness

The Four-Dimensional Lens

When analyzing new code/libraries, ask:

  1. What problem does it solve? (Creational, Structural, Behavioral)
  2. What scope? (Object-level, Class-level, System-level)
  3. When is it applied? (Compile-time, Runtime)
  4. How coupled? (Tight, Loose)

This helps recognize patterns even in unfamiliar code.


Anti-Patterns to Avoid

Anti-Pattern Problem Solution
God Object Class does everything Split by responsibility
Spaghetti Code Tangled, no structure Refactor to layers
Golden Hammer Using one pattern for everything Match pattern to problem
Premature Optimization Optimizing before needed YAGNI, profile first
Copy-Paste Programming Duplication Extract, Rule of Three