Design Patterns
🪚

Design Patterns

 
notion image
 
notion image
 

Common

Creational Design Patterns

  • Constructor
  • Factory
  • Singleton
  • Builder
  • Prototype
  • Abstract

Structural Design Patterns

  • Decorator
  • Facade
  • Flyweight
  • Adapter
  • Proxy

Behavioral Design Patterns

  • Iterator
  • Mediator
  • Observer
  • Visitor

Front-end specific

Module Pattern

  • Object literal notation
  • The Module pattern
  • AMD modules

Creational Design Patterns

Constructor Pattern

The "constructor pattern", as its name suggests, is a class-based pattern that utilizes the constructors in a class to create specific types of objects.

Factory Pattern

The code does not use the new keyword to directly instantiate objects. Instead, it provides a generic interface that delegates the responsibility of creating objects to the corresponding subclass.

use cases:

Create objects for different browsers or devices. For example, a factory class could be used to create different types of video players for different browsers.
JavaScript
class ToyFactory {
    createToy({toyType,color,price}){
        switch(toyType){
            case 'car':
                return new ToyCar(color,price);
            case 'duck':
                return new ToyDuck(color,price);
        }
    }
}

class ToyDuck{
    constructor(color,price){
       this.color=color;
       this.price = price;
    }
}

class ToyCar {
   constructor(color,price){
       this.color=color;
       this.price = price;
    }
}

Singleton Pattern

There should only be one instance of a class, which clients can access from a well-known access point. The single instance should be extendable through subclassing, and clients should be able to use an extended instance without changing their code.

use cases:

  • Services: Services are single instances that store state, configuration, and resources. It's logical to have only one instance of a service in an application.
  • Configurations: If there's an object with a specific configuration, there's no need for a new instance every time it's needed.
  • Databases: Databases like MongoDB use the singleton pattern for connections.
JavaScript
class Configuration{
    static configure = null;
    constructor(config){
        this.endPoint = config.endPoint;
        this.retryStrategy = config.retryStrategy;
    }
    static getConfiguration(config){
        if (!Configuration.configure) {
            Configuration.configure = new Configuration(config)
            }
            return Configuration.configure;
    }
}

The Difference Between a Static Instance of a Class and a Singleton

While a Singleton can be implemented as a static instance, it can also be constructed lazily without using resources or memory until the static instance is needed.

Builder Pattern

The builder pattern is a creational pattern that simplifies the process of building complex objects using simpler ones. This pattern offers a flexible, step-by-step approach to creating these objects while keeping their representation and creation process shielded.

use cases:

  • Create complex objects: DOM where you may need to create a large number of nodes and attributes.

Prototype Pattern

The Prototype pattern is a design pattern that allows new objects to be created by copying an existing object. This is useful when creating a new object from scratch is expensive or complicated. The prototype object acts as a template for creating new objects that are identical to the prototype but can be customized or extended as needed. It also makes object creation more efficient and customization easier by allowing the type of object to be specified without specifying the exact class.
notion image
 
In JavaScript, objects can be cloned using the Object.create method
JavaScript
var plane = {
    takeOff(){
        console.log("Start taking off")
        },
    landing(){
        console.log("Start landing")
    },
} 

const plane1 = Object.create(plane, {color :{value: "red"}});
plane1.takeOff();
plane1.landing();

const plane2 = Object.create(plane, {color :{value: "blue"}});
plane2.takeOff();
plane2.landing();

console.log(plane1.__proto__ === plane2.__proto__ && plane2.__proto__ === plane)

Structural Design Patterns

Decorator Pattern

The Decorator Pattern provides a way to add behavior to existing classes in a system dynamically. It can be used to modify existing systems where additional features need to be added to objects without the need to heavily modify the underlying code. The decorator pattern is a structural pattern that focuses on decoration rather than object creation. It doesn't rely on prototypal inheritance alone and adds decoration to an object, making the process more streamlined. Here's an example to understand this concept better.
notion image

use cases:

Text formatting: For instance, a common application of this pattern is text formatting, where various formatting styles such as bold, italics, and underline can be applied to the same text.
JavaScript
// The constructor to decorate
function MacBook() {
	this.cost = function () { return 997; };
	this.screenSize = function () { return 11.6; }; 
}
// Decorator 1
function Memory( macbook ) {
	var v = macbook.cost(); 
	macbook.cost = function() {
		return v + 75; };
}
// Decorator 2
function Engraving( macbook ){
	var v = macbook.cost(); 
	macbook.cost = function(){
		return v + 200; };
}
// Decorator 3
function Insurance( macbook ){
	var v = macbook.cost(); 
	macbook.cost = function(){
		return v + 250; };
}

var mb = new MacBook(); 
Memory( mb ); 
Engraving( mb ); 
Insurance( mb );

// Outputs: 1522
console.log( mb.cost() ); 
// Outputs: 11.6
console.log( mb.screenSize() );
Mainly it can replace the situation when we need to create subclass to add a method or properties to the base class.

Facade Pattern

The facade pattern provides a simpler interface that hides the complex functionalities of a system.

use cases:

  • Online shopping system: only expose a simple api, but the system actucally need to invoke the inventry, notification, payment systems internally.

Adapter Pattern

Adapter pattern enables interaction between classes with different interfaces by translating the interface of one class to work with another. This pattern is useful when modifying an API or adding new implementations, allowing the interface to work with other parts of a system.

use cases:

The adapter pattern is used to make old APIs work with new ones or when an object needs to work with a class that has an incompatible interface. It can also be used to reuse existing functionality of classes.
JavaScript
// old interface
class TruthAndDare {
  constructor(){
    this.turn = Math.floor(Math.random() * 2) + 1;
  }
  Getturn(){
    if(this.turn == 1){
      this.turn = 2
    }else{
      this.turn = 1
    }
    return this.turn
  }
  playGame(playerOnename,playerTwoname){
    if(this.Getturn() == 1){
      return`${playerOnename}'s turn`
    }else{
      return `${playerTwoname}'s turn`
    }
  }
}

// new interface
class NewTruthAndDare {
  constructor(randomValue){
      this.turn = randomValue
  }
 
  newplayGame(playerOnename,playerTwoname){
    if((this.turn % 2) == 0){
      return `${playerOnename}'s turn`
    }else{
      return `${playerTwoname}'s turn`
    }
  }
}

// Adapter Class
class Adapter {
  constructor(randomValue) {
     this.newGame = new NewTruthAndDare(randomValue)
  }
	playGame(playerOnename,playerTwoname) {
     return this.newGame.newplayGame(playerOnename,playerTwoname)
   };
}

Bridge Pattern

The bridge pattern allows collaboration between components with different interfaces and implementations, such as controlling an air conditioner with a remote, regardless of their type or features. It enables independent work between input and output devices.

use cases:

  • Extend a class in several independent dimensions.
  • Change the implementation at runtime.
  • Share the implementation between objects.

Flyweight Pattern

This pattern involves taking common data structures/objects that are frequently used by many objects and storing them in an external object (known as a flyweight) for sharing.

use cases:

The flyweight pattern is often used in network apps, word processors, and web browsers to prevent loading duplicate images. It enables image caching so that only new images are downloaded from the Internet while existing ones are fetched from the cache when a web page loads.

Proxy Pattern

This term refers to an object that acts as a placeholder and controls access to another object. It includes an interface with properties and methods for clients to access.

use cases:

  • The proxy pattern reduces the workload on the target object, especially when dealing with heavy applications that make numerous network requests. This prevents delays in responding to requests by ensuring that the target object is not overwhelmed with requests.
  • Image preloading
    • JavaScript
      var myImage = (function(){
        var imgNode = document.createElement('img');
        document.body.appendChild(imgNode);
        return {
          setSrc: function(src){
            imgNode.src = src;
          }
        };
      })();
      
      var proxyImage = (function(){
        var img = new Image;
        img.onload = function(){
          myImage.setSrc(this.src);
        };
        return {
          setSrc: function(src){
            myImage.setSrc('file:///C:/Users/svenzeng/Desktop/loading.gif');
            img.src = src;
          }
        };
      })();
      
      proxyImage.setSrc('http://imgcache.qq.com/music/photo/k/000GGDys0yA0Nk.jpg');

Observer Parttern

This design pattern enables observers to wait for input and react to it when notified, without continuously checking for it. The main subject maintains a list of all the observers and notifies them when the event occurs, so they can update their states.
notion image
notion image

use cases

  • Break down large applications into loosely-coupled objects for better code management.
  • Enable dynamic relationships between observers and subscribers to increase flexibility.
  • Improve communication between different parts of the application.
  • Create a one-to-many dependency between loosely-coupled objects.
JavaScript
import './style.css';
import {
  sendToGoogleAnalytics,
  sendToCustomAnalytics,
  sendToEmail,
} from './analytics.js';

const observer = {
  handlers: [],
  observe(handler) {
    this.handlers.push(handler);
  },
  notify(...args) {
    this.handlers.forEach((handler) => {
      handler.apply(this, args);
    });
  },
};

observer.observe(sendToGoogleAnalytics);
observer.observe(sendToCustomAnalytics);
observer.observe(sendToEmail);

const pinkBtn = document.getElementById('pink-btn');
const blueBtn = document.getElementById('blue-btn');

pinkBtn.addEventListener('click', () => {
  const data = '🎀 Click on pink button! 🎀';
  observer.notify(data);
});

blueBtn.addEventListener('click', () => {
  const data = '🦋 Click on blue button! 🦋';
  observer.notify(data);
});
 

Visitor Pattern

 

Command Pattern

Compared to procedural request invocation, command objects have a longer lifecycle. The object's lifecycle is not related to the initial request. This request has been encapsulated in the method of the command object, becoming the behavior of this object.

use cases

  • Redo or undo work
    • JavaScript
      var Ryu = {
          attack: function(){
              console.log('attack');
          },
          defense: function(){
              console.log('defense');
          },
          jump: function(){
              console.log('jump');
          },
          crouch: function(){
              console.log('crouch');
          }
      };
      
      var makeCommand = function(receiver, state){
          return function(){
              receiver[state]();
          };
      };
      
      var commands = {
          "119": "jump", // W
          "115": "crouch", // S
          "97": "defense", // A
          "100": "attack" // D
      };
      
      var commandStack = [];
      
      document.onkeypress = function(ev){
          var keyCode = ev.keyCode,
              command = makeCommand(Ryu, commands[keyCode]);
      
          if(command){
              command();
              commandStack.push(command);
          }
      };
      
      document.getElementById('replay').onclick = function(){
          var command;
          while(command = commandStack.shift()){
              command();
          }
      };
      
  • Command queue: For ordering food, if there are too many orders and not enough chefs, orders can be queued for processing. Queues are also used in animations. For example, if a ball motion program is only suitable for people with an APM of less than 20, users' clicks may interrupt ongoing animations. To solve this, we can encapsulate motion processes of the div into command objects and push them into a queue stack. When the animation is completed, the first command object waiting in the queue will be taken out and executed. To notify the queue after an animation is completed, callback functions or the publish-subscribe mode can be used.
  • Macro command: A macro command is a collection of commands that can be executed all at once by running the macro command.
    • JavaScript
      var closeDoorCommand = {
        execute: function() {
          console.log('Close Door');
        }
      };
      var openPcCommand = {
        execute: function() {
          console.log('Turn on PC');
        }
      };
      var openChromeCommand = {
        execute: function() {
          console.log('Open chrome');
        }
      };
      
      var MacroCommand = function() {
        return {
          commandsList: [],
          add: function(command) {
            this.commandsList.push(command);
          },
          execute: function() {
            for (var i = 0, command; command = this.commandsList[i++];) {
              command.execute();
            }
          }
        }
      };
      
      var macroCommand = MacroCommand();
      macroCommand.add(closeDoorCommand);
      macroCommand.add(openPcCommand);
      macroCommand.add(openChromeCommand);
      macroCommand.execute();
      

Strategy pattern

The strategy pattern involves defining a series of algorithms, encapsulating them one by one, and making them replaceable with each other.
This pattern utilizes techniques such as composition, delegation, and polymorphism to avoid multiple conditional statements. It also supports the open-closed principle by encapsulating algorithms in independent strategies that are easy to switch, understand, and extend. Furthermore, the algorithms in this pattern can be reused in other parts of the system, reducing repetitive copy-pasting tasks.
The Context in this pattern uses composition and delegation to execute algorithms, providing a lighter alternative to inheritance.

use cases:

  • Calculate bons
    • JavaScript
      var strategies = {
        "S": function(salary) {
          return salary * 4;
        },
        "A": function(salary) {
          return salary * 3;
        },
        "B": function(salary) {
          return salary * 2;
        }
      };
      
      var calculateBonus = function(level, salary) {
        return strategies[level](salary);
      };
      
      console.log(calculateBonus('S', 20000));
      console.log(calculateBonus('A', 10000));
      // Output: 80000
      // Output: 30000
      
  • Form validation
    • JavaScript
      var Validator = function(){
          this.cache = []; // Save validation rules
      };
      Validator.prototype.add = function(rule, dom, errorMsg){
          var ary = rule.split( ':' );
          this.cache.push(function(){
              var strategy = ary.shift();
              ary.unshift(dom.value);
              ary.push(errorMsg);
              return strategies[strategy].apply(dom, ary);
          });
      };
      Validator.prototype.start = function(){
          for ( var i = 0, validatorFunc; validatorFunc = this.cache[ i++ ]; ){
              var msg = validatorFunc();
              if ( msg ){
                  return msg;
              }
          }
      };
      
      var strategies = {
        isNonEmpty: function(value, errorMsg) {
          if (value === '') {
            return errorMsg;
          }
        },
        minLength: function(value, length, errorMsg) {
          if (value.length < length) {
            return errorMsg;
          }
        },
        isMobile: function(value, errorMsg) { // mobile number format
          if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
            return errorMsg;
          }
        }
      };
      
      var validateFunc = function(){
      	var validator = new Validator(); // Create a validator object
      	/***************Add some validation rules****************/
      	validator.add( registerForm.userName, 'isNonEmpty', 'Username cannot be empty' );
      	validator.add( registerForm.password, 'minLength:6', 'Password length cannot be less than 6 characters' );
      	validator.add( registerForm.phoneNumber, 'isMobile', 'Incorrect mobile phone number format' );
      	var errorMsg = validator.start(); // Get validation results
      	return errorMsg; // Return validation results
      }
      
      var registerForm = document.getElementById( 'registerForm' );
      
      registerForm.onsubmit = function(){
      	var errorMsg = validateFunc(); // If errorMsg has a specific return value, it means validation has not passed
      	if ( errorMsg ){
      		alert ( errorMsg );
      		return false; // Prevent form submission
      	}
      };

Publish/Subscribe Pattern

The publish/subscribe pattern is a messaging pattern in which senders (publishers) send messages to channels (topics) rather than directly to specific receivers (subscribers). Subscribers subscribe to the channels they are interested in and receive messages when they are sent to those channels.
This pattern enables a more flexible and scalable system, as new subscribers can easily join and leave the system, and new publishers can send messages without needing to know specific subscribers. It also allows for asynchronous communication, as both publishers and subscribers do not need to be online at the same time.
The pattern can be implemented using message queues, such as RabbitMQ, Apache Kafka, etc., or using message brokers and their variants, such as topic, fanout, headers, and direct.
notion image
Example:
  • Node.js — EventEmitter
  • Vue.js — EventBus
JavaScript
 class Myevent{
		constructor(){
			this.eventMap = {}
		}

		on(type, handler){
			if(!(handler instanceof Function)){
				throw new Error('pass a function');
			}
			if(!this.eventMap[type]){
				this.eventMap[type] = []
			}
			this.eventMap[type].push(handler);
		}

		emit(type,params){
		  if(this.eventMap[type]){
			   this.eventMap[type].forEach((handler,index)=>{
				  hander(params)
		})
		}

		off(type,handler){ 
			if(this.eventMap[type]){
			this.eventMap[type].splice(this.eventMap[type].indexOf(handler)>>>0,1)
			}
		}
		}
}

Behavioral Design Patterns

The chain of responsibility

The chain of responsibility pattern allows a request to be passed through multiple objects, creating a chain of loosely coupled objects that either handle the request or pass it on to the next object.

use cases:

  • The process of event bubbling in the DOM

Mediator Pattern (Centralized Controller)

The Mediator pattern is a behavioral design pattern that defines an object to mediate communication between other objects in a system. It promotes loose coupling between objects by prohibiting them from explicitly referring to each other. This allows for objects to be reused in different contexts. In this pattern, the Mediator acts as a hub for communication and controls the communication between the objects. It can be used to handle complex communication in a GUI application, events in a game, or managing interactions in a simulation.
notion image
 
For example:
  • Event delegation

Command Pattern

The Command pattern is a behavioral design pattern that encapsulates a request or operation as an object separate from the object that actually performs the request. This allows for a decoupling of the classes that invoke operations and the classes that perform those operations, making it easier to add new operations or change existing ones.
The Command pattern consists of four main components: the command interface, the concrete command classes, the invoker, and the receiver. The command interface defines a single method that represents the operation. The concrete command classes implement the command interface and contain the specific logic for performing the operation. The invoker is the object that holds a command and is responsible for calling the command's execute() method. The receiver is the object that the command will act upon.
An example of using the Command pattern can be a button on a graphical user interface. When the button is clicked, it sends a command to a specific object (the receiver) to perform some operation. The button itself is the invoker, and the command is the operation that should be performed (e.g. opening a file).
Another example could be a remote control, where the buttons on the remote send commands to the TV to perform specific actions like turning the volume up or changing the channel. The remote control is the invoker, the buttons are the concrete command, the TV is the receiver, and the commands sent are the operation that should be performed.
In summary, the Command pattern is a way of decoupling the objects that invoke operations from the objects that perform those operations. It encapsulates the request or an operation as an object and allows for easy addition and modification of operations, as well as undo and redo operations, by storing the command history.

Mixin Pattern

A mixin is an object that can be used to add reusable functionality to another object or class, without using inheritance. Mixins cannot be used on their own; their sole purpose is to add functionality to objects or classes without inheritance.
notion image
Disadvantages:
Injecting functionality into an object prototype is a bad idea because it leads to both prototype pollution and uncertainty regarding the origin of our functions.

Provider Pattern

Data is passed from parents to deep children in the node tree.

Container/Presentational Pattern

notion image
notion image
 

Module Pattern

A great benefit of having modules, is that we only have access to the values that we explicitly exported using the export keyword. Values that we didn't explicitly export using the export keyword, are only available within that module.
 
Dynamic import
notion image
notion image

Progressive Hydration

notion image
 
notion image
 

Static Rendering (SSG)

Static rendering or static generation(SSG)
 
SSG helps to achieve a faster FCP/TTI
notion image

Incremental Static Generation (ISSG)

Update static content after you have built your site
iSSG allows you to update existing pages and add new ones by pre-rendering a subset of pages in the background even while fresh requests for pages are coming in.
 
iSSG uses the stale-while-revalidate strategy where the user receives the cached or stale version while the revalidation takes place. The revalidation takes place completely in the background without the need for a full rebuild.
notion image

Server-Side Rendering

notion image
 
Challenges:
  1. Managing a large number of HTML files: Every possible route that the user may access requires an individual HTML file to be generated. For example, when using it for a blog, an HTML file will be generated for every blog post available in the data store. Subsequently, edits to any of the posts will require a rebuild for the update to be reflected in the static HTML files. Maintaining a large number of HTML files can be challenging.
  1. Hosting dependency: For an SSG site to be super-fast and responsive, the hosting platform used to store and serve the HTML files should also be good. Superlative performance is possible if a well-tuned SSG website is hosted on multiple CDNs to take advantage of edge-caching.
  1. Limited dynamic content: An SSG site needs to be rebuilt and redeployed every time the content changes. The content displayed may be stale if the site has not been rebuilt and redeployed after any content change. This makes SSG unsuitable for highly dynamic content.

Client-Side Rendering

notion image
This implies that the user will see a blank screen for the entire duration between FP and FCP.

Import on Visibility

We can use the IntersectionObserver API, or use libraries such as react-lazyloador react-loadable-visibility
 
The different ways to load resources are, at a high-level:
  • Eager: load resource right away (the normal way of loading scripts)
  • Lazy (Route-based): load when a user navigates to a route or component
  • Lazy (On interaction): load when the user clicks UI (e.g Show Chat)
  • Lazy (In viewport): load when the user scrolls towards the component
  • Prefetch: load prior to needed, but after critical resources are loaded
  • Preload: eagerly, with a greater level of urgency
 
notion image
 
Dynamically import and add a component to a page.
notion image

Dynamic import and static import

notion image
 
notion image
SSR: @loadable/component
CSR: Suspend
 
OO Basics
  • Abstraction
  • Encapsulation
  • Polymorphism
  • Inheritance
OO Principles
  • Encapsulate what varies
  • Favor composition over inheritance
  • Program to interfaces, not implementations
  • Strive for loosely coupled designs between objects that interact.
  • Classes should be open for extension, but closed for modification.
OO Patterns
  • Strategy - defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

MVC

notion image
In the Model-View-Controller (MVC) architecture, a model may be observed by many views. A view typically observes a model and is notified when the model changes, allowing the view to update itself accordingly. Views are a visual representation of models that present a filtered view of their current state.
One may wonder where user interaction comes into play here. When users click on any elements within the view, it's not the view's responsibility to know what to do next. It relies on a controller to make this decision for it.

MVP

notion image
Model View Presenter (MVP) is a design pattern that derives from the MVC pattern. It aims to improve presentation logic. In MVP, the presenter observes models and updates views when models change. Essentially, the presenter binds models to views. This responsibility was previously held by controllers in MVC. Presenters retrieve data, manipulate it, and determine how the data should be displayed in the view.
In some implementations, the presenter also interacts with a service layer to persist data (models).Models may trigger events, but it is the presenter's role to subscribe to them so that it can update the view. In this passive architecture, there is no concept of direct data binding. Views expose setters that presenters can use to set data.

MVVM

notion image
The Model View ViewModel (MVVM) pattern is a variation of the MVC pattern that is commonly used in client-side frameworks such as Angular and Vue.js. In MVVM, the ViewModel acts as a mediator between the Model and View, exposing data from the Model to the View in a way that is easy to bind. The ViewModel also exposes methods that can be called from the View to update the Model.
Handlebars.js and Underscore.js are two popular templating libraries that allow developers to create reusable templates that can be easily populated with data. This makes it easy to create dynamic HTML content without resorting to manual string concatenation. Controllers are responsible for updating the model when the user manipulates the view, and decoupling models and views in the MVC pattern makes it easier to write unit tests and maintain the application over time.

Templating (Handlebars.js, Underscore.js)

manually create large blocks of HTML markup in memory through string concatenation.
  • Drawbacks
    • difficult to read
  • Advantages
    • dynamically load and store templates externally.

Controllers

Controllers are responsible for updating the model when the user manipulates the view.
 
For Backbone.js, only contain models and views, and views and routers act a little similar to a controller. not MVC/MVP nor MVVM framework. Normally call it MV*.
 
What Does MVC Give Us?
  • Easier overall maintenance. When updates need to be made to the application it is very clear whether the changes are data-centric, meaning changes to models and possibly controllers, or merely visual, meaning changes to views.
  • Decoupling models and views means that it is significantly more straight-forward to write unit tests for business logic.
  • Duplication of low-level model and controller code (i.e., what we may have been using instead) is eliminated across the application.
  • Depending on the size of the application and separation of roles, this modularity allows developers responsible for core logic and developers working on the user interfaces to work simultaneously.
 

Reference