Design Patterns Handbook - Part I

Tuan Tran VanTuan Tran Van
19 min read

Hi everyone! In this article, I will explain what design patterns are and why they are useful.

We will also go through some of the most popular design patterns out there and give examples for each of them. Let's go.

What are the Design Patterns?

Each pattern describes a problem which occurs over and over again in our environment, and then describes the core of the solution to that problem, in a way that you this solution a million times over, without ever doing this the same way twice.

--Christopher Alexander

Patterns provide a framework that can be applied to similar issues but may be solved in different ways, sometimes in the real world and other times in digital space.

Design patterns are reusable solutions to commonly occurring problems within a given context in software design. It's not a finished design that can be transformed directly into the app. Rather, It's a template to solve a recurring design problem in code.

Design Patterns are often confused with algorithms because both concepts describe typical solutions to some known problems. While an algorithm always defines a clear set of actions that can achieve some goal, a pattern is a more high-level description of a solution. The code of the same pattern applied to two different programs may be different.

Design Patterns are also often confused with Architecture Patterns. The architecture of an application refers to the larger structure of the application while a design pattern refers to a method of solving a specific type of problem, much more focused and lower level than the global structure of the system. Architecture focuses on the abstract view of the idea while design focuses on the implementation view of the idea.

The pattern is not a specific piece of code, but a concept for solving a particular problem.

Importance of Design Patterns

Design patterns play a pivotal role in software development, providing standardized solutions that enhance communications among developers and establish shared languages for discussing design principles. They act as a common vocabulary, facilitating seamless collaboration and enabling developers to effectively complex design ideas.

Design patterns offer several benefits for software development:

  • Reusability: Design patterns provide reusable solutions to common problems, reducing the need to reinvent the wheel and promoting code reuse.

  • Flexibility: Design patterns are designed to be adaptable, allowing them to be tailored to specific requirements or contexts.

  • Communication: Design patterns established a shared language among developers, improving communication and collaboration.

  • Maintainability: Design patterns can lead to more maintainable code, making it easier to understand, modify, and extend over time.

When to avoid the use of Design Patterns?

There are situations where it's best to avoid or be cautious about the use of design patterns. Here are some cases when you have to avoid the use of design patterns:

  • Over-Engineering: Introducing design patterns in simple scenarios can lead to unnecessary complexity. It makes the code harder to understand and maintain, especially for new developers.

  • Premature Optimization: Optimizing code prematurely by applying patterns that are meant to solve performance issues can be counterproductive. It's better to profile and understand the issues first before applying specific patterns to address them.

  • Lack of understanding: Misapplying design patterns can introduce bugs and make the code harder to maintain. It's important to thoroughly understand the pattern and its consequences before integrating it into your codebase.

  • Inappropriate context: Each design pattern is suited for specific types of problems. Using a pattern outside of its intended context can lead to awkward and inefficient solutions.

  • Decreased Flexibility: Some patterns introduce structures that can be rigid and hard to change later. It's essential to balance the benefits of the pattern with the need for future flexibility.

  • Readability and Maintainability Concerns: Design patterns can introduce additional layers of abstraction that might obscure the basic functionality of the code. This can make it harder for new developers to get up to speed and for existing developers to maintain the code.

  • Performance Overheads: Some design patterns, like the Proxy or Observer patterns, can introduce additional layers of indirection or excessive event notifications, which might impact performance.

Type of Design Patterns

There are about 23 patterns currently discovered(I hardly think that I will do them all...)

These 23 types can be classified into 3 types:

  • Creational: These patterns are designed for class instantiation. They can be either class-creation patterns or object-creational patterns.

  • Structural: These patterns are designed with regard to a class's structure and composition. The main goal of most of these patterns is to increase the functionality of the classes involved, without changing much of its composition.

  • Behavioral: These patterns are designed depending on how one class communicates with others.

Design Patterns Relationships:

Creational Design Patterns

Creational design patterns abstract the instantiation process. They help make the system independent of how its objects are created, composed, and represented. A class creational pattern uses inheritance to vary the instantiated class, whereas an object creation pattern will delegate instantiation to another object.

Creational patterns give a lot of flexibility in what gets created, who creates it, how it gets created, and when.

These are two recurring themes in these patterns:

  • They all encapsulate knowledge about what concrete class the systems use.

  • They hide how instances of these classes are created and put together.

The Singleton Design Pattern

The singleton pattern only allows a class or object to have a single instance and it uses a global variable to store that instance. You can use lazy loading to make sure that there is only one instance of the class because it will only create the class when you need it.

That prevents multiple instances from being active at the same time which could cause weird bugs. Most of the time this gets implemented in the constructor. The goal of a singleton pattern is typically to regulate the global state of application.

An example of a singleton that you probably use all the time is your logger.

If you work with some of the front-end frameworks like React or Angular, you know all about how tricky it can be to handle logs coming from multiple components. This is a great example of singletons in action because you never want more than one instance of a logger object, especially if you are using some kind of error-tracking tool.

class FoodLogger {
  constructor() {
    this.foodLog = []
  }

  log(order) {
    this.foodLog.push(order.foodItem)
    // do fancy code to send this log somewhere
  }
}

// this is the singleton
class FoodLoggerSingleton {
  constructor() {
    if (!FoodLoggerSingleton.instance) {
      FoodLoggerSingleton.instance = new FoodLogger()
    }
  }

  getFoodLoggerInstance() {
    return FoodLoggerSingleton.instance
  }
}

module.exports = FoodLoggerSingleton

Now you don't have to worry about losing logs from multiple instances because you only have one in your project. So when you want to log the food that has been ordered, you could use the same FoodLogger instance across multiple files or components.

// An example of a Customer class using the singleton

const FoodLogger = require('./FoodLogger')

const foodLogger = new FoodLogger().getFoodLoggerInstance()

class Customer {
  constructor(order) {
    this.price = order.price
    this.food = order.foodItem
    foodLogger.log(order)
  }

  // other cool stuff happening for the customer
}

module.exports = Customer
//An example of the Restaurant class using the same singleton as the Customer class

const FoodLogger = require('./FoodLogger')

const foodLogger = new FoodLogger().getFoodLoggerInstance()

class Restaurant {
  constructor(inventory) {
    this.quantity = inventory.count
    this.food = inventory.foodItem
    foodLogger.log(inventory)
  }

  // other cool stuff happening at the restaurant
}

module.exports = Restaurant

With this singleton pattern in place, you don't have to worry about just getting logs from the main application file. You can get them anywhere in your code base and they will all go to the exact same instance of the logger, which means none of your logs should get lost due to new instances.

The Factory Design Pattern

Factory pattern is one of the most common creation patterns. It loosely connects the application by hiding the implementation details from the client code using the interface. It leaves the creation of object instances to the factory implementation.

We will be using a typical example of a factory that creates different types of users: Manager and Developer, for the sake of simplicity. You can imagine there will be many more types in a real project.

Firstly, Let's define our Manager and Developer classes:

In the above code, we define an IStaff interface, so that both Manager and Developer classes implement it. It's essential for the Factory pattern to work, so the client doesn't need to know a particular class, only the interface.

Another notable thing is the use of NullStaff class(Null Object Pattern). It simplifies the client code and avoids run time bugs when an invalid user type is passed in.

Finally, we have the UserFactory implemented below and the UserService as a client that consumes the factory. The UserService doesn't know the Manager or Developer class. It just passes in a userType and gets a concrete instance of the user class back.

The Abstract Factory Pattern

The abstract factory pattern is a creational pattern that provides an interface for creating families of related or dependent objects without specifying their concrete classes.

This pattern comes under the Gang of Four (GoF) design patterns and is particularly useful when the system must be independent of how its objects are created, composed, and represented, and the system is configured with multiple families of objects.

// Client Code
const payPalFactory = new PayPalGatewayFactory();
const payPalProcessor = payPalFactory.createPaymentProcessor();
const payPalInvoiceGenerator = payPalFactory.createInvoiceGenerator();

payPalProcessor.processPayment();
payPalInvoiceGenerator.generateInvoice();

const stripeFactory = new StripeGatewayFactory();
const stripeProcessor = stripeFactory.createPaymentProcessor();
const stripeInvoiceGenerator = stripeFactory.createInvoiceGenerator();

stripeProcessor.processPayment();
stripeInvoiceGenerator.generateInvoice();

The Abstract Factory pattern involves defining interfaces for creating families of related or dependent objects, and concrete classes that implement these interfaces to create specific objects within those families.

Keys Concept of the Abstract Factory Pattern

  • Abstract Factory Interface: This is the core interface that declares the creation methods for various abstract products. It defines a set of methods, each responsible for creating a different product.

      // Abstract Factory Interface for Payment Gateways
      class PaymentGatewayFactory {
          createPaymentProcessor() {}
          createInvoiceGenerator() {}
      }
    
  • Concrete Factories: These are the concrete implementations of the abstract factory interface. Each concrete is responsible for creating a family of products.

      // Concrete Factory for PayPal
      class PayPalGatewayFactory extends PaymentGatewayFactory {
          createPaymentProcessor() {
              return new PayPalPaymentProcessor();
          }
    
          createInvoiceGenerator() {
              return new PayPalInvoiceGenerator();
          }
      }
    
      // Concrete Factory for Stripe
      class StripeGatewayFactory extends PaymentGatewayFactory {
          createPaymentProcessor() {
              return new StripePaymentProcessor();
          }
    
          createInvoiceGenerator() {
              return new StripeInvoiceGenerator();
          }
      }
    
  • Abstract products: There are abstract product interfaces. They declare interfaces for a set of distinct but related products.

      // Abstract Product Interface for Payment Processor
      class PaymentProcessor {
          processPayment() {}
      }
    
      // Abstract Product Interface for Invoice Generator
      class InvoiceGenerator {
          generateInvoice() {}
      }
    
  • Concrete products: These are the concrete implementations of the abstract product interfaces.

      // Concrete Product for PayPal Payment Processor
      class PayPalPaymentProcessor extends PaymentProcessor {
          processPayment() {
              console.log("Processing payment with PayPal...");
          }
      }
    
      // Concrete Product for PayPal Invoice Generator
      class PayPalInvoiceGenerator extends InvoiceGenerator {
          generateInvoice() {
              console.log("Generating invoice for PayPal...");
          }
      }
    
      // Concrete Product for Stripe Payment Processor
      class StripePaymentProcessor extends PaymentProcessor {
          processPayment() {
              console.log("Processing payment with Stripe...");
          }
      }
    
      // Concrete Product for Stripe Invoice Generator
      class StripeInvoiceGenerator extends InvoiceGenerator {
          generateInvoice() {
              console.log("Generating invoice for Stripe...");
          }
      }
    

Advantages of the Abstract Factory Pattern

There are 3 main advantages of using this pattern:

  • It ensures the creation of objects that are compatible and consistent. By using abstract factories, you guarantee that the created products are designed to be together, promoting system integrity and reducing the risk of competitive components.

  • It promotes flexibility and extensibility in a system. As requirements involve or new features need to be added, you can introduce new concrete factories that implement the abstract factory interface. This allows for the seamless addition of new families of related products without modifying existing client code.

  • it isolates the client code from the details of concrete classes. Clients interact with the abstract interfaces provided by the abstract factory, which shields them from the specifics of how the objects are created.

The difference between the Factory Pattern and the Abstract Factory Pattern

The Factory Pattern and Abstract Factory Pattern are both creational design patterns used to encapsulate object creation. However, they differ in complexity and use cases.

  • Scope and Complexity:

    • Factory Pattern: Focuses on creating a single object. It’s simpler and used when the creation of an object involves some logic that’s better centralized.

    • Abstract Factory Pattern: Focuses on creating families of related or dependent objects. It’s more complex and used when there are multiple types of objects to be created that are designed to work together.

  • Class Structure:

    • Factory Pattern: Typically involves one factory class with a method to create objects.

    • Abstract Factory Pattern: Involves multiple factory classes (factories of factories), each responsible for creating different types of related objects.

  • Use Cases:

    • Factory Pattern: Useful when the exact type of the object to be created is not known until runtime, or when there is complex logic involved in object creation.

    • Abstract Factory Pattern: Useful when a system needs to be independent of how its objects are created, composed, and represented, and when a family of related objects is designed to be used together.

In summary, the Factory Pattern is simpler. It focuses on the creation of single objects with some logic centralized in a factory method. In contrast, the Abstract Factory Pattern is more complex and is used to create families of related objects with multiple objects.

The Prototype Design Pattern

The prototype pattern is a creational design pattern that involves creating new objects by copying an existing object, known as the prototype. Instead of creating new instances through a constructor, objects are cloned from an existing object.

// Prototype object
const carPrototype = {
  brand: 'Generic',
  model: 'Car',
  start: function () {
    console.log(`Starting the ${this.brand} ${this.model}`);
  }
};

// Clone object
const myCar = Object.create(carPrototype);
myCar.brand = 'Toyota';
myCar.model = 'Camry';

// Customized object
const customCar = Object.create(carPrototype);
customCar.brand = 'Tesla';
customCar.model = 'Model S';
customCar.autopilot = true;

// Usage
myCar.start(); // Output: Starting the Toyota Camry
customCar.start(); // Output: Starting the Tesla Model S

As we already know all child objects that are created from a Class, will inherit all the properties of the class that are under the object prototype. After inheriting, all the newly created objects will have all properties from the Class prototype to their __proto __ property. See details here.

So the value of __proto__ on any child objects of the Class (Constructor function), is a direct reference to the Class prototype. Whenever we try to access a property on an object that doesn't exist on an object directly, Javascript will go down to the prototype chain to see if the property is available within the prototype chain.

Key Concepts

There are 2 key components for the prototype pattern:

  • Prototype: This is the object that serves as the template for creating new objects. It's the object to be cloned.

  • Clone: the new created by copying the prototype.

// Prototype object
const carPrototype = {
  wheels: 4,
  start: function () {
    return "Engine started";
  },
  stop: function () {
    return "Engine stopped";
  },
};

// Car constructor function
function Car(make, model) {
  this.make = make;
  this.model = model;
}

// Use Object.create to clone the prototype
Car.prototype = Object.create(carPrototype);

// Create instances using the prototype
const car1 = new Car("Toyota", "Camry");
const car2 = new Car("Honda", "Accord");

// Test the instances
console.log(car1.wheels); // Output: 4
console.log(car2.start()); // Output: Engine started

// Modify a property for a specific instance
car1.wheels = 3;
console.log(car1.wheels); // Output: 3
console.log(car2.wheels); // Output: 4 (unchanged)

// Demonstrate encapsulation
console.log(car1.hasOwnProperty("wheels")); // Output: true
console.log(car1.hasOwnProperty("start")); // Output: false (inherited from prototype)

Advantages of the prototype pattern

I think there are 3 advantages of using the prototype pattern:

  • Flexibility: It provides a flexible way to create new objects, allowing for dynamic changes during runtime.

  • Performance: Object cloning can be more efficient than repeatedly invoking constructors, especially when dealing with complex object hierarchies.

  • Encapsulation: It encapsulates the details of object creation within the prototype, reducing dependencies on concrete implementations.

The biggest benefit of using this pattern in JavaScript is the performance boost gained compared to object-oriented classes. This means that when you define functions inside an object, they will be created by reference. In other words, all child objects will point to the same method instead of creating their own individual copies.

const Warrior = function(name) {
  this.name = name
  this.hp = 100
}

Warrior.prototype.bash = function(target) {
  target.hp -= 15
}

Warrior.prototype.omniSlash = function(target) {
  // The target's hp may not be under 50 or this attack will fail on the opponent
  if (target.hp < 50) {
    return
  }
  target.hp -= 50
}

const sam = new Warrior('Sam')
const lenardo = new Warrior('Lenardo')

console.log(sam.bash === lenardo.bash) // true

If we had instead defined them like this, then they are not the same. Essentially, Javascript has created another copy of (supposedly) the same method for each instance.

const Warrior = function(name) {
  this.name = name
  this.hp = 100

  this.bash = function(target) {
    target.hp -= 15
  }

  this.omniSlash = function(target) {
    // The target's hp may not be under 50 or this attack will fail on the opponent
    if (target.hp < 50) {
      return
    }
    target.hp -= 50
  }
}

const sam = new Warrior('Sam')
const lenardo = new Warrior('Lenardo')

console.log(sam.bash === lenardo.bash) // false

So if we don't use the prototype pattern as in the last example, how crazy would it be when we instantiate many instances? We would have cloned methods cluttering up memory that essentially do the same exact things, which don't even need to be copied unless they rely on state inside instances!

Common Mistakes

There are a few points that we need to pay attention to when working with prototype patterns.

Firstly, we need to understand the requirements and implications of shallow and deep cloning, as the Prototype Pattern allows for both.

// Prototype object
const carPrototype = {
  wheels: 4,
  start: function () {
    return "Engine started";
  },
  stop: function () {
    return "Engine stopped";
  },
};

// Car constructor function
function Car(make, model) {
  this.make = make;
  this.model = model;
}

// Use Object.create to clone the prototype
Car.prototype = Object.create(carPrototype);

// Create instances using the prototype
const car1 = new Car("Toyota", "Camry");
const car2 = new Car("Honda", "Accord");

// Test the instances
console.log(car1.wheels); // Output: 4
console.log(car2.wheels); // Output: 4 (both instances share the same prototype)

// Modify a property for one instance
car1.wheels = 3;
console.log(car1.wheels); // Output: 3
console.log(car2.wheels); // Output: 4 (unchanged for other instance)

// Demonstrate modifications affecting other instances
carPrototype.wheels = 6; // Modify the prototype
console.log(car1.wheels); // Output: 3 (unchanged, as it has its own property now)
console.log(car2.wheels); // Output: 6 (modified because it shares the prototype)

Lastly, we must consider making prototype objects immutable to prevent unintentional changes during cloning.

The Builder Design Pattern

Builder is a creational design pattern that lets you construct complex objects step by step. The pattern allows you to produce different types and representations of an object using the same construction code.

Problem

Imagine a complex object that requires laborious, step-by-step initialization of many fields and nested objects. Such initialization code is usually buried inside a monstrous constructor with lots of parameters. Or even worse, scattered all over the client code.

For example, let's think about how to create a House object. To build a simple house, you need to construct four walls and a floor, install a door, fit a pair of windows, and build a roof. But what if you want a bigger, brighter house, with a backyard and other goodies(like a heating system, plumbing, and electrical wiring)

The simplest solution is to extend the base House class and create a set of subclasses to cover all combinations of the parameters. But eventually, you will end up with a considerable number of classes. Any new parameter, such as a porch style, will require growing this hierarchy even more.

There is another approach that doesn't involve breeding subclasses. You can create a giant constructor right in the base House class with all possible parameters that control the House object. While this approach indeed eliminates the need for subclasses, it creates another problem.

In most cases, most of the parameters will be unused, making the constructor calls pretty ugly. For instance, only a fraction of houses will have swimming pools, so the parameters related to swimming pools will be useless nine times out of ten.

Solution

The Builder pattern suggests that you extract the object construction code out of its own class and move it to separate objects called Builders.

This pattern organizes object construction into a set of steps (buildWalls, buildDoor, etc.).To create an object, you create a series of these steps on the builder object. The important part is that you don't need to call all of these steps. You can call only those steps that are necessary for producing a particular configuration of an object.

The factory pattern is also used to create objects, but it doesn't matter how they are created. The factory pattern is only concerned with the result of the creation, while the builder pattern not only gets the result but also participates in the specific process of creation, which is suitable for creating a complex compound object.

The real-world example of the Builder Pattern in React

One of the use cases for the Builder Pattern in React is creating immutable objects with many option parameters. This might sound a little complicated, but it just means you can use a User Buider to set various properties of a user object (name, email, password, role, etc.) and return a new user object with those properties. This makes it easier to create user objects with many different properties without writing tons of code.

Let me also explain how we can use the Builder pattern to simplify the creation of a User component with optional properties. Look at the example first:

import React from "react";

interface UserProps {
  firstName?: string;
  lastName?: string;
  email?: string;
  avatarUrl?: string;
}

class UserBuilder {
  private props: UserProps;

  constructor() {
    this.props = {};
  }

  withFirstName(firstName: string): UserBuilder {
    this.props.firstName = firstName;
    return this;
  }

  withLastName(lastName: string): UserBuilder {
    this.props.lastName = lastName;
    return this;
  }

  withEmail(email: string): UserBuilder {
    this.props.email = email;
    return this;
  }

  withAvatarUrl(avatarUrl: string): UserBuilder {
    this.props.avatarUrl = avatarUrl;
    return this;
  }

  build(): JSX.Element {
    return <User {...this.props} />;
  }
}

function User(props: UserProps) {
  return (
    <div>
      {props.avatarUrl && <img src={props.avatarUrl} alt="Avatar" />}
      {props.firstName && <p>{props.firstName}</p>}
      {props.lastName && <p>{props.lastName}</p>}
      {props.email && <p>{props.email}</p>}
    </div>
  );
}

function App() {
  const user1 = new UserBuilder().withFirstName("John").withEmail("john@example.com").build();
  const user2 = new UserBuilder().withFirstName("Jane").withLastName("Doe").withAvatarUrl("https://example.com/avatar.jpg").build();

  return (
    <div>
      {user1}
      {user2}
    </div>
  );
}

export default App;

Firstly, we define an UserProps interface that contains all the option properties. Then, we create an User class with these properties as its constructor parameters.

To create an User object with the Builder pattern, we define a UserBuilder class with methods for each optional property. These methods set the value of the corresponding property and return the UserBuilder instance itself to allow chaining. Finally, we define a build method that returns a new User object with the values set by the builder method.

Now, when we need to create a User component with certain properties, we can create a new instance of the UserBuilder class sets the desired properties using the builder method, and calls the build method to get a User object. We can pass this User object as a prop to the User component, which will render with the provided properties.

Using the Builder pattern allows us to create instances of the User component with only the desired properties, without having to pass all possible properties as separate props. This makes our code more concise and easier to maintain.

You may ask why we even need to create a property chain if we can simply pass a parameter as a component attribute.

Using Builder Pattern will give you some benefits when:

  • There is some logic happening with the parameters and you want to abstract it.

  • We need to create some repetitive components and avoid boosting your JSX, you may wrap them into the Builder.

Roundup

We have explored all 5 design patterns of the Creational Patterns, including the Singleton Design Pattern, Factory Design Pattern, Abstract Factory Design Pattern, Prototype Design Pattern, and the Builder Design Pattern. I'm sure that you have already used these patterns even if you didn't realize it. :))

I hope you enjoyed this article and learned something new.

Cheers, and we'll see each other in Part II of the Design Pattern series.

References

https://refactoring.guru/

https://itnext.io/best-practices-of-react-builder-pattern-only-react-pro-know-25b4b93cd39c

https://betterprogramming.pub/the-prototype-pattern-in-javascript-bfe9ff433e6c

https://levelup.gitconnected.com/exploring-prototype-pattern-in-javascript-c96820e246cf

https://levelup.gitconnected.com/exploring-the-abstract-factory-pattern-efef088da96e

https://medium.com/codex/factory-pattern-type-script-implementation-with-type-map-ea422f38862

0
Subscribe to my newsletter

Read articles from Tuan Tran Van directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Tuan Tran Van
Tuan Tran Van

I am a developer creating open-source projects and writing about web development, side projects, and productivity.