Skip to content

Clean Up Your Code With Design Patterns in Javascript

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.