Files
gh-dhofheinz-open-plugins-p…/commands/refactor/patterns.md
2025-11-29 18:20:21 +08:00

22 KiB

Design Pattern Introduction Operation

Introduce proven design patterns to solve recurring design problems and improve code structure.

Parameters

Received from $ARGUMENTS: All arguments after "patterns"

Expected format:

scope:"<path>" pattern:"<pattern-name>" [reason:"<motivation>"]

Parameter definitions:

  • scope (REQUIRED): Path to apply pattern (e.g., "src/services/", "src/components/UserForm.tsx")
  • pattern (REQUIRED): Pattern to introduce
    • factory - Create objects without specifying exact class
    • strategy - Encapsulate interchangeable algorithms
    • observer - Publish-subscribe event system
    • decorator - Add behavior dynamically
    • adapter - Make incompatible interfaces work together
    • repository - Abstract data access layer
    • dependency-injection - Invert control, improve testability
    • singleton - Ensure single instance (use sparingly!)
    • command - Encapsulate requests as objects
    • facade - Simplified interface to complex subsystem
  • reason (OPTIONAL): Why introducing this pattern (e.g., "eliminate switch statement", "improve testability")

Workflow

1. Pattern Selection Validation

Verify pattern is appropriate for the problem:

Anti-patterns to avoid:

  • Using pattern for pattern's sake (over-engineering)
  • Singleton when not needed (global state)
  • Factory when simple constructor suffices
  • Strategy for only 2 simple options

Good reasons to introduce pattern:

  • Eliminate complex conditional logic (Strategy, State)
  • Improve testability (Dependency Injection, Repository)
  • Enable extensibility without modification (Strategy, Decorator, Observer)
  • Simplify complex interface (Facade, Adapter)
  • Separate concerns (Repository, Factory)

2. Analyze Current Code Structure

Understand what needs to change:

# Find relevant files
find <scope> -type f -name "*.ts" -o -name "*.tsx"

# Analyze complexity
npx eslint <scope> --rule 'complexity: [error, { max: 10 }]'

# Check dependencies
npx madge <scope>

3. Pattern-Specific Implementation

Pattern Examples

Pattern 1: Factory Pattern

Use when:

  • Object creation is complex
  • Need to choose between multiple implementations
  • Want to hide creation logic
  • Need centralized object creation

Before (Direct instantiation everywhere):

// Scattered throughout codebase
const emailNotif = new EmailNotification(config);
await emailNotif.send(user, message);

const smsNotif = new SMSNotification(twilioConfig);
await smsNotif.send(user, message);

const pushNotif = new PushNotification(fcmConfig);
await pushNotif.send(user, message);

// Different interfaces, hard to extend

After (Factory Pattern):

// notifications/NotificationFactory.ts
interface Notification {
  send(user: User, message: string): Promise<void>;
}

export class NotificationFactory {
  constructor(private config: NotificationConfig) {}

  create(type: NotificationType): Notification {
    switch (type) {
      case 'email':
        return new EmailNotification(this.config.email);
      case 'sms':
        return new SMSNotification(this.config.sms);
      case 'push':
        return new PushNotification(this.config.push);
      default:
        throw new Error(`Unknown notification type: ${type}`);
    }
  }
}

// Usage
const factory = new NotificationFactory(config);
const notification = factory.create(user.preferences.notificationType);
await notification.send(user, message);

Improvements:

  • Centralized creation logic
  • Consistent interface
  • Easy to add new notification types
  • Configuration hidden from consumers

Pattern 2: Strategy Pattern

Use when:

  • Multiple algorithms for same task
  • Need to switch algorithms at runtime
  • Want to eliminate complex conditionals
  • Algorithms have different implementations but same interface

Before (Complex switch statement):

// PaymentProcessor.ts - 180 lines, complexity: 15
class PaymentProcessor {
  async processPayment(order: Order, method: string) {
    switch (method) {
      case 'credit_card':
        // 40 lines of credit card processing
        const ccGateway = new CreditCardGateway(this.config.stripe);
        const ccToken = await ccGateway.tokenize(order.paymentDetails);
        const ccCharge = await ccGateway.charge(ccToken, order.amount);
        await this.recordTransaction(order.id, ccCharge);
        await this.sendReceipt(order.customer, ccCharge);
        return ccCharge;

      case 'paypal':
        // 40 lines of PayPal processing
        const ppGateway = new PayPalGateway(this.config.paypal);
        const ppAuth = await ppGateway.authenticate();
        const ppPayment = await ppGateway.createPayment(order);
        await this.recordTransaction(order.id, ppPayment);
        await this.sendReceipt(order.customer, ppPayment);
        return ppPayment;

      case 'bank_transfer':
        // 40 lines of bank transfer processing
        const btGateway = new BankTransferGateway(this.config.bank);
        const btReference = await btGateway.generateReference(order);
        await this.sendInstructions(order.customer, btReference);
        await this.recordTransaction(order.id, btReference);
        return btReference;

      case 'crypto':
        // 40 lines of crypto processing
        const cryptoGateway = new CryptoGateway(this.config.crypto);
        const wallet = await cryptoGateway.generateAddress();
        await this.sendInstructions(order.customer, wallet);
        await this.recordTransaction(order.id, wallet);
        return wallet;

      default:
        throw new Error(`Unsupported payment method: ${method}`);
    }
  }
}

After (Strategy Pattern):

// payment/PaymentStrategy.ts
export interface PaymentStrategy {
  process(order: Order): Promise<PaymentResult>;
}

// payment/strategies/CreditCardStrategy.ts
export class CreditCardStrategy implements PaymentStrategy {
  constructor(private gateway: CreditCardGateway) {}

  async process(order: Order): Promise<PaymentResult> {
    const token = await this.gateway.tokenize(order.paymentDetails);
    const charge = await this.gateway.charge(token, order.amount);
    return {
      success: true,
      transactionId: charge.id,
      method: 'credit_card'
    };
  }
}

// payment/strategies/PayPalStrategy.ts
export class PayPalStrategy implements PaymentStrategy {
  constructor(private gateway: PayPalGateway) {}

  async process(order: Order): Promise<PaymentResult> {
    await this.gateway.authenticate();
    const payment = await this.gateway.createPayment(order);
    return {
      success: true,
      transactionId: payment.id,
      method: 'paypal'
    };
  }
}

// payment/strategies/BankTransferStrategy.ts
export class BankTransferStrategy implements PaymentStrategy {
  constructor(private gateway: BankTransferGateway) {}

  async process(order: Order): Promise<PaymentResult> {
    const reference = await this.gateway.generateReference(order);
    return {
      success: true,
      transactionId: reference,
      method: 'bank_transfer',
      requiresManualConfirmation: true
    };
  }
}

// payment/PaymentProcessor.ts - Now clean and extensible
export class PaymentProcessor {
  private strategies: Map<string, PaymentStrategy>;

  constructor(
    strategies: Map<string, PaymentStrategy>,
    private transactionRepo: TransactionRepository,
    private notificationService: NotificationService
  ) {
    this.strategies = strategies;
  }

  async processPayment(order: Order, method: string): Promise<PaymentResult> {
    const strategy = this.strategies.get(method);
    if (!strategy) {
      throw new UnsupportedPaymentMethodError(method);
    }

    const result = await strategy.process(order);
    await this.transactionRepo.record(order.id, result);
    await this.notificationService.sendReceipt(order.customer, result);

    return result;
  }
}

// Setup (dependency injection)
const processor = new PaymentProcessor(
  new Map([
    ['credit_card', new CreditCardStrategy(ccGateway)],
    ['paypal', new PayPalStrategy(ppGateway)],
    ['bank_transfer', new BankTransferStrategy(btGateway)],
    ['crypto', new CryptoStrategy(cryptoGateway)]
  ]),
  transactionRepo,
  notificationService
);

Improvements:

  • Complexity: 15 → 3 (80% reduction)
  • Open/Closed Principle: Add strategies without modifying processor
  • Testability: Each strategy tested independently
  • Maintainability: Clear separation of concerns
  • Extensibility: Add new payment methods easily

Pattern 3: Observer Pattern (Pub-Sub)

Use when:

  • Multiple objects need to react to events
  • Want loose coupling between components
  • Need publish-subscribe event system
  • State changes should notify dependents

Before (Tight coupling, manual notification):

class UserService {
  async createUser(data: CreateUserInput) {
    const user = await this.db.users.create(data);

    // Tightly coupled to all consumers
    await this.emailService.sendWelcome(user);
    await this.analyticsService.trackSignup(user);
    await this.subscriptionService.createTrialSubscription(user);
    await this.notificationService.sendAdminAlert(user);
    await this.auditLogger.logUserCreated(user);

    // Adding new consumer requires modifying this method
    return user;
  }
}

After (Observer Pattern):

// events/EventEmitter.ts
type EventHandler<T = any> = (data: T) => Promise<void> | void;

export class EventEmitter {
  private handlers: Map<string, EventHandler[]> = new Map();

  on(event: string, handler: EventHandler): void {
    if (!this.handlers.has(event)) {
      this.handlers.set(event, []);
    }
    this.handlers.get(event)!.push(handler);
  }

  async emit(event: string, data: any): Promise<void> {
    const handlers = this.handlers.get(event) || [];
    await Promise.all(handlers.map(handler => handler(data)));
  }
}

// events/UserEvents.ts
export const UserEvents = {
  CREATED: 'user.created',
  UPDATED: 'user.updated',
  DELETED: 'user.deleted'
} as const;

// services/UserService.ts - Now decoupled
export class UserService {
  constructor(
    private db: Database,
    private events: EventEmitter
  ) {}

  async createUser(data: CreateUserInput): Promise<User> {
    const user = await this.db.users.create(data);

    // Simply publish event
    await this.events.emit(UserEvents.CREATED, user);

    return user;
  }
}

// subscribers/WelcomeEmailSubscriber.ts
export class WelcomeEmailSubscriber {
  constructor(
    private emailService: EmailService,
    private events: EventEmitter
  ) {
    this.events.on(UserEvents.CREATED, this.handle.bind(this));
  }

  private async handle(user: User): Promise<void> {
    await this.emailService.sendWelcome(user);
  }
}

// subscribers/AnalyticsSubscriber.ts
export class AnalyticsSubscriber {
  constructor(
    private analyticsService: AnalyticsService,
    private events: EventEmitter
  ) {
    this.events.on(UserEvents.CREATED, this.handle.bind(this));
  }

  private async handle(user: User): Promise<void> {
    await this.analyticsService.trackSignup(user);
  }
}

// Setup
const events = new EventEmitter();
new WelcomeEmailSubscriber(emailService, events);
new AnalyticsSubscriber(analyticsService, events);
new SubscriptionSubscriber(subscriptionService, events);
new NotificationSubscriber(notificationService, events);
new AuditSubscriber(auditLogger, events);

const userService = new UserService(db, events);

Improvements:

  • Loose coupling: UserService doesn't know about consumers
  • Open/Closed: Add subscribers without modifying UserService
  • Testability: Test UserService without dependencies
  • Maintainability: Each subscriber isolated
  • Flexibility: Enable/disable subscribers easily

Pattern 4: Dependency Injection

Use when:

  • Want to improve testability
  • Need to swap implementations
  • Want loose coupling
  • Multiple dependencies

Before (Tight coupling, hard to test):

class UserService {
  private db = new Database(process.env.DB_URL!);
  private emailService = new EmailService(process.env.SMTP_CONFIG!);
  private logger = new Logger('UserService');

  async createUser(data: CreateUserInput) {
    this.logger.info('Creating user', data);

    const user = await this.db.users.create(data);

    await this.emailService.sendWelcome(user);

    return user;
  }
}

// Testing is painful - can't mock dependencies
test('createUser', async () => {
  // Cannot inject test database or mock email service!
  const service = new UserService();
  // ...
});

After (Dependency Injection):

// Define interfaces
interface IDatabase {
  users: {
    create(data: CreateUserData): Promise<User>;
    findById(id: string): Promise<User | null>;
  };
}

interface IEmailService {
  sendWelcome(user: User): Promise<void>;
}

interface ILogger {
  info(message: string, data?: any): void;
  error(message: string, error?: Error): void;
}

// Inject dependencies
class UserService {
  constructor(
    private db: IDatabase,
    private emailService: IEmailService,
    private logger: ILogger
  ) {}

  async createUser(data: CreateUserInput): Promise<User> {
    this.logger.info('Creating user', data);

    const user = await this.db.users.create(data);

    await this.emailService.sendWelcome(user);

    return user;
  }
}

// Production setup
const db = new PostgresDatabase(config.database);
const emailService = new SMTPEmailService(config.smtp);
const logger = new WinstonLogger('UserService');
const userService = new UserService(db, emailService, logger);

// Test setup - Easy mocking!
test('createUser sends welcome email', async () => {
  const mockDb = {
    users: {
      create: jest.fn().mockResolvedValue({ id: '1', email: 'test@example.com' })
    }
  };
  const mockEmail = {
    sendWelcome: jest.fn().mockResolvedValue(undefined)
  };
  const mockLogger = {
    info: jest.fn(),
    error: jest.fn()
  };

  const service = new UserService(mockDb, mockEmail, mockLogger);
  await service.createUser({ email: 'test@example.com', name: 'Test' });

  expect(mockEmail.sendWelcome).toHaveBeenCalledWith({ id: '1', email: 'test@example.com' });
});

Improvements:

  • Testability: Easy to inject mocks
  • Flexibility: Swap implementations (PostgreSQL → MongoDB)
  • Loose coupling: Depends on interfaces, not implementations
  • Clear dependencies: Constructor shows all dependencies

Pattern 5: Repository Pattern

Use when:

  • Want to abstract data access
  • Need to swap data sources
  • Want consistent query interface
  • Separate domain from persistence

Before (Data access mixed with business logic):

class UserService {
  async getUsersByRole(role: string) {
    // Direct database queries in service
    const users = await prisma.user.findMany({
      where: { role },
      include: {
        profile: true,
        posts: { where: { published: true } }
      }
    });
    return users;
  }

  async getActiveUsers() {
    const users = await prisma.user.findMany({
      where: { status: 'active', deletedAt: null }
    });
    return users;
  }

  // Many more methods with direct queries...
}

After (Repository Pattern):

// repositories/UserRepository.ts
export interface IUserRepository {
  findById(id: string): Promise<User | null>;
  findByEmail(email: string): Promise<User | null>;
  findByRole(role: string): Promise<User[]>;
  findActive(): Promise<User[]>;
  create(data: CreateUserData): Promise<User>;
  update(id: string, data: Partial<User>): Promise<User>;
  delete(id: string): Promise<void>;
}

export class PrismaUserRepository implements IUserRepository {
  constructor(private prisma: PrismaClient) {}

  async findById(id: string): Promise<User | null> {
    return this.prisma.user.findUnique({
      where: { id },
      include: { profile: true }
    });
  }

  async findByEmail(email: string): Promise<User | null> {
    return this.prisma.user.findUnique({
      where: { email },
      include: { profile: true }
    });
  }

  async findByRole(role: string): Promise<User[]> {
    return this.prisma.user.findMany({
      where: { role },
      include: {
        profile: true,
        posts: { where: { published: true } }
      }
    });
  }

  async findActive(): Promise<User[]> {
    return this.prisma.user.findMany({
      where: { status: 'active', deletedAt: null }
    });
  }

  async create(data: CreateUserData): Promise<User> {
    return this.prisma.user.create({ data });
  }

  async update(id: string, data: Partial<User>): Promise<User> {
    return this.prisma.user.update({ where: { id }, data });
  }

  async delete(id: string): Promise<void> {
    await this.prisma.user.update({
      where: { id },
      data: { deletedAt: new Date() }
    });
  }
}

// services/UserService.ts - Clean business logic
export class UserService {
  constructor(private userRepository: IUserRepository) {}

  async getUsersByRole(role: string): Promise<User[]> {
    return this.userRepository.findByRole(role);
  }

  async getActiveUsers(): Promise<User[]> {
    return this.userRepository.findActive();
  }

  // Business logic, no persistence concerns
}

// Easy to swap data sources
class InMemoryUserRepository implements IUserRepository {
  private users: Map<string, User> = new Map();

  async findById(id: string): Promise<User | null> {
    return this.users.get(id) || null;
  }

  // ... other methods using in-memory Map
}

// Testing with in-memory repository
test('getUsersByRole', async () => {
  const repo = new InMemoryUserRepository();
  await repo.create({ id: '1', email: 'admin@example.com', role: 'admin' });

  const service = new UserService(repo);
  const admins = await service.getUsersByRole('admin');

  expect(admins).toHaveLength(1);
});

Improvements:

  • Separation of concerns: Business logic separate from persistence
  • Testability: Easy to use in-memory repository for tests
  • Flexibility: Swap Prisma → TypeORM → MongoDB without changing services
  • Consistency: Standardized query interface
  • Caching: Easy to add caching layer in repository

Pattern 6: Decorator Pattern

Use when:

  • Want to add behavior dynamically
  • Need multiple combinations of features
  • Avoid subclass explosion
  • Wrap functionality around core

Before (Subclass explosion):

class Logger { log(message: string) { /* ... */ } }
class TimestampLogger extends Logger { /* adds timestamp */ }
class ColorLogger extends Logger { /* adds colors */ }
class FileLogger extends Logger { /* writes to file */ }
class TimestampColorLogger extends TimestampLogger { /* both timestamp and color */ }
class TimestampFileLogger extends TimestampLogger { /* both timestamp and file */ }
// Need class for every combination!

After (Decorator Pattern):

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

// Base implementation
class ConsoleLogger implements Logger {
  log(message: string): void {
    console.log(message);
  }
}

// Decorators
class TimestampDecorator implements Logger {
  constructor(private logger: Logger) {}

  log(message: string): void {
    const timestamp = new Date().toISOString();
    this.logger.log(`[${timestamp}] ${message}`);
  }
}

class ColorDecorator implements Logger {
  constructor(private logger: Logger, private color: string) {}

  log(message: string): void {
    this.logger.log(`\x1b[${this.color}m${message}\x1b[0m`);
  }
}

class FileDecorator implements Logger {
  constructor(private logger: Logger, private filePath: string) {}

  log(message: string): void {
    this.logger.log(message);
    fs.appendFileSync(this.filePath, message + '\n');
  }
}

// Compose decorators
let logger: Logger = new ConsoleLogger();
logger = new TimestampDecorator(logger);
logger = new ColorDecorator(logger, '32'); // green
logger = new FileDecorator(logger, './app.log');

logger.log('Hello World');
// Output: [2025-01-15T10:30:00.000Z] Hello World (in green, also in file)

Improvements:

  • Flexibility: Mix and match decorators
  • No subclass explosion: N decorators instead of 2^N classes
  • Open/Closed: Add decorators without modifying logger
  • Composition over inheritance

Output Format

# Design Pattern Introduction Report

## Pattern Applied: <pattern-name>

**Scope**: <path>
**Reason**: <motivation>

## Problem Statement

**Before**:
- <issue 1>
- <issue 2>
- <issue 3>

**Symptoms**:
- Complex conditional logic
- Tight coupling
- Difficult to test
- Hard to extend

## Solution: <Pattern Name>

**Benefits**:
- <benefit 1>
- <benefit 2>
- <benefit 3>

**Trade-offs**:
- <trade-off 1> (if any)

## Implementation

### Files Created
- <new-file-1>
- <new-file-2>

### Files Modified
- <modified-file-1>
- <modified-file-2>

### Code Changes

**Before**:
```typescript
<original-code>

After:

<refactored-code-with-pattern>

Verification

Tests:

  • All existing tests: PASS
  • New pattern tests: 12 added
  • Coverage: 78% → 85%

Metrics:

  • Complexity: 15 → 4 (73% improvement)
  • Coupling: High → Low
  • Extensibility: Improved

Usage Guide

How to use the new pattern:

<usage-example>

How to extend:

<extension-example>

Next Steps

Additional Improvements:


Pattern Introduction Complete: Code is now more flexible, testable, and maintainable.


## Error Handling

**Pattern not appropriate**:

Warning: may not be the best solution for this problem.

Current problem: Suggested pattern:

Reason:


**Over-engineering risk**:

Warning: Introducing may be over-engineering for current needs.

Current complexity: LOW Pattern complexity: HIGH

Recommendation: Consider simpler solutions first:

  1. Extract method/function
  2. Use simple conditional
  3. Wait until pattern truly needed (YAGNI)