learning

Phase 11: Design Patterns

1108 words6 min read
Phase 11: Design Patterns
Authors

Demystifying Design Patterns for Absolute Beginners

Have you ever encountered a problem while coding and thought, "Someone must have solved this exact problem before"?

You are right. They have.

In the mid-1990s, a group of four authors (affectionately known as the "Gang of Four") published a book cataloging common software problems and their proven solutions. These solutions are called Design Patterns.

A design pattern is not a piece of code you can copy and paste. It is a blueprint or a concept. It’s like a recipe for a cake: the recipe tells you what steps to take, but you still have to bake it yourself, and you can tweak the ingredients to suit your needs.

Design patterns are generally divided into three main categories: Creational, Structural, and Behavioral. Let's explore the most essential ones with real-world analogies and TypeScript code.


1. Creational Patterns

These patterns provide elegant ways to create objects. Instead of just using the new keyword everywhere (which can tightly couple your code), Creational patterns give you flexibility.

The Singleton Pattern

The Analogy: The President of a Country. A country can only have one active President at a time. No matter who asks to speak to the President, they are all directed to the exact same person.

The Concept: Ensure a class has only one instance, and provide a global point of access to it.

The Use Case: Database connections. You don't want to open a new connection to your database every time a user requests data; you want to reuse a single, shared connection.

class Database {
  private static instance: Database;

  // The constructor is private! You cannot use `new Database()` from outside.
  private constructor() {
    console.log("Connecting to the database...");
  }

  // This is the only way to get the object
  public static getInstance(): Database {
    if (!Database.instance) {
      Database.instance = new Database();
    }
    return Database.instance;
  }

  public query(sql: string) {
    console.log(`Executing: ${sql}`);
  }
}

// Usage
const db1 = Database.getInstance(); // "Connecting to the database..."
const db2 = Database.getInstance(); // Does not connect again!

console.log(db1 === db2); // true. They are the exact same object.

The Factory Pattern

The Analogy: A Car Manufacturing Plant. When you order a car from a dealership, you don't build the engine and weld the doors yourself. You simply say, "I want a Sedan," and the factory builds it and hands it to you.

The Concept: Create a special class (the Factory) whose only job is to construct other objects based on the input you give it.

// The Products
interface Vehicle { drive(): void; }
class Car implements Vehicle { drive() { console.log("Driving a car"); } }
class Truck implements Vehicle { drive() { console.log("Driving a truck"); } }

// The Factory
class VehicleFactory {
  static createVehicle(type: string): Vehicle {
    if (type === "car") return new Car();
    if (type === "truck") return new Truck();
    throw new Error("Unknown vehicle type");
  }
}

// Usage
const myRide = VehicleFactory.createVehicle("car");
myRide.drive(); // "Driving a car"

2. Structural Patterns

These patterns deal with how objects are composed to form larger, more complex structures, while keeping those structures flexible.

The Adapter Pattern

The Analogy: A Travel Power Adapter. You live in the US and travel to the UK. Your laptop charger has two flat prongs, but the wall socket in the UK has three rectangular holes. You can't change your laptop, and you can't change the wall. So, you use a physical Adapter that acts as a middleman.

The Concept: Allows classes with incompatible interfaces to work together.

The Use Case: You are using an old XML library, but your new system only understands JSON. You write an Adapter that takes the XML, converts it to JSON, and passes it along.

The Facade Pattern

The Analogy: Ordering at a Restaurant. When you order a burger, you just tell the waiter, "I want a burger." You don't go to the kitchen, turn on the grill, chop the onions, toast the buns, and assemble it. The waiter provides a simple "Facade" that hides the massive complexity of the kitchen.

The Concept: Provide a simple, top-level interface to a complex subsystem containing many moving parts.

// The Complex Subsystems
class Amplifier { turnOn() { console.log("Amp on"); } }
class DVDPlayer { play() { console.log("DVD playing"); } }
class Projector { turnOn() { console.log("Projector on"); } }

// The Facade
class HomeTheaterFacade {
  constructor(
    private amp: Amplifier,
    private dvd: DVDPlayer,
    private projector: Projector
  ) {}

  watchMovie() {
    console.log("Get ready for a movie...");
    this.amp.turnOn();
    this.projector.turnOn();
    this.dvd.play();
  }
}

// Usage
const theater = new HomeTheaterFacade(new Amplifier(), new DVDPlayer(), new Projector());
theater.watchMovie(); // One simple command triggers all the complex internal logic!

3. Behavioral Patterns

These patterns handle the communication and assignment of responsibilities between objects.

The Strategy Pattern

The Analogy: Google Maps Navigation. When you want to go to the grocery store, Google Maps lets you choose a Strategy: Walking, Biking, or Driving. The destination is the same, but the algorithm used to get you there changes based on your choice.

The Concept: Define a family of algorithms, encapsulate each one into a class, and make them interchangeable at runtime.

The Use Case: A checkout system that can dynamically switch between calculating taxes for the US, the UK, or Canada without using massive if/else blocks.

// The Strategies
interface CheckoutStrategy { pay(amount: number): void; }

class CreditCardStrategy implements CheckoutStrategy {
  pay(amount: number) { console.log(`Paid $${amount} via Credit Card`); }
}
class PayPalStrategy implements CheckoutStrategy {
  pay(amount: number) { console.log(`Paid $${amount} via PayPal`); }
}

// The Context (The Shopping Cart)
class ShoppingCart {
  private strategy: CheckoutStrategy;

  // We can inject whatever strategy we want!
  constructor(strategy: CheckoutStrategy) {
    this.strategy = strategy;
  }

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

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

// Usage
const cart = new ShoppingCart(new CreditCardStrategy());
cart.checkout(100); // Paid $100 via Credit Card

cart.setStrategy(new PayPalStrategy()); // Dynamically swap behavior!
cart.checkout(50); // Paid $50 via PayPal

The Observer Pattern

The Analogy: YouTube Subscriptions. You subscribe to your favorite YouTuber. When they upload a new video, they don't manually call your phone to tell you. Instead, YouTube's system automatically broadcasts a notification to all subscribers simultaneously.

The Concept: Define a one-to-many dependency so that when one object changes state, all its dependents are notified automatically.

The Use Case: React state management (like Redux), or DOM Event Listeners (e.g., button.addEventListener('click', onClick)).


Summary

Design patterns are the vocabulary of senior engineers. By learning them, you stop reinventing the wheel and start applying robust, proven solutions to the architectural challenges you face every day.

Tags

#design-patterns#oop#architecture