Design patterns are essential in software development because they provide proven solutions to common problems, making code easier to understand, reuse, and maintain. They help ensure that software is well-structured and robust, reducing the chances of errors and improving development efficiency.

for example when building something with lego we make sure everything fits together and works as intended. Same applies to design pattern in software development, they have tried and given proven solutions to the common problems that developers face when building software.

few of the reasons why we need them is:

  • Reusability: Just like you can use the same Lego block in many different creations, design patterns let you reuse solutions. This saves time because you don’t have to invent a new solution every time you face a similar problem.

  • Consistency: When you and your friends build with Lego using the same instructions, your creations look similar and work well together. In software, design patterns help developers create code that is consistent and easier to understand.

  • Efficiency: Imagine trying to build a Lego car without knowing the best way to make it sturdy and fast. You’d probably make a lot of mistakes and it would take a long time. Design patterns give developers a shortcut to efficient solutions, reducing the chance of mistakes and speeding up the development process.

  • Communication: If you and your friends know the same building techniques, you can talk about your creations more easily. Similarly, design patterns provide a common language for developers. When they talk about a pattern, they all understand the same solution, making collaboration smoother.

These design patterns are categories into main 3 bodies or into groups such as

  • Creational Pattern
  • Structural Pattern
  • Behavioral Pattern

Lets see few of the examples that are most commonly used software development.

Singleton Pattern

This pattern comes under creational pattern and that class has one instance and provide global point of access to that instance.

here is a famous example for single pattern in javascript

class Database {
  constructor(connection) {
    if (Database.instance) {
      return Database.instance;
    }
    this.connection = connection;
    Database.instance = this;
    return this;
  }
  getConnection() {
    return this.connection;
  }
}

// Usage
const db1 = new Database("MySQL_Connection");
console.log(db1.getConnection()); // Output: MySQL_Connection

const db2 = new Database("PostgreSQL_Connection");
console.log(db2.getConnection()); // Output: MySQL_Connection

console.log(db1 === db2); // Output: true

in the above example of a database connection, the Singleton pattern ensures that your application maintains a single connection to the database, reducing the overhead of creating multiple connections and ensuring that all parts of the application can access the same data consistently.

Factory Pattern

The Factory pattern is a creational design pattern that provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created.

As an example :

class Car {
  constructor(make, model, year) {
    this.make = make;
    this.model = model;
    this.year = year;
  }

  getDescription() {
    return `${this.year} ${this.make} ${this.model}`;
  }
}

class CarFactory {
  static createCar(type) {
    switch (type) {
      case 'sedan':
        return new Car('Toyota', 'Camry', 2023);
      case 'suv':
        return new Car('Honda', 'CR-V', 2023);
      case 'sports':
        return new Car('Porsche', '911', 2023);
      default:
        return null;
    }
  }
}

// Usage
const sedan = CarFactory.createCar('sedan');
console.log(sedan.getDescription()); // Output: 2023 Toyota Camry

const suv = CarFactory.createCar('suv');
console.log(suv.getDescription()); // Output: 2023 Honda CR-V

const sports = CarFactory.createCar('sports');
console.log(sports.getDescription()); // Output: 2023 Porsche 911

Few of the benefits of using this pattern:

  • Encapsulation
  • Flexibility
  • Reusability

Decorator Pattern

The Decorator pattern is a structural design pattern that allows you to dynamically add behavior and responsibilities to objects without modifying their code. It’s like adding decorations to a cake without changing the cake itself.

Imagine you have a basic coffee and you want to add different flavors or extras like milk, sugar, or whipped cream. You can use the Decorator pattern to add these extras dynamically.

Example:

// Basic coffee class
class Coffee {
  cost() {
    return 5;
  }

  description() {
    return 'Basic Coffee';
  }
}

// Decorator class
class CoffeeDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }

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

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

// Milk decorator
class MilkDecorator extends CoffeeDecorator {
  cost() {
    return this.coffee.cost() + 1;
  }

  description() {
    return this.coffee.description() + ', Milk';
  }
}

// Sugar decorator
class SugarDecorator extends CoffeeDecorator {
  cost() {
    return this.coffee.cost() + 0.5;
  }

  description() {
    return this.coffee.description() + ', Sugar';
  }
}

// Whipped Cream decorator
class WhippedCreamDecorator extends CoffeeDecorator {
  cost() {
    return this.coffee.cost() + 1.5;
  }

  description() {
    return this.coffee.description() + ', Whipped Cream';
  }
}

// Usage
let myCoffee = new Coffee();
console.log(myCoffee.description() + ' costs $' + myCoffee.cost()); // Output: Basic Coffee costs $5

myCoffee = new MilkDecorator(myCoffee);
console.log(myCoffee.description() + ' costs $' + myCoffee.cost()); // Output: Basic Coffee, Milk costs $6

myCoffee = new SugarDecorator(myCoffee);
console.log(myCoffee.description() + ' costs $' + myCoffee.cost()); // Output: Basic Coffee, Milk, Sugar costs $6.5

myCoffee = new WhippedCreamDecorator(myCoffee);
console.log(myCoffee.description() + ' costs $' + myCoffee.cost()); // Output: Basic Coffee, Milk, Sugar, Whipped Cream costs $8

And the benefits of using this pattern are:

  • Flexible: Allows behaviors to be added or removed dynamically.
  • Reusable: Decorators can be combined in different ways, making the code more reusable.
  • Open/Closed Principle: Enhances objects without modifying their code.

Strategy Pattern

The Strategy pattern is a behavioral design pattern that enables selecting an algorithm’s behavior at runtime. It defines a family of algorithms, encapsulates each one, and makes them interchangeable. The pattern allows the algorithm to vary independently from the clients that use it.

A common example of the Strategy pattern is in payment processing systems. Different payment methods (like credit card, PayPal, and bank transfer) can be implemented as different strategies. This allows you to easily switch between payment methods without changing the main processing code.

// Context: This is the class that uses a payment strategy
class PaymentProcessor {
  constructor(strategy) {
    this.strategy = strategy;
  }

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

  processPayment(amount) {
    return this.strategy.pay(amount);
  }
}

// Strategy interface
class PaymentStrategy {
  pay(amount) {
    throw new Error('Pay method must be implemented');
  }
}

// Concrete strategies
class CreditCardStrategy extends PaymentStrategy {
  constructor(cardNumber) {
    super();
    this.cardNumber = cardNumber;
  }

  pay(amount) {
    console.log(`Paying $${amount} using Credit Card ${this.cardNumber}`);
    // Implement credit card payment processing here
    return true;
  }
}

class PayPalStrategy extends PaymentStrategy {
  constructor(email) {
    super();
    this.email = email;
  }

  pay(amount) {
    console.log(`Paying $${amount} using PayPal account ${this.email}`);
    // Implement PayPal payment processing here
    return true;
  }
}

class BankTransferStrategy extends PaymentStrategy {
  constructor(accountNumber) {
    super();
    this.accountNumber = accountNumber;
  }

  pay(amount) {
    console.log(`Paying $${amount} using Bank Transfer to account ${this.accountNumber}`);
    // Implement bank transfer payment processing here
    return true;
  }
}

// Usage
const paymentProcessor = new PaymentProcessor(new CreditCardStrategy('1234-5678-9012-3456'));
paymentProcessor.processPayment(100); // Output: Paying $100 using Credit Card 1234-5678-9012-3456

paymentProcessor.setStrategy(new PayPalStrategy('user@example.com'));
paymentProcessor.processPayment(200); // Output: Paying $200 using PayPal account user@example.com

paymentProcessor.setStrategy(new BankTransferStrategy('987654321'));
paymentProcessor.processPayment(300); // Output: Paying $300 using Bank Transfer to account 987654321

And the benefits are :

  • Flexibility: Easily switch between different payment methods at runtime.
  • Open/Closed Principle: New payment methods can be added without modifying existing code.
  • Encapsulation: Encapsulates the payment algorithms, making the code more modular and easier to maintain.

I have touched only the most common pattern that being used in the software development.

Hope you enjoy the this article, see you in the next post.

Thanks