Skip to content

Clean Up Your Code With Design Patterns in Javascript

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

This is a recap from the talk 'Design Patterns in Javascript' by Tim Winfred. Check out the full talk on Youtube!

Design patterns are a bit of a controversial topic in the dev community. While some developers believe they are overly complicated, others are dogmatic about using them. As a JavaScript engineer, there's a good chance you'll need to know them at some point in your career, whether you subscribe to design patterns or not. Let's unpack a few commonly-used JavaScript design patterns together, and discuss how they can make your code cleaner and easier to maintain.

What is a design pattern?

In software engineering, a __ design pattern__ is a general repeatable solution to a commonly occurring problem in software design. A design pattern isn't a finished design that can be transformed directly into code. It is a description or template for how to solve a problem that can be used in many different situations.

In addition, patterns allow developers to communicate using well-known, well understood names for software interactions. Common design patterns can be improved over time, making them more robust than ad-hoc designs.

Types of JavaScript Design Patterns

  • Creational Design Patterns: Situation-specific patterns that reduce complexity by controlling object creation.
  • Structural Design Patterns: Realizing relationships among entities to simplify design.
  • Behavioral Design Patterns: Identify common communication patterns among objects to increase flexibility in carrying out communication.

There is a fourth group called the Concurrey Design Patterns, but these are patterns that deal with the multi-thread programming pattern.

Factory Design Pattern (Creational)

Define an interface for creating a single object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.

factory-pattern

/**
 * The Creator class declares the factory method that is supposed to return an
 * object of a Product class. The Creator's subclasses usually provide the
 * implementation of this method.
 */
abstract class Creator {

    public abstract factoryMethod(): Product;
    public someOperation(): string {
        const product = this.factoryMethod();
        return `Creator: The same creator's code has just worked with ${product.operation()}`;
    }
}

class ConcreteCreator1 extends Creator {
    public factoryMethod(): Product {
        return new ConcreteProduct1();
    }
}

interface Product {
    operation(): string;
}

class ConcreteProduct1 implements Product {
    public operation(): string {
        return '{Result of the ConcreteProduct1}';
    }
}

function clientCode(creator: Creator) {
    console.log('Client: I\'m not aware of the creator\'s class, but it still works.');
    console.log(creator.someOperation());
}

/**
 * The Application picks a creator's type depending on the configuration or
 * environment.
 */
console.log('App: Launched with the ConcreteCreator1.');
clientCode(new ConcreteCreator1());
console.log('');

Facade Design Pattern (Structural)

Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use.

facade-pattern

export default class User {
  private firstName: string
  private lastName: string
  private bankDetails: string | null
  private age: number
  private role: string
  private isActive: boolean

  constructor({firstName,lastName,bankDetails,age,role,isActive} : IUser){
      this.firstName = firstName
      this.lastName = lastName
      this.bankDetails = bankDetails
      this.age = age
      this.role = role
      this.isActive = isActive
  }

  getBasicInfo() {
      return {
          firstName: this.firstName,
          lastName: this.lastName,
          age : this.age,
          role: this.role
      }
  }

  activateUser() {
      this.isActive = true
  }

  updateBankDetails(bankInfo: string | null) {
      this.bankDetails= bankInfo
  }

  getBankDetails(){
      return this.bankDetails
  }

  deactivateUser() {
      this.isActive = false
  }
}
export default interface IUser {
  firstName: string
  lastName: string
  bankDetails: string
  age: number
  role: string
  isActive: boolean
}

export default class UserFacade{
  protected user: User
  constructor(user : User){
      this.user = user
  }

  activateUserAccount(bankInfo : string){
      this.user.activateUser()
      this.user.updateBankDetails(bankInfo)
      return this.user.getAllDetails()
  }

  deactivateUserAccount(){
      this.user.deactivateUser()
      this.user.updateBankDetails(null)
  }
}

Strategy Design Pattern (Behavioral)

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm exist very independently from clients that use it.

strategy-pattern

/**
 * The Context defines the interface of interest to clients.
 */
class Context {

    private strategy: Strategy;

    constructor(strategy: Strategy) {
        this.strategy = strategy;
    }

    public setStrategy(strategy: Strategy) {
        this.strategy = strategy;
    }

    public doSomeBusinessLogic(): void {
        console.log('Context: Sorting data using the strategy (not sure how it\'ll do it)');
        const result = this.strategy.doAlgorithm(['a', 'b', 'c', 'd', 'e']);
        console.log(result.join(','));
    }
}

interface Strategy {
    doAlgorithm(data: string[]): string[];
}

class ConcreteStrategyA implements Strategy {
    public doAlgorithm(data: string[]): string[] {
        return data.sort();
    }
}

class ConcreteStrategyB implements Strategy {
    public doAlgorithm(data: string[]): string[] {
        return data.reverse();
    }
}

const context = new Context(new ConcreteStrategyA());
console.log('Client: Strategy is set to normal sorting.');
context.doSomeBusinessLogic();

console.log('');

console.log('Client: Strategy is set to reverse sorting.');
context.setStrategy(new ConcreteStrategyB());
context.doSomeBusinessLogic();

Decorator Design Pattern (Structural)

Attach additional responsabilities to an object, dynamically keeping the same interface. Decorators provide a flexible alternative to subclassing for extending functionality.

Command Design Pattern (Behavioral)

Encapsulate a request as an object, thereby allowing for the paramerization of clients with different requests, and the queuing or logging the requests.

Singleton Design Pattern (Creational)

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

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co