S

Understanding Design Patterns: The Key to Writing Reusable Code

PinoyFreeCoder
Mon Dec 16 2024

Understanding Design Patterns: The Key to Writing Reusable Code

Design patterns provide reusable solutions to common software design problems. By mastering patterns like Singleton, Factory, Observer, and Strategy, developers can create scalable, maintainable, and efficient applications. This guide explains these patterns with real-world scenarios and code examples.

1. Singleton Pattern

The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. It’s commonly used in logging, configuration management, and database connections.


      class Singleton {
          constructor() {
              if (Singleton.instance) {
                  return Singleton.instance;
              }
              Singleton.instance = this;
          }
      }
  
      const instance1 = new Singleton();
      const instance2 = new Singleton();
      console.log(instance1 === instance2); // true
      

Real-World Use Case: A logging service that ensures all parts of an application write to the same log file:


      class Logger {
          constructor() {
              if (Logger.instance) {
                  return Logger.instance;
              }
              this.logs = [];
              Logger.instance = this;
          }
  
          log(message) {
              this.logs.push(message);
              console.log(`LOG: ${message}`);
          }
      }
  
      const logger1 = new Logger();
      const logger2 = new Logger();
      logger1.log("First log");
      logger2.log("Second log");
      console.log(logger1.logs); // ['First log', 'Second log']
      

2. Factory Pattern

The Factory pattern creates objects without specifying the exact class to instantiate. It’s useful when the type of object required is determined at runtime.


      class Car {
          constructor(type) {
              this.type = type;
          }
      }
  
      class CarFactory {
          static createCar(type) {
              return new Car(type);
          }
      }
  
      const sedan = CarFactory.createCar("Sedan");
      const suv = CarFactory.createCar("SUV");
      console.log(sedan.type); // Sedan
      console.log(suv.type); // SUV
      

Real-World Use Case: A notification system that creates different types of notifications based on user preferences:


      class Notification {
          constructor(message) {
              this.message = message;
          }
  
          send() {
              console.log("Sending notification: " + this.message);
          }
      }
  
      class EmailNotification extends Notification {
          send() {
              console.log("Sending email: " + this.message);
          }
      }
  
      class SMSNotification extends Notification {
          send() {
              console.log("Sending SMS: " + this.message);
          }
      }
  
      class NotificationFactory {
          static createNotification(type, message) {
              switch (type) {
                  case "email":
                      return new EmailNotification(message);
                  case "sms":
                      return new SMSNotification(message);
                  default:
                      return new Notification(message);
              }
          }
      }
  
      const email = NotificationFactory.createNotification("email", "Hello via Email!");
      email.send(); // Sending email: Hello via Email!
      

3. Observer Pattern

The Observer pattern establishes a one-to-many relationship where changes in one object (the subject) notify and update dependent objects (observers). It’s widely used in event-driven systems.


      class Subject {
          constructor() {
              this.observers = [];
          }
  
          attach(observer) {
              this.observers.push(observer);
          }
  
          detach(observer) {
              this.observers = this.observers.filter(obs => obs !== observer);
          }
  
          notify() {
              this.observers.forEach(observer => observer.update());
          }
      }
  
      class Observer {
          update() {
              console.log("Observer updated!");
          }
      }
  
      const subject = new Subject();
      const observer1 = new Observer();
      const observer2 = new Observer();
  
      subject.attach(observer1);
      subject.attach(observer2);
      subject.notify(); // Observer updated! (twice)
      

Real-World Use Case: A stock market tracker where investors (observers) receive updates when a stock price changes:


      class Stock {
          constructor(price) {
              this.price = price;
              this.investors = [];
          }
  
          addInvestor(investor) {
              this.investors.push(investor);
          }
  
          updatePrice(newPrice) {
              this.price = newPrice;
              this.notifyInvestors();
          }
  
          notifyInvestors() {
              this.investors.forEach(investor => investor.notify(this.price));
          }
      }
  
      class Investor {
          constructor(name) {
              this.name = name;
          }
  
          notify(price) {
              console.log(`${this.name} notified of stock price: ${price}`);
          }
      }
  
      const stock = new Stock(100);
      const investor1 = new Investor("Alice");
      const investor2 = new Investor("Bob");
  
      stock.addInvestor(investor1);
      stock.addInvestor(investor2);
      stock.updatePrice(120); // Alice and Bob get notified
      

4. Strategy Pattern

The Strategy pattern allows you to define a family of algorithms and make them interchangeable without modifying the client code.


      class PaymentStrategy {
          processPayment(amount) {
              throw new Error("This method should be overridden!");
          }
      }
  
      class CreditCardPayment extends PaymentStrategy {
          processPayment(amount) {
              console.log(`Paid ${amount} using Credit Card.`);
          }
      }
  
      class PayPalPayment extends PaymentStrategy {
          processPayment(amount) {
              console.log(`Paid ${amount} using PayPal.`);
          }
      }
  
      class PaymentProcessor {
          setStrategy(strategy) {
              this.strategy = strategy;
          }
  
          process(amount) {
              this.strategy.processPayment(amount);
          }
      }
  
      const paymentProcessor = new PaymentProcessor();
      paymentProcessor.setStrategy(new CreditCardPayment());
      paymentProcessor.process(100); // Paid 100 using Credit Card.
      paymentProcessor.setStrategy(new PayPalPayment());
      paymentProcessor.process(200); // Paid 200 using PayPal.
      

Conclusion

Mastering design patterns like Singleton, Factory, Observer, and Strategy helps developers build robust and maintainable software. By understanding their use cases and applying them effectively, you can solve common coding challenges with ease. Which pattern is your favorite? Share your thoughts below!

Start Your Online Store with Shopify

Build your e-commerce business with the world's leading platform. Get started today and join millions of successful online stores.

🎉 3 MONTHS FREE for New Users! 🎉
Get Started
shopify