Files
asepharyana-hub-guide/skills/design-patterns/references/catalog.md
T
asepharyana e513cddd68 feat(hub-guide): expand plugin with 26 best-practice skills, hooks, and references
Transform hub-guide from a single-skill Hub monorepo guide into a
comprehensive programming best-practice plugin covering all situations.

Skills (26):
- Core: engineering-principles, clean-code, clean-architecture,
  design-patterns, testing, error-handling, security, api-design,
  git-workflow, documentation, logging-observability, performance
- Languages: typescript, python, rust, go
- Frameworks: react-frontend, elysiajs, hono-backend, drizzle-database, nextjs
- Infrastructure: docker, ci-cd, monitoring
- Monorepo: monorepo, hub-guide (existing)

Hooks:
- SessionStart: auto-detect project type and activate relevant skills
- PreToolUse (Write|Edit): inject language-specific rules per file type

Reference files for deep dives:
- clean-architecture/references/solid.md (SOLID + component principles)
- design-patterns/references/catalog.md (full GoF catalog with examples)
- testing/references/mocks.md (test double taxonomy)

Restructure plugin to modern skills/ directory format.
2026-07-25 11:35:16 +07:00

11 KiB

Design Patterns Catalog

Creational Patterns

Factory Method

Define an interface for creating an object, but let subclasses decide which class to instantiate.

// The pattern
interface PaymentGateway { charge(amount: Money): Result }
class StripeGateway implements PaymentGateway { ... }
class MidtransGateway implements PaymentGateway { ... }

class PaymentFactory {
  static create(type: 'stripe' | 'midtrans'): PaymentGateway {
    if (type === 'stripe') return new StripeGateway();
    return new MidtransGateway();
  }
}

When: A class doesn't know the exact type of objects it must create. Modern alt: (type) => new Gateway(type) — factory function, not a class.

Abstract Factory

Provide an interface for creating families of related objects.

interface UIFactory { createButton(): Button; createDialog(): Dialog }
class MaterialFactory implements UIFactory { ... }
class AntDesignFactory implements UIFactory { ... }

When: You need to enforce that objects from the same family are used together (Material button with Material dialog). Real example: UI theme systems, database abstraction across vendors.

Builder

Separate the construction of a complex object from its representation.

class QueryBuilder {
  private select: string[] = [];
  private from = '';
  private where: string[] = [];

  select(...fields: string[]) { this.select.push(...fields); return this; }
  from(table: string) { this.from = table; return this; }
  where(condition: string) { this.where.push(condition); return this; }
  build() { return `SELECT ${this.select.join(', ')} FROM ${this.from} WHERE ${this.where.join(' AND ')}`; }
}

// Usage
new QueryBuilder().select('id', 'name').from('users').where('active = true').build();

When: An object has many optional parts or complex construction.

Singleton

Ensure a class has only one instance and provide a global access point.

// The pattern — but prefer DI
class Config {
  private static instance: Config;
  static getInstance(): Config {
    if (!Config.instance) Config.instance = new Config();
    return Config.instance;
  }
}

⚠️ Use sparingly. Singletons are global state in disguise. Prefer DI container managing a single instance. Only acceptable: Logging, hardware interfaces, actual system-wide singletons.

Prototype

Create new objects by cloning an existing instance.

class Document implements Cloneable {
  clone(): Document { return structuredClone(this); }
}

When: Creating objects is expensive and cloning is cheaper (config objects, document templates).


Structural Patterns

Adapter

Convert one interface into another that the client expects.

// Legacy interface
class LegacyMailer { sendMail(from: string, to: string, body: string) { ... } }
// New interface
interface NotificationService { send(recipient: string, message: string): void }

class MailerAdapter implements NotificationService {
  constructor(private legacy: LegacyMailer) {}
  send(recipient: string, message: string) {
    this.legacy.sendMail('noreply@x.com', recipient, message);
  }
}

When: Interface mismatch between expected and actual. Wrap, don't modify.

Bridge

Decouple an abstraction from its implementation so they can vary independently.

// Abstraction
abstract class Remote { abstract press(): void }
class TVRemote extends Remote { constructor(private device: TV) {} }
class RadioRemote extends Remote { constructor(private device: Radio) {} }

// Implementation
interface Device { on(): void; off(): void }
class SonyTV implements Device { ... }
class PhilipsTV implements Device { ... }

When: Both the abstraction and implementation vary independently.

Composite

Compose objects into tree structures to represent part-whole hierarchies.

interface FileSystemNode { getSize(): number; }
class File implements FileSystemNode { constructor(private size: number) {} getSize() { return this.size; } }
class Directory implements FileSystemNode {
  constructor(private children: FileSystemNode[]) {}
  getSize() { return this.children.reduce((acc, c) => acc + c.getSize(), 0); }
}

When: Tree structures (UI trees, file systems, menu hierarchies).

Decorator

Dynamically add responsibilities to an object.

interface DataSource { write(data: string): void; read(): string }
class FileDataSource implements DataSource { ... }

class CompressionDecorator implements DataSource {
  constructor(private wrappee: DataSource) {}
  write(data: string) { this.wrappee.write(compress(data)); }
  read() { return decompress(this.wrappee.read()); }
}

new CompressionDecorator(new EncryptionDecorator(new FileDataSource('file.txt')));

When: You need to layer behavior (middleware, streams, I/O pipelines). Modern alt: Hono/Express middleware chains are a functional take on Decorator.

Facade

Provide a unified interface to a complex subsystem.

class PaymentFacade {
  async pay(amount: Money, method: PaymentMethod): Promise<Receipt> {
    const gateway = PaymentFactory.create(method.type);
    const result = await gateway.charge(amount);
    await this.receiptRepo.save(result.receipt);
    await this.notifier.send(result.receipt);
    return result.receipt;
  }
}

When: Simplifying a complex subsystem with a single entry point.

Flyweight

Share common state across many objects to save memory.

class Character {
  constructor(public char: string, public style: TextStyle) {} // style shared
}
class TextStyle { constructor(public font: string, public size: number, public bold: boolean) {} }

When: Many fine-grained objects with shared intrinsic state (text editors, game particle systems).

Proxy

Control access to another object.

class CachedUserRepo implements UserRepository {
  private cache = new Map<string, User>();
  constructor(private real: UserRepository) {}
  async findById(id: string): Promise<User | null> {
    if (this.cache.has(id)) return this.cache.get(id)!;
    const user = await this.real.findById(id);
    if (user) this.cache.set(id, user);
    return user;
  }
}

When: Lazy loading, caching, access control, logging — without modifying the real object.


Behavioral Patterns

Strategy

Define a family of algorithms, encapsulate each one, and make them interchangeable.

interface AuthStrategy { authenticate(creds: Credentials): Promise<User> }
class PasswordAuth implements AuthStrategy { ... }
class OAuthStrategy implements AuthStrategy { ... }
class MFAStrategy implements AuthStrategy { ... }

class AuthService {
  constructor(private strategy: AuthStrategy) {}
  async login(creds: Credentials) { return this.strategy.authenticate(creds); }
}

When: Multiple algorithms for the same task. Modern alt: Pass a lambda/function.

Observer

Define a one-to-many dependency so that when one object changes state, all dependents are notified.

class EventBus {
  private handlers = new Map<string, Function[]>();
  on(event: string, handler: Function) { ... }
  emit(event: string, data: unknown) { ... }
}

When: Event handling, pub/sub, data binding. Modern alt: Reactive streams (RxJS), event emitters.

Command

Encapsulate a request as an object, allowing parameterization, queuing, and undo.

interface Command { execute(): void; undo(): void }
class CreateOrderCommand implements Command { ... }
class CancelOrderCommand implements Command { ... }
class CommandQueue {
  private history: Command[] = [];
  execute(cmd: Command) { cmd.execute(); this.history.push(cmd); }
  undo() { this.history.pop()?.undo(); }
}

When: Job queues, undo/redo, transaction logging. Modern alt: Functions as first-class commands (JS/TS closures).

Template Method

Define the skeleton of an algorithm, letting subclasses override specific steps.

abstract class DataExporter {
  export(): string {
    const data = this.fetch();
    const formatted = this.format(data);
    return this.write(formatted);
  }
  abstract fetch(): unknown;
  abstract format(data: unknown): string;
  abstract write(data: string): string;
}
class CSVExporter extends DataExporter { ... }
class JSONExporter extends DataExporter { ... }

When: Multiple implementations share the same algorithm structure but vary in steps.

State

Allow an object to alter its behavior when its internal state changes.

interface OrderState { next(order: Order): void; cancel(order: Order): void }
class PendingState implements OrderState { ... }
class PaidState implements OrderState { ... }
class ShippedState implements OrderState { ... }
class CancelledState implements OrderState { ... }

When: An object's behavior depends on its state and must change at runtime (orders, documents, workflows).

Chain of Responsibility

Pass a request along a chain of handlers until one processes it.

abstract class Handler {
  constructor(protected next?: Handler) {}
  abstract handle(request: HttpRequest): HttpResponse | null;
}

class AuthHandler extends Handler { handle(r) { return r.isAuthenticated ? this.next?.handle(r) : new HttpResponse(401); } }
class RateLimitHandler extends Handler { handle(r) { return r.notRateLimited ? this.next?.handle(r) : new HttpResponse(429); } }
class Router extends Handler { handle(r) { return new HttpResponse(200, 'OK'); } }

new AuthHandler(new RateLimitHandler(new Router()));

When: Middleware, validation pipelines, logging chains.

Visitor

Represent an operation to be performed on elements of an object structure.

interface ASTNode { accept(v: Visitor): void }
class NumberNode implements ASTNode { accept(v) { v.visitNumber(this); } }
class BinaryOpNode implements ASTNode { accept(v) { v.visitBinaryOp(this); } }

interface Visitor { visitNumber(node: NumberNode): void; visitBinaryOp(node: BinaryOpNode): void }
class Evaluator implements Visitor { ... }
class ASTPrinter implements Visitor { ... }

When: You need to add new operations to a stable object structure. Use with caution — it violates OCP if the structure changes.


When to NOT Use a Pattern

  • Pattern for pattern's sake — a simple function is better than a Strategy class with one implementation.
  • AbstractFactoryFactory — over-abstracting what a simple new handles.
  • Singleton as global state — use DI with single instance.
  • Observer with no actual observers — simple callbacks suffice.
  • Visitor with a changing object structure — every new type means updating all visitors.