Design Patterns: Building Maintainable Code Architecture

Master creational, structural, and behavioral design patterns with modern applications

Design Patterns: Building Maintainable Code Architecture

Design patterns are proven solutions in software development. This article explores common patterns and their practical applications.

Design Patterns Overview

Why Design Patterns Matter

Value of Design Patterns:
┌─────────────────────────────────────────────────────┐
│                                                     │
│   Code Reuse                                        │
│   └── Proven solutions to common problems          │
│                                                     │
│   Maintainability                                   │
│   └── Clear code structure                         │
│                                                     │
│   Team Collaboration                                │
│   └── Shared design vocabulary                     │
│                                                     │
│   Flexibility                                       │
│   └── Adapt to changing requirements               │
│                                                     │
└─────────────────────────────────────────────────────┘
TypePurposeCommon Patterns
CreationalObject creationSingleton, Factory, Builder
StructuralObject compositionAdapter, Decorator, Proxy
BehavioralObject interactionObserver, Strategy, Command

Creational Patterns

Singleton Pattern

// Singleton - Ensures only one instance exists
class Database {
  private static instance: Database;
  private connection: Connection;

  private constructor() {
    this.connection = this.connect();
  }

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

  private connect(): Connection {
    return new Connection('mongodb://localhost:27017');
  }

  query(sql: string) {
    return this.connection.execute(sql);
  }
}

// Usage
const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1 === db2); // true

Factory Pattern

// Product interface
interface Button {
  render(): void;
  onClick(handler: () => void): void;
}

// Concrete products
class WindowsButton implements Button {
  render() {
    console.log('Rendering Windows style button');
  }
  onClick(handler: () => void) {
    console.log('Binding Windows click event');
    handler();
  }
}

class MacButton implements Button {
  render() {
    console.log('Rendering Mac style button');
  }
  onClick(handler: () => void) {
    console.log('Binding Mac click event');
    handler();
  }
}

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

// Usage
const button = ButtonFactory.createButton('mac');
button.render();

Builder Pattern

// Building complex objects
class QueryBuilder {
  private query: string = '';
  private table: string = '';
  private conditions: string[] = [];
  private orderBy: string = '';
  private limitValue: number = 0;

  select(fields: string[]): this {
    this.query = `SELECT ${fields.join(', ')}`;
    return this;
  }

  from(table: string): this {
    this.table = table;
    return this;
  }

  where(condition: string): this {
    this.conditions.push(condition);
    return this;
  }

  order(field: string, direction: 'ASC' | 'DESC' = 'ASC'): this {
    this.orderBy = `ORDER BY ${field} ${direction}`;
    return this;
  }

  limit(count: number): this {
    this.limitValue = count;
    return this;
  }

  build(): string {
    let sql = `${this.query} FROM ${this.table}`;

    if (this.conditions.length > 0) {
      sql += ` WHERE ${this.conditions.join(' AND ')}`;
    }
    if (this.orderBy) {
      sql += ` ${this.orderBy}`;
    }
    if (this.limitValue > 0) {
      sql += ` LIMIT ${this.limitValue}`;
    }

    return sql;
  }
}

// Usage
const query = new QueryBuilder()
  .select(['id', 'name', 'email'])
  .from('users')
  .where('status = "active"')
  .where('age > 18')
  .order('created_at', 'DESC')
  .limit(10)
  .build();

console.log(query);
// SELECT id, name, email FROM users WHERE status = "active" AND age > 18 ORDER BY created_at DESC LIMIT 10

Structural Patterns

Adapter Pattern

// Old payment interface
interface OldPaymentSystem {
  processPayment(amount: number): void;
}

class LegacyPayment implements OldPaymentSystem {
  processPayment(amount: number) {
    console.log(`Legacy system processing: $${amount}`);
  }
}

// New payment interface
interface NewPaymentSystem {
  pay(data: { amount: number; currency: string }): Promise<boolean>;
}

// Adapter
class PaymentAdapter implements NewPaymentSystem {
  private legacyPayment: OldPaymentSystem;

  constructor(legacyPayment: OldPaymentSystem) {
    this.legacyPayment = legacyPayment;
  }

  async pay(data: { amount: number; currency: string }): Promise<boolean> {
    try {
      this.legacyPayment.processPayment(data.amount);
      return true;
    } catch (error) {
      return false;
    }
  }
}

// Usage
const legacy = new LegacyPayment();
const adapter = new PaymentAdapter(legacy);
await adapter.pay({ amount: 100, currency: 'USD' });

Decorator Pattern

// Base component interface
interface Coffee {
  cost(): number;
  description(): string;
}

// Base coffee
class SimpleCoffee implements Coffee {
  cost() {
    return 10;
  }
  description() {
    return 'Simple coffee';
  }
}

// Decorator base class
abstract class CoffeeDecorator implements Coffee {
  protected coffee: Coffee;

  constructor(coffee: Coffee) {
    this.coffee = coffee;
  }

  cost(): number {
    return this.coffee.cost();
  }

  description(): string {
    return this.coffee.description();
  }
}

// Concrete decorators
class MilkDecorator extends CoffeeDecorator {
  cost() {
    return this.coffee.cost() + 3;
  }
  description() {
    return `${this.coffee.description()} + milk`;
  }
}

class SugarDecorator extends CoffeeDecorator {
  cost() {
    return this.coffee.cost() + 1;
  }
  description() {
    return `${this.coffee.description()} + sugar`;
  }
}

// Usage
let coffee: Coffee = new SimpleCoffee();
console.log(`${coffee.description()}: $${coffee.cost()}`);

coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
console.log(`${coffee.description()}: $${coffee.cost()}`);
// Simple coffee + milk + sugar: $14

Proxy Pattern

// Image interface
interface Image {
  display(): void;
}

// Real image
class RealImage implements Image {
  private filename: string;

  constructor(filename: string) {
    this.filename = filename;
    this.loadFromDisk();
  }

  private loadFromDisk() {
    console.log(`Loading image: ${this.filename}`);
  }

  display() {
    console.log(`Displaying image: ${this.filename}`);
  }
}

// Proxy image (lazy loading)
class ProxyImage implements Image {
  private realImage: RealImage | null = null;
  private filename: string;

  constructor(filename: string) {
    this.filename = filename;
  }

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

// Usage
const image = new ProxyImage('photo.jpg');
// Image not loaded yet

image.display(); // Loads on first call
image.display(); // Uses cached instance

Behavioral Patterns

Observer Pattern

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

// Subject interface
interface Subject {
  subscribe(observer: Observer): void;
  unsubscribe(observer: Observer): void;
  notify(data: any): void;
}

// Event emitter implementation
class EventEmitter implements Subject {
  private observers: Observer[] = [];

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

  unsubscribe(observer: Observer) {
    const index = this.observers.indexOf(observer);
    if (index > -1) {
      this.observers.splice(index, 1);
    }
  }

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

// Concrete observers
class Logger implements Observer {
  update(data: any) {
    console.log('Logging:', data);
  }
}

class EmailNotifier implements Observer {
  update(data: any) {
    console.log('Sending email notification:', data);
  }
}

// Usage
const emitter = new EventEmitter();
const logger = new Logger();
const emailer = new EmailNotifier();

emitter.subscribe(logger);
emitter.subscribe(emailer);

emitter.notify({ event: 'user_registered', userId: 123 });

Strategy Pattern

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

// Concrete strategies
class CreditCardPayment implements PaymentStrategy {
  private cardNumber: string;

  constructor(cardNumber: string) {
    this.cardNumber = cardNumber;
  }

  pay(amount: number) {
    console.log(`Credit card payment: $${amount}, Card: ${this.cardNumber}`);
  }
}

class PayPalPayment implements PaymentStrategy {
  private email: string;

  constructor(email: string) {
    this.email = email;
  }

  pay(amount: number) {
    console.log(`PayPal payment: $${amount}, Email: ${this.email}`);
  }
}

class CryptoPayment implements PaymentStrategy {
  pay(amount: number) {
    console.log(`Crypto payment: $${amount}`);
  }
}

// Context
class PaymentContext {
  private strategy: PaymentStrategy;

  setStrategy(strategy: PaymentStrategy) {
    this.strategy = strategy;
  }

  executePayment(amount: number) {
    this.strategy.pay(amount);
  }
}

// Usage
const payment = new PaymentContext();

payment.setStrategy(new CreditCardPayment('1234-5678'));
payment.executePayment(100);

payment.setStrategy(new PayPalPayment('user@example.com'));
payment.executePayment(200);

Command Pattern

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

// Receiver
class TextEditor {
  private content: string = '';

  write(text: string) {
    this.content += text;
  }

  deleteLast(length: number) {
    this.content = this.content.slice(0, -length);
  }

  getContent() {
    return this.content;
  }
}

// Concrete command
class WriteCommand implements Command {
  private editor: TextEditor;
  private text: string;

  constructor(editor: TextEditor, text: string) {
    this.editor = editor;
    this.text = text;
  }

  execute() {
    this.editor.write(this.text);
  }

  undo() {
    this.editor.deleteLast(this.text.length);
  }
}

// Invoker
class CommandManager {
  private history: Command[] = [];

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

  undo() {
    const command = this.history.pop();
    if (command) {
      command.undo();
    }
  }
}

// Usage
const editor = new TextEditor();
const manager = new CommandManager();

manager.execute(new WriteCommand(editor, 'Hello '));
manager.execute(new WriteCommand(editor, 'World'));
console.log(editor.getContent()); // Hello World

manager.undo();
console.log(editor.getContent()); // Hello

Modern Applications

Design Patterns in React

// Compound Components Pattern
const Tabs = ({ children, defaultTab }) => {
  const [activeTab, setActiveTab] = useState(defaultTab);

  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      {children}
    </TabsContext.Provider>
  );
};

Tabs.List = ({ children }) => (
  <div className="tab-list">{children}</div>
);

Tabs.Tab = ({ value, children }) => {
  const { activeTab, setActiveTab } = useContext(TabsContext);
  return (
    <button
      className={activeTab === value ? 'active' : ''}
      onClick={() => setActiveTab(value)}
    >
      {children}
    </button>
  );
};

Tabs.Panel = ({ value, children }) => {
  const { activeTab } = useContext(TabsContext);
  return activeTab === value ? <div>{children}</div> : null;
};

// Usage
<Tabs defaultTab="tab1">
  <Tabs.List>
    <Tabs.Tab value="tab1">Tab 1</Tabs.Tab>
    <Tabs.Tab value="tab2">Tab 2</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panel value="tab1">Content 1</Tabs.Panel>
  <Tabs.Panel value="tab2">Content 2</Tabs.Panel>
</Tabs>

Dependency Injection

// Dependency injection container
class Container {
  private services = new Map<string, any>();

  register<T>(key: string, factory: () => T) {
    this.services.set(key, factory);
  }

  resolve<T>(key: string): T {
    const factory = this.services.get(key);
    if (!factory) {
      throw new Error(`Service ${key} not registered`);
    }
    return factory();
  }
}

// Usage
const container = new Container();

container.register('database', () => new Database());
container.register('userService', () =>
  new UserService(container.resolve('database'))
);

const userService = container.resolve('userService');

Best Practices Summary

Design Pattern Selection Guide:
┌─────────────────────────────────────────────────────┐
│   When to Use                                       │
│   ├── Recurring similar problems                   │
│   ├── Need increased flexibility                   │
│   ├── Team needs shared vocabulary                 │
│   └── Anticipating changes                         │
│                                                     │
│   Avoid Over-Engineering                            │
│   ├── Simple problems don't need complex patterns  │
│   ├── Make it work first, then optimize            │
│   ├── Introduce patterns during refactoring        │
│   └── Follow YAGNI principle                       │
│                                                     │
│   Common Combinations                               │
│   ├── Factory + Singleton                          │
│   ├── Strategy + Factory                           │
│   ├── Decorator + Composite                        │
│   └── Observer + Command                           │
└─────────────────────────────────────────────────────┘
ScenarioRecommended Pattern
Global state managementSingleton
Object creation variesFactory
Feature extensionDecorator
Event handlingObserver
Algorithm switchingStrategy

Design patterns aren’t silver bullets, but tools in your toolbox. Choose the right pattern to make your code more elegant.