Design Pattern is a widely acknowledged concept in the software engineering industry in terms of the benefits it brings to areas of code-reuse and maintainability. As a software developer, you likely stumble upon this term at one point. Unsurprisingly, without even knowing it, the chances are that you might have already implemented them somewhere in the development journey.
A design pattern is used to identify reusable solutions that can be applied to recurring problems that software developers commonly face during software design. They represent time-tested solutions and best practices adopted by object-oriented software developers over time.
This blog will be your guide to everything you need to know about popular JavaScript design patterns. The only prerequisite is that you should have basic knowledge of JavaScript and Object-Oriented Programming concepts.
Starting with the historical perspective, we will do an in-depth exploration of various common JavaScript design patterns from an object-oriented view. By the end, you will be accustomed to various JavaScript design patterns along with a basic idea of their implementation.
Let’s get started!
History of Design Pattern
Since its inception, the concept of design pattern has been around in the programming world. But it was not formalized till 1994 when one of the most influential work was published called “Design Patterns: Elements Of Reusable Object-Oriented Software” – written by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides – a group that became known as the Gang of Four (or GoF).
In this book, 23 object-oriented design patterns are featured. Since then, the “pattern approach” became popular in the software engineering industry, and after that, dozens of other patterns have been discovered.
What is a Design Pattern?
Design patterns can be considered pre-made blueprint applied to solve a particular design problem. It is not a finished piece of code that can be directly applied to your program. But rather, it is more like a template or description that can give you an idea of approaching a problem and inspiring solutions. Hence, the code for the two separate programming scenarios, implementing the same pattern can be different.
Now, if you are wondering how a pattern gets discovered, it’s simple. When the same solution gets repeated over and over, someone will eventually recognize it, put a name to it, and then describe the solution in detail. That’s how a pattern gets discovered. Definitely, they were not forged overnight.
A design pattern is often confused with algorithms.
Structure of a Design Pattern
As mentioned in the above section, the author of a design pattern provides documentation. Even though there is no strict agreement among the pattern community regarding the documentation template structure, the following are the sections that are usually present.
Some other sections are Applicability, Collaborations, Consequences, etc.
Why Patterns?
As previously mentioned, we already use patterns every day. They help us solve recurring design problems. But is it necessary to spend time learning them? Let’s look into a few key benefits that design patterns grant us.
1. Avoid reinventing the wheel:
Most of the frequently faced design problems already have a well-defined solution that is associated with a pattern. Patterns are proven solutions that can speed up development.
2. Codebase Maintenance:
Patterns help in implementing DRY(Do not Repeat Yourself) – the concept which helps to prevent your codebase from growing large and unwieldy.
3. Easily reused:
Reusing patterns assists in preventing minor subtle issues that can cause major problems in the application development process. This also improves code readability for coders and architects familiar with the patterns.
4. Enables efficient communication:
Patterns add to a developer’s vocabulary. This allows developers to communicate using well-known, well-understood names for software interactions, making communication faster.
5. Improve your object-oriented skills:
Now even if you never encounter any of these problems, learning patterns can give you insights into various approaches to solving problems using object-oriented principles.
Criticism of Patterns
Over time design patterns have also received a fair share of criticism. Let’s peek into the popular arguments against patterns.
1. Increases Complexity:
Inappropriate use of patterns creates undesired complexity. This is a problem suffered by many novices, who try to apply the pattern wherever they can think of, even in situations where simpler code would do just fine.
2. Reduced Relevance:
In “Design Patterns in Dynamic Languages,” Peter Norvig points out that over half of the design patterns in the 1994 book (written by GoF) are workarounds for missing language features. In many cases, patterns just become kludges that gave the programming language the much-needed super-abilities it lacked then.
As the language features, frameworks, and libraries evolved, there is no reason to use a few patterns anymore.
3. Lazy Design:
As suggested by Paul Graham in “Revenge of the Nerds” (2002), patterns are a form of lazy design, when the developer is not focused on the problem requirement at hand. Instead of creating a new and appropriate design for the problem, they might just reuse the existing design patterns because they think they should.
So far, we have seen what design patterns are and also discussed their advantages and disadvantages. Now it’s time for in-depth exploration of various types of JS design patterns available.
Note: In the upcoming sessions, we will explore object-oriented JavaScript implementations of both classical and modern design patterns. It is to be noted that a few of the classic design patterns mentioned in the GoF book have reduced relevance over time. Hence they will be omitted, and modern patterns from sources like Addy Osmani’s Learn JavaScript Design Patterns will be included.
JavaScript Design Patterns
JavaScript is one of the most in-demand programming languages for web development today. As we will be concentrating on JavaScript design patterns in this article, let’s just have a quick recap of essential JavaScript features that will aid in smoother understanding.
a) Flexible with programming styles
JavaScript has support for procedural, object-oriented, and functional programming styles.
b) Supports First-class Functions
This means functions can be passed as arguments to other functions just like a variable.
c) Prototype-based Inheritance
Though JavaScript supports objects, unlike other OOPs languages, JavaScript doesn’t have the concept of class or class-based inheritance in its basic form. Instead, it uses something called prototype-based or instance-based inheritance.
Note: In ES6, even though the keyword “class” is introduced, it still utilizes prototype-based inheritance in the hood.
To know more about defining “class” using JavaScript, do check out this useful post by Stoyan Stefanov on three ways to define a JavaScript class.
Categories of Design Pattern
Based on intent, the JavaScript design pattern can be categorized into 3 major groups:
a) Creational Design Pattern
These patterns focus on handling object creation mechanisms. A basic object creation approach in a program can lead to an added complexity. Creational JS design patterns aim to solve this problem by controlling the creation process.
Few patterns that fall under this category are – Constructor, Factory, Prototype, Singleton, etc.
b) Structural Design Patterns
These patterns are concerned with object composition. They explain simple ways to assemble objects and classes into larger structures. They help ensure that when one part of a system changes, the entire structure of the system doesn’t need to do the same, keeping them flexible and efficient.
Few patterns that fall under this category are – Module, Decorator, Facade, Adapter, Proxy, etc.
c) Behavioral Design Patterns
These patterns focus on improving the communication and assignment of responsibilities between dissimilar objects in a system.
Few patterns that fall under this category are – Chain of Responsibility, Command, Observer, Iterator, Strategy, Template, etc.
With this understanding of the categorization, let’s examine each JavaScript design pattern.
Creational Design Patterns
1. Constructor Pattern
The constructor pattern is one of the most simple, popular, and modern JS design patterns. As suggested by the name, the purpose of this pattern is to aid constructor creation.
In Addy’s words-
“A constructor is a special method used to initialize a newly created object once the memory has been allocated for it. In JavaScript, as almost everything is an object, we’re most often interested in object constructors.”
Example:
In the below code, we have defined a function/class Person with attributes name and age. The getDetails() method will print the name and age of the person in the format –
“Name is age years old!”
The syntax is given in 2 formats – (a) traditional function-based syntax and (b) EC6 class syntax.
Then, we instantiate an object for the class Person by invoking the constructor method using the new keyword and passing respective attribute values.
// a) Traditional "function" based syntax
function Person(name,age) {
this.name = name;
this.age = age;
this.getDetails = function () {
console.log(`${this.name} is ${this.age} years old!`);
}
}
// b) ES6 "class" syntax
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
this.getDetails = function () {
console.log(`${this.name} is ${this.age} years old!`);
};
}
}
//Creating new instance of Person
const personOne = new Person('John',20);
personOne.getDetails(); // Output - “John is 20years old!”
2. Factory Pattern
The Factory pattern is another creational pattern concerned with creating objects but using some sort of generic interface. According to GoF’s book, this pattern has the following responsibility.
“Define an interface for creating an object, but let subclasses decide which class to instantiate.”
This pattern is typically used when we need to handle object groups that share similar characters yet are different through appropriate custom calls. An example would bring more clarity.
Note: Though the definition particularly mentions that an interface needs to be defined, we don’t have interfaces in JavaScript. Therefore, we are going to implement it using an alternative way.
Example:
Here, the shapeFactory constructor is responsible for creating new objects of the constructors’ Rectangle, Square, and Circle. The createShape() inside shapeFactory takes in parameters, depending on which it delegates the responsibility of object instantiation to the respective class.
//Factory method for creating new shape instances
function shapeFactory(){
this.createShape = function (shapeType) {
var shape;
switch(shapeType){
case "rectangle":
shape = new Rectangle();
break;
case "square":
shape = new Square();
break;
case "circle":
shape = new Circle();
break;
default:
shape = new Rectangle();
break;
}
return shape;
}
}
// Constructor for defining new Rectangle
var Rectangle = function () {
this.draw = function () {
console.log('This is a Rectangle');
}
};
// Constructor for defining new Square
var Square = function () {
this.draw = function () {
console.log('This is a Square');
}
};
// Constructor for defining new Circle
var Circle= function () {
this.draw = function () {
console.log('This is a Circle);
}
};
var factory = new shapeFactory();
//Creating instance of factory that makes rectangle,square,circle respectively
var rectangle = factory.createShape('rectangle');
var square = factory.createShape('square');
var circle= factory.createShape('circle');
rectangle.draw();
square.draw();
circle.draw();
/*
OUTPUT
This is a Rectangle
This is a Square
This is a Circle
*/
3. Prototype Pattern
An object that supports cloning is called a prototype. Using the prototype pattern, we can instantiate new objects based on a template of an existing object through cloning.
As the prototype pattern is based on prototypal inheritance, we can utilize the native prototypical strengths of JavaScript. In the previous JS design patterns, we were trying to imitate features of other languages in JavaScript, which is not the case here.
Example:
Here we have a prototype class car, which is cloned to create a new object myCar using Object.create
feature defined by ES5 standard.
// Prototype Class
const car = {
noOfWheels: 4,
start() {
return 'started';
},
stop() {
return 'stopped';
},
};
//using Object.create to create clones - as recommended by ES5 standard
const myCar = Object.create(car, { owner: { value: 'John' } });
console.log(myCar.__proto__ === car); // true
4. Singleton Pattern
The singleton pattern is a creational JavaScript design pattern that restricts the instantiation of a class to a single object. It creates a new instance of the class if one doesn’t exist and if existing already, it simply returns a reference to it. It is also known as the Strict Pattern.
A singleton pattern solves two problems at the same time, violating the Single Responsibility Principle.
- Guarantees that there is only a single instance of a class.
- Provide a global access point to this instance.
A practical example would be a single database object shared by different parts of the program. There is no need to create a new instance of a database when one is already existing.
One drawback of the pattern is the difficulty associated with testing. There are hidden dependencies objects, which are difficult to single out to test.
Example:
//Singleton class
var Singleton = (function () {
var instance;
function createDBInstance() {
var object = new Object("I am the DataBase instance");
return object;
}
return {
getDBInstance: function () {
if (!instance) {
instance = createDBInstance();
}
return instance;
}
};
})();
function run() {
var instance1 = Singleton.getDBInstance();
var instance2 = Singleton.getDBInstance();
console.log("Same instance? " + (instance1 === instance2));
}
run(); // OUTPUT = "Same instance? true"
Structural Design Patterns
1. Adapter Pattern
The adapter is a structural JS design pattern that allows objects or classes with incompatible interfaces to collaborate. It matches interfaces of different classes or objects; therefore, they can work together despite incompatible interfaces. It is also referred to as the Wrapper pattern.
A real-world analogy would be trying to connect a projector to a laptop. The projector might have a VGA plug, and the laptop might have an HDMI plug. So we require an adapter that can make these two unrelated interfaces compatible.
This pattern will include a class that will be responsible for joining the incompatible interfaces/functionalities.
Example:
The below code shows an online flight ticket pricing calculation system. There is an old interface that performs pricing calculations in one way. There is a new, improved interface with additional features like user identification and improvised calculations.
An adapter class is introduced, which allows the client program to continue working without any API changes by matching the old interface with the new one.
// old interface
function TicketPrice() {
this.request = function(start, end, overweightLuggage) {
// price calculation code...
return "$150.34";
}
}
// new interface
function NewTicketPrice() {
this.login = function(credentials) { /* process credentials */ };
this.setStart = function(start) { /* set start point */ };
this.setDestination = function(destination) { /* set destination */ };
this.calculate = function(overweightLuggage) {
//price calculation code...
return "$120.20";
};
}
// adapter interface
function TicketAdapter(credentials) {
var pricing = new NewTicketPrice();
pricing.login(credentials);
return {
request: function(start, end, overweightLuggage) {
pricing.setStart(start);
pricing.setDestination(end);
return pricing.calculate(overweightLuggage);
}
};
}
var pricing = new TicketPrice();
var credentials = { token: "30a8-6ee1" };
var adapter = new TicketAdapter(credentials);
// original ticket pricing and interface
var price = pricing.request("Bern", "London", 20);
console.log("Old price: " + price);
// new ticket pricing with adapted interface
price = adapter.request("Bern", "London", 20);
console.log("New price: " + price);
2. Composite Pattern
Composite is a structural JavaScript design pattern that lets you compose objects into tree structures and then work with these structures as if they were individual objects. According to GoF’s book, this pattern composes objects into tree structures to represent part-whole hierarchies. It is also known as a partitioning JS design pattern.
The perfect example of this pattern would be tree control. The nodes of the tree either contain an individual object (leaf node) or a group of objects (a subtree of nodes).
Modern JS frameworks like React and Vue use the composite pattern to build user interfaces. The entire view is divided into components. Each component can contain multiple components. This method is preferred because of the ease of development and scalability compared to fewer monolithic objects. The composite pattern reduces the complexity of a system by allowing you to work with small objects and build them up into larger ones.
Example:
A file-folder(directory) structure is explained in the below code. Here a directory can have two types of entities, a file or another directory, which can contain files or directories and so on.
We have two classes – File and Directory. We can add or remove files in the Directory and also getFileName, and the display will list all file names inside the directory.
function File(name) {
this.name = name;
}
File.prototype.display = function () {
console.log(this.name);
}
function Directory(name) {
this.name = name;
this.files = [];
}
Directory.prototype.add = function (file) {
this.files.push(file);
}
Directory.prototype.remove = function (file) {
for (let i = 0, length = this.files.length; i < length; i++) {
if (this.files[i] === file) {
this.files.splice(i, 1);
return true;
}
}
return false;
}
Directory.prototype.getFileName = function (index) {
return this.files[index].name;
}
Directory.prototype.display = function() {
console.log(this.name);
for (let i = 0, length = this.files.length; i < length; i++) {
console.log(" ", this.getFileName(i));
}
}
directoryOne = new Directory('Directory One');
directoryTwo = new Directory('Directory Two');
directoryThree = new Directory('Directory Three');
fileOne = new File('File One');
fileTwo = new File('File Two');
fileThree = new File('File Three');
directoryOne.add(fileOne);
directoryOne.add(fileTwo);
directoryTwo.add(fileOne);
directoryThree.add(fileOne);
directoryThree.add(fileTwo);
directoryThree.add(fileThree);
directoryOne.display();
directoryTwo.display();
directoryThree.display();
/*
Directory One
File One
File Two
Directory Two
File One
Directory Three
File One
File Two
File Three
*/
3. Module Pattern
Module Pattern is another prevalent JavaScript design pattern for keeping our code clean, separated, and organized. A module is a piece of self-contained code that can be updated without affecting other components. As the concept of access modifier is not supported in JavaScript, the modules help in mimicking the behavior of private/public access hence providing encapsulation.
The typical code structure will be like this:
(function() {
// declare private variables and/or functions
return {
// declare public variables and/or functions
}
})();
Example:
Here we have the flexibility of renaming like we have renamed addAnimal to add. A point to be noted is that we can’t invoke removeAnimal from an outside environment as it is dependent on the private property container.
function AnimalContainter () {
//private variables and/or functions
const container = [];
function addAnimal (name) {
container.push(name);
}
function getAllAnimals() {
return container;
}
function removeAnimal(name) {
const index = container.indexOf(name);
if(index < 1) {
throw new Error('Animal not found in container');
}
container.splice(index, 1)
}
return {
public variables and/or functions
add: addAnimal,
get: getAllAnimals,
remove: removeAnimal
}
}
const container = AnimalContainter();
container.add('Hen');
container.add('Goat');
container.add('Sheep');
console.log(container.get()) //Array(3) ["Hen", "Goat", "Sheep"]
container.remove('Sheep')
console.log(container.get()); //Array(2) ["Hen", "Goat"]
4. Decorator Pattern
Decorators are a structural JS design pattern that aims to promote code reuse. This pattern allows behavior to be added to an individual object dynamically, without affecting the behavior of other objects from the same class. Decorators can also provide a flexible alternative to subclassing for extending functionality.
Since JavaScript allows us to add methods and properties to objects dynamically, implementing this JavaScript pattern is a very straight-forward process. Do check out Addy Osmani’s post to know more about Decorators.
Example:
Let’s take a look at the simple implementation.
// A vehicle constructor
function Vehicle( vehicleType ){
// some sane defaults
this.vehicleType = vehicleType || "car";
this.model = "default";
this.license = "00000-000";
}
// Test instance for a basic vehicle
var testInstance = new Vehicle( "car" );
console.log( testInstance );
// Outputs:
// vehicle: car, model:default, license: 00000-000
// Lets create a new instance of vehicle, to be decorated
var truck = new Vehicle( "truck" );
// New functionality we're decorating vehicle with
truck.setModel = function( modelName ){
this.model = modelName;
};
truck.setColor = function( color ){
this.color = color;
};
// Test the value setters and value assignment works correctly
truck.setModel( "CAT" );
truck.setColor( "blue" );
console.log( truck );
// Outputs:
// vehicle:truck, model:CAT, color: blue
// Demonstrate "vehicle" is still unaltered
var secondInstance = new Vehicle( "car" );
console.log( secondInstance );
// Outputs:
// vehicle: car, model:default, license: 00000-000
5. Facade Pattern
The facade pattern consists of a facade, which is an object that acts as a “front-face” for a much complex structural code. Developers normally use this pattern when a system is very complex or difficult to understand to provide a simpler interface to the client. This helps to create an abstraction layer between what is shown publicly and what is implemented behind the curtain.
Example:
Here Mortgage is a facade for Bank, Credit, and Background.
var Mortgage = function(name) {
this.name = name;
}
Mortgage.prototype = {
applyFor: function(amount) {
// access multiple subsystems...
var result = "approved";
if (!new Bank().verify(this.name, amount)) {
result = "denied";
} else if (!new Credit().get(this.name)) {
result = "denied";
} else if (!new Background().check(this.name)) {
result = "denied";
}
return this.name + " has been " + result +
" for a " + amount + " mortgage";
}
}
var Bank = function() {
this.verify = function(name, amount) {
// complex logic ...
return true;
}
}
var Credit = function() {
this.get = function(name) {
// complex logic ...
return true;
}
}
var Background = function() {
this.check = function(name) {
// complex logic ...
return true;
}
}
function run() {
var mortgage = new Mortgage("Joan Templeton");
var result = mortgage.applyFor("$100,000");
alert(result);
}
6. Proxy Pattern
As the name suggests, the Proxy Pattern provides a surrogate or placeholder for another object to control access, reduce cost, and reduce complexity. The proxy could interface to anything – a network connection, a large object in memory, a file, or some other resource that is expensive or impossible to duplicate.
Here, we will create a proxy object that ‘stands in’ for the original object. The proxy interface will be the same as that of the original object so that the client may not even be aware they are dealing with a proxy rather than the real object. In the proxy, extra functionality can be provided, for example, caching, checking some preconditions, etc.
There are three common situations in which the Proxy pattern is applicable.
- A virtual proxy is a placeholder for expensive to create or resource-intensive objects.
- A remote proxy controls access to the remote object.
- A protective proxy controls access rights to a sensitive master object. The caller’s access permissions are checked prior to forwarding the request.
Example:
The following code will aid you in getting a gist of Proxy implementation. We have an external API FlightListAPI for accessing Flight Details databases. We will create a proxy FlightListProxy which will act as the interface through which the client can access the API.
/* External API*/
var FlightListAPI = function() {
//creation
};
FlightListAPI.prototype = {
getFlight: function() {
// get master list of flights
console.log('Generating flight List');
},
searchFlight: function(flightDetails) {
// search through the flight list based on criteria
console.log('Searching for flight');
},
addFlight: function(flightData) {
// add a new flight to the database
console.log('Adding new flight to DB');
}
};
// creating the proxy
var FlightListProxy = function() {
// getting a reference to the original object
this.flightList = new FlightListAPI();
};
FlightListProxy.prototype = {
getFlight: function() {
return this.flightList.getFlight();
},
searchFlight: function(flightDetails) {
return this.flightList.searchFlight(flightDetails);
},
addFlight: function(flightData) {
return this.flightList.addFlight(flightData);
},
};
console.log("----------With Proxy----------")
const proxy = new FlightListProxy()
console.log(proxy.getFlight());
/*
OUTPUT
----------With Proxy----------
Generating flight List
*/
Behavioural Design Pattern
1. Chain of Responsibility Pattern
This is a behavioral JavaScript design pattern that creates a chain of receiver objects for a request. This pattern promotes loose coupling. We can avoid coupling the sender of a request to a receiver, and more than one receiver can handle the request.
The receiving objects will be linked together, and they can choose to act on the request and/or pass it to the next receiver object. It is also easy to add new receiver objects to the chain.
Event Handling in DOM is one implementation of the Chain of Responsibility pattern.
Once an event is fired, it propagates through the DOM hierarchy, calling every event handler it runs into until it finds the appropriate “event listener” and then acts on it.
Example:
Let us consider the scenario of an ATM. When we request an amount for withdrawal, the machine processes the request and dispends the amount as combinations of available note denominations ($100, $50, $20, $10, $5, $1).
In this code on requesting an amount, a Request object is created. This object then invokes a series of get calls, which are chained together, each one handling a particular denomination. Finally, the user receives the amount as a note combination which satisfies the amount value.
var Request = function(amount) {
this.amount = amount;
console.log("Request Amount:" +this.amount);
}
Request.prototype = {
get: function(bill) {
var count = Math.floor(this.amount / bill);
this.amount -= count * bill;
console.log("Dispense " + count + " $" + bill + " bills");
return this;
}
}
function run() {
var request = new Request(378); //Requesting amount
request.get(100).get(50).get(20).get(10).get(5).get(1);
}
2. Command Pattern
Command Pattern is a behavioral JS design pattern that aims to encapsulate actions or operations as objects. This pattern is useful in scenarios where we want to decouple or split the objects executing the commands from objects issuing the commands. Command objects allow you to centralize the processing of these actions/operations.
The four participants involved in the command pattern are command, receiver, invoker, and client.
- Command – A command object knows about the receiver and invokes a method of the receiver. Values for parameters of the receiver method are stored in the command.
- Client – The client’s responsibility is to create the command object and pass it to the invoker.
- Invoker – The invoker receives the command object from the client, and it’s only responsibility is to call (or invoke) a command.
- Receiver – Then, the receiver receives the command and looks for a method to call based on the received command.
Example:
In our example, the calculator object contains four methods – add, subtract, divide, and multiply. Command objects define a method execute, which has the responsibility of invoking a method.
var calculator = {
add: function(x, y) {
return x + y;
},
subtract: function(x, y) {
return x - y;
},
divide: function(x,y){
return x/y;
},
multiply: function (x,y){
return x*y;
}
}
var manager = {
execute: function(name, args) {
if (name in calculator) {
return calculator[name].apply(calculator, [].slice.call(arguments, 1));
}
return false;
}
}
console.log(manager.execute("add", 5, 2)); // prints 7
console.log(manager.execute("multiply", 2, 4)); // prints 8
3. Observer Pattern
The Observer is a behavioral JS design pattern that lets you define a subscription mechanism to notify multiple objects (observers) about any events that happen to the object (subject) they’re observing. This pattern is also called Pub/Sub, short for Publication/Subscription. It defines a one-to-many dependency between objects, promotes loose coupling, and facilitates good object-oriented design.
The observer pattern is the foundation of event-driven programming. We write event handler functions that will be notified when a certain event fires.
Example:
We have set up a Subject function Click and extended it using the prototype. We have created methods to subscribe and unsubscribe objects to the Observer collection, which is handled by the clickHandler function. Also, there is a fire method to propagate any changes in the Subject class object to the subscribed Observers.
function Click() {
this.observers = []; // observers
}
Click.prototype = {
subscribe: function(fn) {
this.observers.push(fn);
},
unsubscribe: function(fn) {
this.observers = this.observers.filter(
function(item) {
if (item !== fn) {
return item;
}
}
);
},
fire: function(o, thisObj) {
var scope = thisObj;
this.observers.forEach(function(item) {
item.call(scope, o);
});
}
}
function run() {
var clickHandler = function(item) {
console.log("Fired:" +item);
};
var click = new Click();
click.subscribe(clickHandler);
click.fire('event #1');
click.unsubscribe(clickHandler);
click.fire('event #2');
click.subscribe(clickHandler);
click.fire('event #3');
}
/* OUTPUT:
Fired:event #1
Fired:event #3
*/
4. Iterator Pattern
The Iterator Pattern lets you access and traverses through elements of an aggregate object (collection) sequentially without exposing its underlying representation. This pattern allows JavaScript developers to design looping constructs that are far more flexible and sophisticated. In ES6, Iterator and Generators are introduced, which further aids in the Iteration pattern implementation.
Example:
This is a simple straight-forward code for front-to-back iteration. We have defined two methods for the Iterator – hasNext() and next().
const items = [1,"hello",false,99.8];
function Iterator(items){
this.items = items;
this.index = 0; // to start from beginning position of array
}
Iterator.prototype = {
// returns true if a next element is available
hasNext: function(){
return this.index < this.items.length;
},
//returns next element
next: function(){
return this.items[this.index++]
}
}
//Instantiate object for Iterator
const iterator = new Iterator(items);
while(iterator.hasNext()){
console.log(iterator.next());
}
/*
OUTPUT
1
hello
false
99.8
*/
5. Template Pattern
The template pattern defines the skeleton of an algorithm in operation in terms of some high-level steps. These steps are themselves implemented by additional helper methods in the same class as the template method. The objects that implement these steps retain the original structure of the algorithm but have the option to redefine or adjust certain steps.
Example:
Here we have an abstract class datastore that offers an interface to implement the template method by defining primitive steps for an algorithm. And we have a concrete MySQL class, which implements the primitive steps defined in the abstract class.
// implement template method
var datastore = {
process: function() {
this.connect();
this.select();
this.disconnect();
return true;
}
};
function inherit(proto) {
var F = function() { };
F.prototype = proto;
return new F();
}
function run() {
var mySql = inherit(datastore);
// implement template steps
mySql.connect = function() {
console.log("MySQL: connect step");
};
mySql.select = function() {
console.log("MySQL: select step");
};
mySql.disconnect = function() {
console.log("MySQL: disconnect step");
};
mySql.process();
}
run();
/*
MySQL: connect step
MySQL: select step
MySQL: disconnect step
*/
6. Strategy Pattern
Strategy Patterns allows one of a family of algorithms to be selected on-the-fly at runtime. The pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable at run-time without client interference.
Example:
We have created a class Shipping which encapsulates all possible strategies for shipping a parcel – FedEx, UPS, and USPS. Using this pattern, we can swap the strategy during runtime and generate appropriate output.
//Strategy1
function FedEx(){
this.calculate = package => {
//calculations happen here..
return 2.99
}
}
//Strategy2
function UPS(){
this.calculate = package => {
//calculations happen here..
return 1.59
}
}
//Strategy3
function USPS(){
this.calculate = package => {
//calculations happen here..
return 4.5
}
}
// encapsulation
function Shipping(){
this.company = "";
this.setStrategy = (company) => {
this.company=company;
}
this.calculate = (package) =>{
return this.company.calculate(package);
}
}
//usage
const fedex = new FedEx();
const ups = new UPS();
const usps = new USPS();
const package = { from: 'Alabama',to:'Georgia',weight:1.5};
const shipping = new Shipping();
shipping.setStrategy(fedex);
console.log("Fedex:" +shipping.calculate(package)); // OUTPUT => "Fedex:2.99"
Anti-Patterns
While it’s important to know about design patterns, it is equally important to know about Anti-Patterns. If a design pattern can be considered as a best practice, an anti-pattern represents just the opposite.
The term anti-pattern was coined in 1995 by Andrew Koenig. According to Koenig, an anti-pattern is a bad solution to a particular problem that resulted in a bad situation.
Few examples of anti-patterns in JavaScript are the following:
- Polluting the global namespace by defining a large number of variables in the global context
- Passing strings rather than functions to either setTimeout or setInterval as this triggers the use of eval() internally.
- Modifying the Object class prototype (this is a particularly bad anti-pattern)
To summarize, an anti-pattern is a bad design that is worthy of documenting. Knowledge about them will help you recognize such anti-patterns in your code and hence improve code quality.
Applying Design Patterns and Testing
Once a design pattern has been implemented and validated, we need to ensure that it works across multiple browser and browser versions seamlessly. LambdaTest is a cross browser testing platform for manual and automated cross browser testing. It includes more than 2000 real browsers & browser versions, and allows browser compatibility regression testing across all major browsers and browser versions.
You can also leverage LT Browser, a developer friendly tool to perform a detailed analysis of the responsiveness of your design patterns across popular devices and viewports.
Conclusion
Design patterns represent some of the best practices adopted by experienced object-oriented software developers. They are time-tested solutions for various software design problems. In this article, we have explored common design patterns in JavaScript. Additionally, we also briefly discussed Anti-Patterns and how we can test websites with such patterns on LambdaTest platform. Hopefully, this chapter will enable you to get accustomed to various JavaScript design patterns. To dive deeper into the concept, do checkout Learn JavaScript Design Patterns by Addy Osmani.
Top comments (6)
If there is one thing I'd love to see more here then it's design patterns. Code or architectural, they are important and there's too much noise about code formatting or frameworks and too little material on design patterns.
Small advice though : would be great to break this into a proper series. Each segment, including the history bit could be its own short article. Would make it so much easier to follow (limited time and attention span and all)
Most design patterns are tied to a given paradigm, language (as the OP said, to workaround language weaknesses, flaws or lack if features) and even outdated in newer versions (e.g. Java supports lambdas since quite a few by now).
You can spend some time learning OOP design patterns, then switch to FP rendering most of them totally unnecessary.
It's not that FP can solve all things with built-in features, as example, side-effects are a pain in pure FP, hence you got monads (which one can agree that are a sort of design pattern in FP).
Despite that, on most of the rest is like... Factory pattern? Here yo got a lambda.
In multi-paradigm languages like JavaScript it's totally absurd to apply design patterns because you can use the best paradigm in each situation, hence using design patterns can be either overengineering or lack of understanding of core language features or programming paradigms.
Would be easier to read if was multiple short articles on different design patterns and how to apply them!
Great read, I saved it for reading later, its only a bit too long..
One correction I would like to suggest is about the Observer pattern, as its not the same as the Pub/Sub pattern, Pub / Sub pattern is about decoupling the subscribers and publishers of events, and is much more involved...
Best article to understand patterns and their relations. Thank you 🙏
It is a excellent article, gratz.