patterns

SKILL.md

Patterns - Software Design & Architecture

Know when to use (and when NOT to use) design patterns


When to Use This Skill

Use this skill when:

  • Architecting new systems or refactoring existing ones
  • Identifying code smells and anti-patterns
  • Choosing appropriate design patterns for specific problems
  • Applying SOLID principles to improve code quality
  • Making code more testable, maintainable, and extensible

Don't use this skill when:

  • Simple, straightforward code is sufficient
  • Premature optimization or abstraction would add complexity
  • The problem doesn't warrant pattern application

Critical Patterns

Pattern 1: SOLID Principles

When: Designing maintainable, testable code

Single Responsibility Principle (SRP):

// ✅ Good: Each class has one responsibility
class UserRepository {
  save(user: User) { /* DB logic */ }
}

class EmailService {
  sendWelcomeEmail(user: User) { /* Email logic */ }
}

class UserReportGenerator {
  generate(user: User) { /* Report logic */ }
}

// ❌ Bad: God class doing everything
class UserService {
  saveUser(user: User) { /* ... */ }
  sendEmail(user: User) { /* ... */ }
  generateReport(user: User) { /* ... */ }
}

Dependency Inversion Principle (DIP):

// ✅ Good: Depend on abstractions
interface MessageSender {
  send(message: string): void;
}

class EmailService implements MessageSender {
  send(message: string) { /* SMTP logic */ }
}

class UserRegistration {
  constructor(private messageSender: MessageSender) {}

  register(user: User) {
    this.messageSender.send(`Welcome ${user.name}`);
  }
}

// ❌ Bad: Tight coupling to concrete class
class UserRegistration {
  private emailService = new EmailService();

  register(user: User) {
    this.emailService.send(`Welcome ${user.name}`);
  }
}

Why: SOLID principles create code that's easier to test, maintain, and extend. They prevent common pitfalls like tight coupling and god classes.

For complete SOLID deep dive: SOLID Principles Reference


Pattern 2: Factory Pattern

When: Creating objects with complex initialization or multiple variants

Good:

interface Button {
  render(): void;
}

class WindowsButton implements Button {
  render() { console.log('Render Windows button'); }
}

class MacButton implements Button {
  render() { console.log('Render Mac button'); }
}

class ButtonFactory {
  static create(os: 'windows' | 'mac'): Button {
    switch (os) {
      case 'windows': return new WindowsButton();
      case 'mac': return new MacButton();
      default: throw new Error('Unknown OS');
    }
  }
}

// Usage
const button = ButtonFactory.create('windows');
button.render();

When to use:

  • Object creation is complex or varies by context
  • Need to decouple object creation from usage
  • Creating families of related objects

When NOT to use:

  • Simple object creation (just use new)
  • Only one type of object exists

Pattern 3: Strategy Pattern

When: Swapping algorithms or behaviors at runtime

Good:

interface PaymentStrategy {
  pay(amount: number): void;
}

class CreditCardPayment implements PaymentStrategy {
  pay(amount: number) {
    console.log(`Paid $${amount} with credit card`);
  }
}

class PayPalPayment implements PaymentStrategy {
  pay(amount: number) {
    console.log(`Paid $${amount} with PayPal`);
  }
}

class ShoppingCart {
  constructor(private paymentStrategy: PaymentStrategy) {}

  checkout(amount: number) {
    this.paymentStrategy.pay(amount);
  }
}

// Usage
const cart = new ShoppingCart(new CreditCardPayment());
cart.checkout(100);

Bad:

// ❌ Multiple if/else statements
class ShoppingCart {
  checkout(amount: number, paymentType: string) {
    if (paymentType === 'creditCard') {
      // Process credit card
    } else if (paymentType === 'paypal') {
      // Process PayPal
    } else if (paymentType === 'crypto') {
      // Process crypto
    }
  }
}

Why: Strategy pattern eliminates conditional logic and makes adding new payment methods easy without modifying existing code (Open/Closed Principle).


Pattern 4: Observer Pattern

When: Implementing event-driven systems or pub/sub

Good:

interface Observer {
  update(data: any): void;
}

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

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

  notify(data: any) {
    this.observers.forEach(observer => observer.update(data));
  }
}

class EmailSubscriber implements Observer {
  update(data: any) {
    console.log(`Email sent: ${data}`);
  }
}

class SMSSubscriber implements Observer {
  update(data: any) {
    console.log(`SMS sent: ${data}`);
  }
}

// Usage
const newsletter = new Subject();
newsletter.subscribe(new EmailSubscriber());
newsletter.subscribe(new SMSSubscriber());
newsletter.notify('New article published!');

When to use:

  • One-to-many dependencies
  • Event systems
  • State synchronization across components

Pattern 5: Adapter Pattern

When: Integrating incompatible interfaces

Good:

// Legacy interface
class OldPaymentGateway {
  processPayment(amount: number) {
    console.log(`Old gateway: $${amount}`);
  }
}

// New interface
interface PaymentProcessor {
  pay(amount: number, currency: string): void;
}

// Adapter
class PaymentAdapter implements PaymentProcessor {
  constructor(private oldGateway: OldPaymentGateway) {}

  pay(amount: number, currency: string) {
    if (currency !== 'USD') {
      throw new Error('Old gateway only supports USD');
    }
    this.oldGateway.processPayment(amount);
  }
}

When to use:

  • Integrating third-party libraries
  • Working with legacy code
  • Reusing existing classes with different interfaces

Anti-Patterns

❌ Anti-Pattern 1: Premature Abstraction

Don't do this:

// ❌ Over-engineered for simple config
interface ConfigStrategy { get(key: string): any; }
class JSONConfigStrategy implements ConfigStrategy { /* ... */ }
class YAMLConfigStrategy implements ConfigStrategy { /* ... */ }
class ConfigFactory { /* ... */ }

Do this instead:

// ✅ Start simple
const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000
};

Why: YAGNI (You Aren't Gonna Need It). Add abstraction when you have a concrete need, not "just in case."


❌ Anti-Pattern 2: God Object

Don't do this:

// ❌ One class doing everything
class Application {
  handleAuth() { /* ... */ }
  renderUI() { /* ... */ }
  saveToDatabase() { /* ... */ }
  sendEmail() { /* ... */ }
  generateReports() { /* ... */ }
  // 50 more methods...
}

Do this instead:

// ✅ Apply Single Responsibility Principle
class AuthService { /* ... */ }
class UIRenderer { /* ... */ }
class DatabaseService { /* ... */ }
class EmailService { /* ... */ }
class ReportGenerator { /* ... */ }

❌ Anti-Pattern 3: Copy-Paste Programming

Don't do this:

// ❌ Duplicated code
function validateEmail(email: string) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

function validateUserEmail(user: User) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(user.email);
}

Do this instead:

// ✅ DRY (Don't Repeat Yourself)
function isValidEmail(email: string) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

function validateUserEmail(user: User) {
  return isValidEmail(user.email);
}

Code Examples

Example 1: Factory Pattern for Payment Processing

interface PaymentProcessor {
  process(amount: number): Promise<void>;
}

class StripeProcessor implements PaymentProcessor {
  async process(amount: number) {
    console.log(`Processing $${amount} via Stripe`);
  }
}

class PayPalProcessor implements PaymentProcessor {
  async process(amount: number) {
    console.log(`Processing $${amount} via PayPal`);
  }
}

class PaymentFactory {
  static create(provider: 'stripe' | 'paypal'): PaymentProcessor {
    switch (provider) {
      case 'stripe': return new StripeProcessor();
      case 'paypal': return new PayPalProcessor();
    }
  }
}

// Usage
const processor = PaymentFactory.create('stripe');
await processor.process(99.99);

Example 2: Dependency Injection for Testability

interface Logger {
  log(message: string): void;
}

class UserService {
  constructor(private logger: Logger) {}

  createUser(name: string) {
    this.logger.log(`Creating user: ${name}`);
    // User creation logic
  }
}

// Production
const service = new UserService(new ConsoleLogger());

// Testing
const mockLogger = { log: jest.fn() };
const testService = new UserService(mockLogger);

For comprehensive examples and detailed implementations, see the references/ folder.


Quick Reference

When to Use Each Pattern

Pattern Use When Don't Use When
Factory Complex object creation, multiple variants Simple new is sufficient
Builder Many optional parameters Few parameters, simple construction
Singleton Truly need ONE global instance Just want convenience (use DI)
Adapter Integrating incompatible interfaces Interfaces already compatible
Decorator Adding responsibilities dynamically Static behavior is fine
Facade Simplifying complex subsystem Subsystem is already simple
Observer Event-driven, one-to-many updates Simple callbacks work
Strategy Swappable algorithms at runtime Fixed algorithm
Command Undo/redo, queuing operations Direct method calls work

SOLID Quick Checklist

  • S: Each class has single, focused responsibility
  • O: New features added via extension, not modification
  • L: Subtypes don't break parent class behavior
  • I: Interfaces are small and focused
  • D: Depend on abstractions, inject dependencies

Progressive Disclosure

For detailed implementations and examples:

  • Design Patterns Guide - Complete pattern implementations (Factory, Builder, Singleton, Adapter, Decorator, Facade, Observer, Strategy, Command)
  • SOLID Principles - Deep dive into SRP, OCP, LSP, ISP, DIP with real-world examples

References


Maintained by dsmj-ai-toolkit

Weekly Installs
5
First Seen
Feb 16, 2026
Installed on
github-copilot5
codex5
amp5
kimi-cli5
gemini-cli5
opencode5