Common JavaScript design patterns

Anton Ioffe - August 30th 2023 - 18 minutes read

Introduction to Design Patterns

Design patterns play a crucial role in the world of programming, especially in languages like JavaScript and TypeScript. They represent the best practices used by experienced software engineers to solve common problems in software design. These patterns serve as templates that can be imported into our code whenever confronted by a specific type of problem.

In the broadest sense, design patterns in JavaScript can be categorized into three main types: Creational, Structural, and Behavioral. Each type addresses specific aspects of code organization and problem-solving.

Creational design patterns focus on the process of object creation. The goal of these patterns is to create an object in a flexible and reusable way, without specifying the exact classes of objects that will be created. With these patterns, the code is not directly tied to the object's class, allowing for easier modification and extension of the code in the future. Examples of creational design patterns include the Factory Method, Abstract Factory, Builder, and Singleton.

Meanwhile, Structural design patterns focus on the composition of classes and objects. They help ensure that different parts of a program fit together neatly, enhancing understandability and completeness. The Composite, Decorator, and Adapter design patterns fall under this category.

Finally, Behavioral design patterns are centered on communication between objects, or more specifically, how objects can work together to perform tasks. The Observer, Strategy, and Command design patterns are part of this category.

Each of these design patterns brings unique advantages to the table when it comes to creating robust, flexible, and maintainable code. Better yet, using these patterns can also significantly reduce the complexity of your code while increasing its readability – a win-win for both the developer and any future code maintainers.

In this series of articles, we'll dive deep into these design patterns, dissecting each one thoroughly with practical JavaScript and TypeScript examples. From understanding their structure to their real-world applications, we'll explore how to apply them aptly in our everyday coding practices.

Prepare to embark on a journey to elevate your coding practices with design patterns and the power they offer to JavaScript and TypeScript developers.

Types of Design Patterns

Design patterns represent the best practices used by experienced developers. They help solve common issues encountered while designing a software or application. In the ecosystem of JavaScript and TypeScript, broadly speaking, there are three types of design patterns that we can leverage: Creational, Structural, and Behavioral patterns.

Creational Patterns are all about class instantiation or object creation mechanisms. These patterns aim to create objects for you, either directly or indirectly. In JavaScript and TypeScript, we often utilize Constructor pattern (creating an object using the new keyword), Factory pattern (creating an object without exposing the creation logic), and Singleton pattern (ensuring a class has only one instance) as the approaches for object creation.

// Constructor pattern
function House(area) {
    this.area = area;
}
const myHouse = new House(2000);

Moving on to Structural Patterns. These patterns focus on assembling different entities to ensure they collaborate to achieve a common goal. They are about organizing different classes and objects to form larger systems and provide new functionality. Classic examples in JavaScript and TypeScript include Decorator pattern (adding behavior to an object dynamically), Facade pattern (simplifying the interface of a complex system), and Adapter pattern (fits between two incompatible types of objects).

// Decorator pattern
class DecoratedHouse {
    constructor(private house: House) {}
    getCubicFeet() {
        return this.house.area * 10;
    }
}

Lastly, Behavioral Patterns are about communication or the delegation of responsibilities between objects, rendering the communication between them more flexible and efficient. Some of the well-known behavioral patterns in JavaScript and TypeScript include Observer pattern (an object maintains a list of dependents and notifies them of state changes), Strategy pattern (a group of algorithms encapsulated and made interchangeable), and Command pattern (encapsulates a request as an object and passes to a function).

// Observer pattern
class Subject {
    constructor() {
        this.observers = [];
    }
    addObserver(observer) {
        this.observers.push(observer);
    }
    notifyAllObservers() {
        this.observers.forEach((observer) => observer.update());
    }
}

Remember, the best use of design patterns is to provide solutions to specific problems in certain situations. All these patterns thrive on the principles of Object-Oriented Programming (OOP) - encapsulation, inheritance, polymorphism, and abstraction. They provide a structure that allows efficient and reusable code thereby enhancing the efficiency of the development process.

Now it's your turn to review some code. Can you spot elements of these design patterns in your own code or code you've seen? Is there a specific scenario in JavaScript/TypeScript development where a pattern appears more often?

Creational Design Patterns

Creational Design Patterns find use when you need to define and control how objects are created. They manage the object creation process by solving common problems that occur during object-oriented programming. The goal of these patterns is to abstract or encapsulate the object creation process and simplify client usage.

Let's delve a little deeper by analyzing some common Creational Design Patterns:

  1. Factory Pattern: This pattern provides a Generic interface for creating objects in a superclass. Instead of having the client instantiate the object directly using a constructor, you defer the object creation logic to a factory function. This isolates the object creation details from the client, achieving a more modular and manageable code structure.
class CarFactory {
  static createCar(model) {
    switch(model) {
      case 'sedan':
        return new Sedan();
      case 'hatchback':
        return new Hatchback();
      default:
        return new SUV();
    }
  }
}

In this example, the client only needs to call CarFactory.createCar('sedan') to get a Sedan car object. The object creation details are encapsulated within the CarFactory class.

  1. Singleton Pattern: This pattern ensures that a class has only one instance and provides a global access point to that instance. This is particularly useful when a single instance of a class is enough to control actions. A common example are loggers or database connections.
class Singleton {
  constructor() {
    if(!Singleton.instance) {
      Singleton.instance = this;
    }
    return Singleton.instance;
  }
}

The instance is stored statically on the class itself, so subsequent instantiation calls would return the same instance.

  1. Prototype Pattern: This pattern is based on the prototypical inheritance feature of JavaScript. It creates objects by cloning an existing object (prototype). It's equivalent to "copying" an object.
// Creating a prototype object
let protoCar = {
  drive() {
    return 'Driving...';
  }
}

// Create an object with protoCar as it's prototype
let myCar = Object.create(protoCar);
  1. Builder Pattern: The intent of the Builder design pattern is to separate the construction of a complex object from its representation. By doing so, the same construction process can create different representations.
class CarBuilder {
  constructor() {
    this.car = new Car();
  }
  addWheel() {
    this.car.add('wheel');
    return this;
  }
  addEngine() {
    this.car.add('engine');
    return this;
  }
  build() {
    return this.car;
  }
}

The user can then create a car as follows:

let carBuilder = new CarBuilder();
let car = carBuilder.addWheel().addEngine().build();

Comparing the Prototype and Factory patterns, while both are used to handle object creation, they differ in one key aspect. The factory pattern simply creates a new object whereas the prototype pattern clones or copies an existing object. The prototype pattern might be more appropriate when object creation is costly and you'd prefer to clone or copy an existing object. On the contrary, the factory pattern is for when you'd want a fresh instance each time and have more control over creation.

In conclusion, each of these patterns has its advantages and context of usage. Understanding how they work and when to use them can significantly improve code reusability, readability, and maintainability.

Your challenge now is to implement a variant of the Singleton pattern known as Monostate or Borg pattern. This pattern allows multiple instances to be created, but they all share the same state. If a change is made to any one instance, all instances reflect that change. This is a nice twist to the current Singleton pattern where you must refrain from creating multiple instances. Happy coding!

Structural Design Patterns

Structural design patterns are the third category of design patterns that deal with class and object composition. They focus on how classes and objects can be combined to form larger structures. In this section, we will focus on the Module and Model-View-Controller (MVC) patterns popular in JavaScript and TypeScript.

The Module design pattern is one of the most used and essential patterns in JavaScript and TypeScript. It allows us to create separate modules of code that are independent, reusable, and abstract our code for security and to hide the implementation details.

Let's take a look at a simplified example of a JavaScript module:

var myModule = (function() {
    'use strict';

    // private variable
    var privateVar = 5;

    // private method
    var privateMethod = function() {
        return 'Private stuff';
    };

    return {
        // public method
        publicMethod: function() {
        return 'The public can see me!' + ' ' + privateMethod();
        }
    };
})();
console.log(myModule.publicMethod()); 
// outputs "The public can see me! Private stuff"

Notice that the private variable and method aren't accessible from outside the module's scope. This kind of encapsulation is one of the main advantages of the Module pattern.

Another common pattern is Model-View-Controller (MVC). This architectural pattern decouples major components of an application, creating a separation of concerns and making the code cleaner and easier to maintain. Some may argue that MVC is indeed a factory pattern due to its instantiating nature.

In JavaScript, MVC could look something like this:

function Student(name) {
    this.name = name; 
}

Student.prototype = {
    constructor: Student,
    getName: function() {
    return this.name;
    }
};

// View
function StudentView() {}

StudentView.prototype = {
    printStudentDetails: function(name) {
    console.log('Student: ' + name);
    }
};

// Controller
function StudentController(model, view) {
    this.model = model;
    this.view = view;
}

StudentController.prototype = {
    setStudentName: function(name) {
    this.model.name = name;
    },
    updateView: function() {
    this.view.printStudentDetails(this.model.getName());
    }
};

var model = new Student("John");
var view = new StudentView();
var controller = new StudentController(model, view);

controller.setStudentName("John Doe");
controller.updateView();

There's a significant debate around whether MVC is truly a design pattern or just a big design principle that splits code into three interconnected elements, each with its designated role. It's fascinating to compare MVC to the Model-View-ViewModel (MVVM) model, which is quite popular in present development environments. While the two share similarities, the main difference lies within the ViewModel, which, unlike MVC's Controller, includes a data converter intended for the View.

It's important to note that while MVC is separation of concerns at play, it does not always mean it is a Singleton. Singleton ensures that a class should have only one instance and provides a global point of access. It can be used in MVC architecture. However, it's not required or recommended in every scenario but it might play a considerable role in some cases because of global app state handling.

It's worth considering MVC as a Singleton where the controllers are intermediaries that interact with models for data and prepare it to be rendered by the view. This way, we guarantee that there is only a single instance of data flow, creating a streamlined communication process between components of the application.

Hopefully, this breakdown serves as a beneficial introduction to structural patterns and how they function within JavaScript and TypeScript. Remember, the decision to use a specific pattern depends on the problem you face. The more you understand these patterns, the better equipped you'll be to write efficient, cleaner code. To deepen your understanding, look into the mechanics of other structural patterns such as Composite, Decorator, and Proxy. And consider attempting to implement and experiment with both Module and MVC design patterns on your own.

Behavioral Design Patterns

Behavioral design patterns deal with algorithms and the assignment of responsibilities between objects. They are concerned with how objects interact and how they divide responsibilities. The main goal of behavioral patterns is to increase flexibility in carrying out communication, to handle complex controls, and to manage relationships between objects at runtime.

Now, let us dive deeper to understand the most commonly used behavioral design patterns in Javascript and Typescript.

Observer Pattern

The Observer pattern provides a simple way for objects to communicate with each other. In this pattern, an object, called the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes. The observer pattern is essentially a publish/subscribe model.

class Subject {
  constructor() {
    this.observers = []; // array of observer functions
  }

  subscribe(fn) {
    this.observers.push(fn);
  }

  unsubscribe(fnToRemove) {
    this.observers = this.observers.filter(fn => fn !== fnToRemove);
  }

  fire() {
    this.observers.forEach(fn => fn.call());
  }
}

In this example, the Subject class maintains a list of observer functions, which can be added to or removed from the list. The fire method will call each observer function when the subject is changed.

Strategy Pattern

The Strategy pattern defines a set of algorithms that can be used interchangeably. In other words, it allows us to choose the algorithm to be used at runtime. This pattern is useful when there is a set of related algorithms, and a client object needs to be able to dynamically pick and choose an algorithm from this set that suits its current need.

class Shipping {
  constructor() {
    this.company = '';
  }

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

  calculate(package) {
    return this.company.calculate(package);
  }
}

In this case, Shipping plays the role of the context, where clients can dynamically change the strategy (setStrategy) of performing some operation (calculate).

Iterator Pattern

The Iterator pattern allows clients to effectively loop over a collection of objects. A common programming task is to traverse and manipulate a collection of objects. These collections may be stored as an array or perhaps something more complex.

function createIterator(array) {
  let nextIndex = 0;

  return {
    next: function() {
      return nextIndex < array.length
        ? { value: array[nextIndex++], done: false }
        : { done: true };
    }
  };
}

In the above example, you see the basis of the iterator design pattern: creating an iterator object that takes care of traversing an array.

These examples merely scratch the surface of behavioral design patterns in JavaScript and TypeScript. There are many more such as Command, Mediator, and Visitor.

In general, practicing with these patterns will enrich your understanding and enable you to write better, more flexible, and maintainable code. Remember, the key to using these patterns effectively is understanding not just how they work, but also when to apply them.

Design Patterns in JavaScript Libraries and Frameworks

Design patterns, which embody good practices and optimum solutions for standard software problems, are essential elements in any software architecture. They can drastically improve the maintainability, scalability, and robustness of applications. Especially in prominent JavaScript libraries and frameworks like React and Node.js, design patterns have been leveraged remarkably.

Examining React, the question arises whether it employs the classic MVC (Model-View-Controller) design pattern. However, React does not follow this conventional model but leans towards the VDOM (Virtual DOM) technology instead. A light-weight replica of the actual DOM, VDOM forms an integral part of the React's Diffing algorithm comprehensively enhancing React's speed.

Regardless, the flexibility to use design patterns in React should be lauded. Developers can structure their applications using MVC or its variations based on a project's needs. For instance, consider an application module in React employing an Observer pattern. Multiple 'child' components can 'observe' or react to state changes in a 'parent' component, ensuring data consistency across the module. Alternatively, patterns such as Module, Factory, or higher-order components can be adopted, each giving a unique shape to your React application architecture.

Next, we consider Node.js, well-known for its non-blocking, event-driven, single-threaded technology. The types of design patterns in Node.js, such as Factory, Revealing Module, and Observer patterns, are significantly influenced by its architecture. One cannot miss mentioning the Middleware pattern, fundamental to Node.js architecture, which sequences multiple handlers in a specific order, each performing a function before transferring control to the next. Here is an example of using a Middleware pattern in a Node.js web server.

// defining a middleware function
const middleware = (req, res, next) => {
  console.log('This is a middleware example');
  next();  // boot the 'next' hook function
};

// using the middleware function in the app
app.use(middleware);

While organising code and enhancing reusability, each design pattern also holds its strengths and weaknesses concerning complexity. Performance, memory management, and code readability are some of the considerations for pattern selection.

Challenge:

Evaluate Singleton and Prototype design patterns applied to React and Node.js. Analyze the performance enhancement or architectural advantage gained by their implementation as compared to the existing design patterns. Also, explore the variation in pattern choices across different libraries or frameworks such as React versus Node.

Learning Design Patterns

Understanding the concept of design patterns and properly working with them is one of the most essential skills for every javascript/typescript developer. So why should someone learn design patterns and even more important, how to do it correctly?

Design patterns are predefined solutions to common problems that occur frequently in software design. They represent a set of practices and tactics that have been proven to solve particular software design problems effectively.

By studying design patterns, developers gain a repertoire of solutions they can apply to their projects. Additionally, it allows developers to communicate more efficiently, having a shared language and solutions to refer to.

Let's dive into why it's a must-know subject for every proficient javascript/typescript programmer:

Practical applicability

Design patterns have broad applicability in different types of projects. Large scale or small, web applications or data processing systems, you will often encounter situations where a design pattern will present itself as the ideal solution.

Mastering the principles of software design

Learning design patterns drives a deeper understanding of general software design concepts like modularity, abstraction, coupling, and cohesion. These concepts lie at the heart of good, maintainable code.

Well-tested solutions

Design patterns are tried and true; they have been through the trenches of hundreds of thousands of projects and have proven their effectiveness and efficiency. Utilizing a design pattern guarantees robustness and reliability.

Enhance problem-solving skills

By understanding design patterns, a developer is trained to see the generic solution schema instead of focusing on specific problems. This mindset shift will greatly enhance problem-solving capabilities and decrease development time significantly.

Now, how should you approach learning design patterns? Here are some guidelines:

  1. Start with the basics: Learn about the Singleton or Factory patterns, they are comparatively simple and are used quite often. It establishes a good base to move forward.

  2. Build projects: There is no substitute for practical experience. Try to implement patterns in small projects. For instance, try to implement the Observer pattern in a small chat application.

  3. Observe and analyze: Devote time to study open-source projects and libraries. Try to identify patterns used in the code.

  4. Don't rush: There is no need to know all patterns immediately; focus on mastering one before moving to the next.

Is it necessary to be familiar with all of them? The simple answer is, no. Not all design patterns will be useful in every project. Depending on the kind of projects you usually work on, certain patterns will likely be more prominent. As a rule of thumb, it is more beneficial to understand a few patterns but in great depth.

As for the contemporary significance of design patterns, they are as relevant today as they have ever been. They offer robust solutions to recurring problems and greatly assist in maintaining code quality standards.

However, as software engineering evolves, so do patterns. Emerging programming models and paradigms influence patterns' development as newer solutions are identified or older ones refined. Therefore, it is crucial to stay updated on these changes. Gather insights by continuously studying and working with design patterns to deepen your knowledge and grow your skill set.

By understanding and mastering design patterns, you solidify your position as a senior javascript/typescript developer and increase your ability to deliver efficient, reliable, and high-quality code.

Conclusion

In the course of this article, we've walked through an array of intricate JavaScript and TypeScript development patterns, exploring their complexities through numerous real-world examples. We have dived deep into all the corners of problem solving in JavaScript and TypeScript and discussed the multitudes of possible approaches, contemplating on their benefits and drawbacks.

Our discourse formed an expanse covering code performance, memory management, code complexity, readability, and the necessity for maintaining modularity and reusability. We also spared no effort in examining some of the most common mistakes developers make and provided insights on how to avoid such pitfalls.

The world of JavaScript and TypeScript is constantly evolving. With the advent of new technologies and frameworks, the landscape of JavaScript and TypeScript will undoubtedly continue its transformation. Design patterns in JavaScript will become more sophisticated, and patterns that are dominant today might make room for more efficient ones tomorrow. TypeScript being a superset of JavaScript adds more powerful features and guarantees more safety and clarity - a quality of code that could be crucial with scaled applications, taking web development to a whole new level of complexity and precision.

Consequently, to keep up to pace with the rapid advancement of technology, there is a vital need for continual learning, proactive understanding, and standing open to adopting and constructing new design patterns. As JavaScript's complexity increases, so does the depth and variety of its patterns and paradigms.

In conclusion, these patterns are just tools at our disposal - it's more about when and how you use them that determines the true craftsmanship. Each pattern has its place, and selecting the right one can dramatically enhance our programming experience and deliver more efficient, maintainable code.

Remember the overarching principle - code should always be as simple as it can be and still solve the problem at hand, no simpler. Let's not complicate our code merely because we understand complex patterns. Using the right pattern at the right time makes you an efficient developer, and that is the ultimate goal.

Appendix: Interview Questions based on JavaScript Design Patterns

Design patterns are fundamental knowledge for anyone venturing into software development, especially if you're specializing in JavaScript or TypeScript.

Let's delve into a compilation of some potential interview questions and comprehensive answers revolving around design patterns in these languages.

  1. What is a design pattern and why is it important?

Design patterns are well-known solutions to recurring problems in the software design process. They reflect best practices and provide a shared vocabulary for software design. Understanding design patterns helps developers avoid coding pitfalls and make effective design decisions.

  1. Describe the Singleton Pattern and its use cases in JavaScript.

The Singleton pattern restricts the instantiation of a class to a single object and provides a global access point to this instance. This is useful in scenarios like logging, database connections, or anything else where you'd need only one instance of an object class.

JavaScript implementation:

// Singleton function which creates a new instance only if it does not exist yet.
var Singleton = (function () {
    var instance;

    function createInstance() {
        var object = new Object("I am the instance");
        return object;
    }

    return {
        getInstance: function () {
            if (!instance) {
                instance = createInstance();
            }
            return instance;
        }
    };
})();
  1. Please explain the Factory Pattern and provide an example in TypeScript.

The Factory pattern is a creational pattern that provides an interface for creating objects in a super class, but allows subclasses to alter the type of objects that will be created. It's useful when you're dealing with a system that has numerous possible types of classes, and you don't know which type of class to create until runtime.

TypeScript implementation:

// `createProduct` method returns an instance of a different class based on input type parameter.
interface Product {
    functionA(): void;
}

class ProductA implements Product {
    functionA() {
        console.log("ProductA function");
    }
}

class ProductB implements Product {
    functionA() {
        console.log("ProductB function");
    }
}

class ProductFactory {
    public static createProduct(type: string): Product {
        if (type === "A") {
            return new ProductA();
        } else if (type === "B") {
            return new ProductB();
        }
        throw new Error("Invalid product type.");
    }
}
  1. What are the benefits of the Observer Pattern in JavaScript?

The Observer pattern offers a subscription model where objects ('observers') can subscribe to an event and be notified when this event happens. This pattern is beneficial in JavaScript for event handling, where you would want a function to be executed whenever a specific event occurs.

  1. Could you describe the Decorator Pattern in the context of TypeScript?

The Decorator pattern is a structural design pattern that allows you to add new behavior to objects dynamically by placing them inside special wrapper objects. In TypeScript, decorators provide a way to add annotations and modify classes, methods, and properties at design time.

Here is an example of using the Decorator pattern in TypeScript:

// The @ReadOnly decorator makes the method read only
function ReadOnly(target, key, descriptor) {
    descriptor.writable = false;
    return descriptor;
}

class MyClass {
    @ReadOnly
    myMethod() { console.log('Hello') }
}

let myClass = new MyClass();
myClass.myMethod = function() { console.log('Hello Decorated') }; // This will fail because the method is read only now.

I hope these questions shed light on design patterns and their critical role in the JavaScript/TypeScript ecosystem. Understanding these patterns can improve your code structure, performance, and readability while aiding in solving common software design problems.

Don't Get Left Behind:
The Top 5 Career-Ending Mistakes Software Developers Make
FREE Cheat Sheet for Software Developers