Design Patterns

Overview

Design patterns are typical solutions to common problems in software design. They are like blueprints that you can customize to solve a particular design problem in your code. They are not finished designs, but templates for how to solve a problem.

Core Concepts

  • Creational Patterns: Deal with object creation mechanisms, trying to solve the problem of creating objects in an existing system while hiding the logic of how they are created.
    • Singleton: Ensures a class has only one instance and provides a global point of access to it.
    • Factory Method: Provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created.
    • Abstract Factory: Lets you produce families of related objects without specifying their concrete classes.
    • Builder: Used to construct complex objects step by step.
  • Structural Patterns: Deal with how classes and objects are composed to form larger structures.
    • Adapter: Allows incompatible interfaces to work together.
    • Decorator: Dynamically adds behavior to an object without changing its implementation.
    • Facade: Provides a simplified interface to a complex system of classes.
    • Proxy: Provides a placeholder for another object to control access to it.
  • Behavioral Patterns: Deal with how objects communicate and assign responsibilities.
    • Observer: A subscription mechanism to notify multiple objects about any events that happen to the object they’re observing.
    • Strategy: Defines a family of algorithms, encapsulates each one, and makes them interchangeable.
    • Command: Turns a request into a stand-alone object that contains all information about the request.
    • State: Allows an object to alter its behavior when its internal state changes.

Code Examples

// Strategy Pattern Example
interface Strategy {
    execute(): void;
}

class PaymentWithCreditCard implements Strategy {
    execute() { console.log("Paying with Credit Card"); }
}

class PaymentWithPayPal implements Strategy {
    execute() { console.log("Paying with PayPal"); }
}

class PaymentContext {
    private strategy: Strategy;
    constructor(strategy: Strategy) { this.strategy = strategy; }
    setStrategy(strategy: Strategy) { this.strategy = strategy; }
    pay() { this.strategy.execute(); }
}

const context = new PaymentContext(new PaymentWithCreditCard());
context.pay(); // Paying with Credit Card
context.setStrategy(new PaymentWithPayPal());
context.pay(); // Paying with PayPal

Use Cases

  • Consistency: Ensuring a common vocabulary among developers.
  • Maintainability: Decoupling components to make them easier to change without affecting other parts of the system.
  • Reusability: Applying proven solutions to recurring problems.

Gotchas

  • Over-Engineering: Applying patterns where they aren’t needed (the “Golden Hammer” fallacy).
  • Complexity: Introducing additional classes and interfaces can make the code harder to follow if the pattern is overkill for the problem.

Related Notes