Files
skills/skills/solid/references/design-patterns.md
T
mathiasandClaude Opus 5 56a947b2a0 refactor(plugin)!: ship as a Claude Code plugin, one dir link per harness
Skills move to skills/<name>/SKILL.md and the repo gains both manifests, so
it is now a marketplace hosting exactly one plugin whose root is the repo
root (source: "."). Claude Code installs it from git; no symlinks involved.

Why: install.sh wired the library into every consuming repo as an ABSOLUTE
symlink to $CHECKOUT_DIR. The checkout moved three times, most recently when
local-dev@1966d5a dropped the embedded .skills/ copy, and nothing migrated
the links — 243 dangling links across 12 repos, hidden from git by the
**/.claude/skills/ ignore rule. Worse, ~/dev spans two path roots
(/Users/mathias on the iCloud-shared Macs, /home/mathias on koala), so an
absolute link stored there is broken on one side by construction. Re-running
the installer cannot fix that; only removing the mechanism can.

install.sh keeps the harnesses with no plugin system and drops from 42 links
per harness to one directory link, via the convergent ~/.agents/skills
convention that Pi and OpenCode both read:

  ~/.agents/skills          -> $CHECKOUT_DIR/skills
  ~/.config/crush/skills    -> ~/.agents/skills
  ~/.config/opencode/skills -> ~/.agents/skills

wire_claude_repo is deleted outright rather than gated behind a flag — on a
mixed-root fleet it cannot be made correct. Antigravity and Vibe targets go
too; both unused. CHECKOUT_DIR keeps its host-local default deliberately:
pointing it into ~/dev would put a git working tree inside iCloud with two
concurrent writers.

Taskfile drops five near-identical wiring loops (they had already drifted)
and delegates to install.sh. `task check` gains frontmatter validation, a
stray-root-entry gate, and manifest/tag version parity — the loose
secret-hygiene.md sat unloadable for months because nothing checked layout.

Verified: claude plugin validate . passes clean; a probe skill loaded as
skills:zzprobe through --plugin-dir; task check green at 42 skills.

Refs: #23
Bump-Type: minor

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m
2026-08-23 14:29:51 +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