Mastering Design Patterns

banner

Table of Contents

    Share

    If you've been coding long enough, you've most definitely seen situations where your project begins to feel like a complex maze of if-else statements and switch cases. That typically indicates that you need more structure, which is precisely where design patterns come into play. Consider them as tried-and-true methods that enable you to develop software that is more manageable, scalable, and clean without having to start from scratch. 

    The fundamentals of design patterns, their significance, and some useful examples that you can use for your own projects are all explained in this article. 

    What are Design Patterns in Software Development?  

    Design patterns are fundamentally reusable fixes for typical issues that arise in software design. Consider them as templates or blueprints, not ready-to-copy code, but tried-and-true methods that you can modify to suit your particular requirements. They help you tackle issues like object creation, how classes interact, or structuring your code for better maintainability.  

    For example, a design pattern provides an organised approach to dealing with recurrent problems rather than hardcoding everything. They are not magic, but just smart ways to organize your code that have been battle-tested by developers over the years. 

    Why bother with Design Patterns? The Real Benefits.  

    Okay, so why should you care? Using design patterns can turn your code from a pile of chaos into something robust and flexible. 

    • Scalability and Maintainability: Growing your app and adding new features becomes easy and seamless without any disruptions. 
    • Clean, Reusable Code: With patterns it becomes easier to write testable, modular code that's simpler to debug and reuse across projects.  
    • Team Communication: They create a shared vocabulary. Say "Observer Pattern" to another developer, and they get you right away. It's more like speaking the same language. 
    • Best Practices Baked In: These are not random ideas, but rather drawn from industry standards to help you avoid some common pitfalls. 

    If you're building anything from a simple web app to a complex system, patterns like these can save you hours of refactoring down the line. 

     

    A Quick History of Design Patterns 

    Design patterns didn't just appear out of thin air. They trace back to architecture. In 1977, Christopher Alexander wrote about "patterns" in urban design, ideas that could be reused to solve common layout problems.  

    Skip to the 1980s and '90s, the concept was borrowed by the software folks. The major milestone was in 1994, when the "Gang of Four" – Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, published their book Design Patterns: Elements of Reusable Object-Oriented Software. They grouped patterns into three main categories: Creational, Structural, and Behavioral. This book essentially set the standard, and we're still using those ideas today - the widely referenced GoF design patterns. 


    Design patterns can be categorized into three main types:  

    1. Creational Patterns  

    Focusing on object creation mechanisms, these patterns provide ways to create objects in a way that is flexible, independent of their concrete implementations, and appropriate for the particular needs of the application. The Builder, Factory, and Singleton patterns are a few examples.  

    When you need to create objects without hardcoding types, like in applications with multiple user roles or configuration, they are an excellent option. 

     

    2. Structural Patterns  

    These patterns deal with how classes and objects are built to create larger structures while maintaining their efficiency and flexibility. They help ensure that classes and objects can work together effectively to achieve a common goal. Examples include the Adapter pattern, Decorator pattern, and Composite pattern.  

    They are ideal to use when dealing with class hierarchies or bridging incompatible interfaces. 

     

    3. Behavioral Patterns  

    These patterns deal with interactions and communication between objects and classes. They help provide solutions for effectively managing the flow of control, responsibilities, and behavior between objects. Observer pattern, Strategy pattern, and Command pattern are some of the examples.   

    They are perfect for scenarios like event handling or algorithm swapping, where objects need to interact dynamically. 

    Command Pattern: 

    Let's kick off with the Command Pattern, a behavioral one that's super handy for decoupling senders and receivers of requests. Consider building a smart home app to control lights in a variety of settings: on, off, dim, brighten, color changes, party mode and so on. Without a pattern, one would end up with in chaotic maze of if-else or switch statements that would grow complex as you add features. 

     

    The Command Pattern treats each action as an object (a "command") that can be executed, undone, logged, or queued. It involves: 

    • Receiver: The thing doing the work (e.g., the Light class). 
    • Command Interface: Defines execute() and undo(). 
    • Concrete Commands: Specific actions like TurnOnCommand. 
    • Invoker: Triggers the command (e.g., a button). 
    • Client/UI: Sets it all up. 

     

    Just Consider this example: 

    // Receiver: Handles the actual logic 

    export class Light { 

      turnOn() { console.log("Light is ON"); } 

      turnOff() { console.log("Light is OFF"); } 

      dim() { console.log("Light is DIMMED"); } 

      brighten() { console.log("Light is BRIGHTENED"); } 

      changeColor(color: string) { console.log(`Light color changed to ${color}`); } 

      partyMode() { console.log("Party Mode ON"); } 

    export interface Command { 

      execute(): void; 

      undo?(): void; 

    class TurnOnCommand implements Command { 

      constructor(private light: Light) {} 

      execute() { 

        this.light.turnOn(); 

        console.log("TurnOn executed"); 

      } 

      undo() { 

        this.light.turnOff(); 

        console.log("TurnOn undone"); 

      } 

    // Invoker: Like a button component 

    export const ButtonInvoker = ({ label, command, onExecute }) => { 

      const handleClick = () => { 

        command.execute(); 

        onExecute?.(command);  // Add to history/queue 

      }; 

      return <button onClick={handleClick}>{label}</button>; 

    }; 

    // Client/UI Setup 

    const light = new Light(); 

    const turnOn = new TurnOnCommand(light);  // Create command 

    turnOn.execute();  // Run it 

    // For Undo/Queue 

    const history: Command[] = []; 

    history.push(turnOn); 

    const last = history.pop(); 

    last?.undo?.(); 

    See how this decouples things? Adding a new command (like FlashCommand) is just a new class without touching the existing if-else mess. Benefits: Easy undo/redo, logging, queuing. I've used this in remote control apps, and it keeps the code sane. 

    Observer Pattern: 

    The Observer Pattern is another behavioural gem that establishes a one-to-many relationship where a "subject" notifies "observers" of changes. It resembles a YouTube channel where you upload a video (subject changes), and subscribers (observers) get notified automatically. 

     

    Key parts: 

    • Subject: Tracks observers and broadcasts updates. 
    • Observers: React to notifications. 

     

    In React, this is baked into state management, consider hooks or Redux. 

     
    For example: 

    // Subject (e.g., YouTube Channel) 

    class Subject { 

      constructor() { 

        this.observers = []; 

      } 

      addObserver(observer) { 

        this.observers.push(observer); 

      } 

      removeObserver(observer) { 

        this.observers = this.observers.filter(obs => obs !== observer); 

      } 

      notifyObservers(message) { 

        this.observers.forEach(obs => obs.update(message)); 

      } 

      uploadVideo(title) { 

        console.log(`New video: ${title}`); 

        this.notifyObservers(`New video uploaded: ${title}`); 

      } 

    }  

    // Observer (Subscriber) 

    class Observer { 

      constructor(name) { 

        this.name = name; 

      } 

      update(message) { 

        console.log(`${this.name} received: ${message}`); 

      } 

    }  

    // Usage 

    const channel = new Subject(); 

    const sub1 = new Observer("Alice"); 

    const sub2 = new Observer("Bob"); 

    channel.addObserver(sub1); 

    channel.addObserver(sub2); 

    channel.uploadVideo("Design Patterns 101"); 

     

    Advantages: Real-time updates without tight coupling, modular code, reusable in apps like news feeds or chat systems. New articles in a news app automatically alert subscribed component. Learning this helped me clean up event-driven code in my projects.  

    Strategy Pattern: 

    Strategy Pattern, also behavioural, lets you define a family of algorithms, encapsulate them, and swap them at runtime. It's ideal for eliminating long if-else chains, like choosing payment methods in an e-commerce app. 

     

    Structure: 

    • Context: Uses the strategy. 
    • Strategy Interface: Common method (e.g., pay()). 
    • Concrete Strategies: Different implementations. 

     

    Code sample (JavaScript): 

    // Strategy Interface (implied by method) 

    class CreditCardPayment { 

      pay(amount) { 

        console.log(`💳 Paid ₹${amount} using Credit Card.`); 

      } 

    }  

    class PayPalPayment { 

      pay(amount) { 

        console.log(`💰 Paid ₹${amount} using PayPal.`); 

      } 

    }  

    class UpiPayment { 

      pay(amount) { 

        console.log(`📱 Paid ₹${amount} using UPI.`); 

      } 

    // Context 

    class PaymentProcessor { 

      setStrategy(paymentMethod) { 

        this.paymentMethod = paymentMethod; 

      } 

      makePayment(amount) { 

        this.paymentMethod.pay(amount); 

      } 

    // Client 

    const payment = new PaymentProcessor(); 

    payment.setStrategy(new PayPalPayment()); 

    payment.makePayment(1500); 

     

    • Real-world uses: Sorting algorithms, authentication (Google vs. email), game AI behaviours.  
    • Pros: Flexible, follows Open/Closed Principle (add strategies without changing core code).  
    • Cons: More classes, but worth it for maintainability. I've swapped navigation strategies in apps based on user preferences. 

    Factory Method Pattern: 

    Moving on to creational patterns, the Factory Method allows you to generate objects without explicitly defining classes by deferring object instantiation to subclasses. It's great when you have a family of related objects, like vehicles in a store app. 

     

    Consider this: 

    from abc import ABC, abstractmethod 

     

    # Product Interface 

    class Vehicle(ABC): 

        @abstractmethod 

        def drive(self): 

            pass 

     

    # Concrete Products 

    class Car(Vehicle): 

        def drive(self): 

            print("Driving a Car") 

     

    class Bike(Vehicle): 

        def drive(self): 

            print("Riding a Bike") 

     

    # Factory Interface 

    class VehicleFactory(ABC): 

        @abstractmethod 

        def create_vehicle(self): 

            pass 

     

        def deliver_vehicle(self): 

            vehicle = self.create_vehicle() 

            print("Vehicle ready for delivery") 

            vehicle.drive() 

     

    # Concrete Factories 

    class CarFactory(VehicleFactory): 

        def create_vehicle(self): 

            return Car() 

     

    class BikeFactory(VehicleFactory): 

        def create_vehicle(self): 

            return Bike() 

     

    # Client 

    def client(factory: VehicleFactory): 

        factory.deliver_vehicle() 

     

    client(CarFactory()) 

    client(BikeFactory()) 

     

    To add a Truck? Just a new Truck class and TruckFactory; no changes elsewhere.  

     

    • Benefits: Extensible, promotes abstraction.  
    • Real uses: Database connectors, payment processors. This pattern has saved me when scaling apps with multiple configs. 

    Conclusion 

    To sum up, design patterns are essential tools for developers because they provide tried-and-true fixes for common design issues and encourage code scalability, maintainability, and reuse. Developers can create software solutions that are reliable, adaptable, and easy to maintain by understanding and implementing these patterns effectively. They improve the flexibility and adaptability of your software, from the decoupling magic of Command to the creation flexibility of Factory Method. If you're just starting, pick one, like Observer for event stuff, and implement it in a side project. You'll see the difference. 

     

    At ThoughtMinds, we quietly lean on these patterns every day, using reusable code solutions and proven architectural approaches, whether we're building automation workflows or fine-tuning AI systems - because clean, scalable foundations make everything else move faster. 

    Talk to Our Experts