Design patterns are advanced object-oriented solutions to commonly occurring software problems. Patterns are about reusable designs and interactions of objects. Each pattern has a name and becomes part of a vocabulary when discussing complex design solutions.
“Each pattern describes a problem which occurs over and over again … and then describes the core of the solution to that problem, in such a way that you can use this solution a million times over, without doing it the same way twice.” – Christopher Alexander
The 23 Gang of Four (GoF) patterns are generally considered the foundation for all other patterns. There are 23 different known design patterns, and they can be separated into three categories by purpose: Creational, Structural, and Behavioral (see below for a complete list).
C - Creational
S - Structural
B - Behavioral
Why Design Patterns?
We already use design 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.
Do not Repeat Yourself (DRY) Patterns help in implementing DRY – this helps to keep our codebase lean.
Reusability Code reusability helps to minimise bugs and test functionality in a clean and concise manner. This also helps to improve code readability.
Proven Solutions You can be assured that they were revised multiple times and optimizations were probably implemented.
Eases communication: When developers are familiar with design patterns, they can more easily communicate with one another about potential solutions to a given problem.
Creational Design Patterns
Factory Method
A Factory Pattern or Factory Method Pattern says that just define an interface or abstract class for creating an object but let the subclasses decide which class to instantiate. In other words, subclasses are responsible to create the instance of the class.
A Factory Method creates new objects as instructed by the client. One way to create objects in JavaScript is by invoking a constructor function with the new operator. There are maybe scenarios, where the client does not know which one of several candidates objects to instantiate. The Factory Method allows the client to delegate object creation while still retaining control over which type to instantiate.
The factory method will simply set up and return the new object when you call the function. Check out this example:
const VEHICLE_TYPE = {
CAR: 'car',
VAN: 'van',
};
const VEHICLE_COLOR = {
WHITE: 'white',
SILVER: 'silver',
ORANGE: 'orange',
};
const VEHICLE_TRANSMISSION = {
MANUAL: 'manual',
AUTO: 'auto',
};
class Car {
constructor(options) {
this.vehicleType = options.vehicleType;
this.doors = options.doors ?? 4;
this.transmission = options.transmission ?? VEHICLE_TRANSMISSION.AUTO;
this.color = options.color ?? VEHICLE_COLOR.SILVER;
}
}
class Van {
constructor(options) {
this.vehicleType = options.vehicleType;
this.transmission = options.transmission ?? VEHICLE_TRANSMISSION.MANUAL;
this.color = options.color ?? VEHICLE_COLOR.ORANGE;
}
}
class VehicleFactory {
createVehicle(options) {
switch (options.vehicleType) {
case VEHICLE_TYPE.CAR:
this.vehicle = Car;
break;
case VEHICLE_TYPE.VAN:
this.vehicle = Van;
break;
}
return new this.vehicle(options);
}
}
const carFactory = new VehicleFactory();
const car = carFactory.createVehicle({
vehicleType: VEHICLE_TYPE.CAR,
color: VEHICLE_COLOR.WHITE,
doors: 3,
});
const van = carFactory.createVehicle({
vehicleType: VEHICLE_TYPE.VAN,
color: VEHICLE_COLOR.ORANGE,
doors: 5,
});
// true
console.log(car.vehicleType === VEHICLE_TYPE.CAR);
// { vehicleType: 'car', color: 'white', doors: 5, transmission: 'auto' }
console.log(car);
// true
console.log(van.vehicleType === VEHICLE_TYPE.VAN);
// { vehicleType: 'van', color: 'orange', doors: 5, transmission: 'manual' }
console.log(van);
Extensibility is the key objective of the Factory Method. Factory Methods are often used in applications that maintain, manage, or even manipulate different collections of objects but at the same time have many characteristics (i.e. methods and properties) in common. An example would be a collection of documents with a mix of markdown documents, pdf documents, and RTF documents.
Abstract Factory
Abstract Factory creates groups of common objects without specifying their concrete classes. There are situations when we have some of the same types of factories and we want to encapsulate the logic of choice, what of the factories use to a given task.
Suppose we have two Abstract Factories whose task is to create vehicles such as cars and vans. One is the Car Factory which creates vehicles that are cars and the other is the Van Factory which creates vehicles that are vans. Both Factories creates vehicles, but they differ in what they do, which is their common theme. This is an implementation of the Abstract Factory pattern.
class Vehicle {
startEngine() {}
}
class Car extends Vehicle {
startEngine() {
console.log('Car: Start engine');
}
}
class Van extends Vehicle {
startEngine() {
console.log('Van: Start engine');
}
}
class VehicleFactory {
make() {}
}
class CarFactory extends VehicleFactory {
makeCar() {
console.log('Car created');
return new Car();
}
}
class VanFactory extends VehicleFactory {
makeVan() {
console.log('Van created');
return new Van();
}
}
const carFactory = new CarFactory();
const car = carFactory.makeCar();
car.startEngine();
const vanFactory = new VanFactory();
Over time the Abstract Factory and Factory Method patterns have merged into a more general pattern called Factory. A Factory is simply an object that creates other objects.
Builder
The Builder is a creational design pattern that allows you to construct a complex object by specifying the type and content only. Construction details are hidden from the client entirely.
The biggest reason for using the Builder pattern is to simplify client code that creates complex objects. The client can still direct the steps taken by the Builder without knowing how the actual work is implemented under the hood. Builders frequently encapsulate the construction of Composite objects (another GoF design pattern) because the code involved is often complex and repetitive.
Let's look at an example to demonstrate this pattern using a Vehicle Builder class:
const VEHICLE_TYPE = {
CAR: 'car',
VAN: 'van',
};
const VEHICLE_COLOR = {
WHITE: 'white',
SILVER: 'silver',
ORANGE: 'orange',
};
const VEHICLE_TRANSMISSION = {
MANUAL: 'manual',
AUTO: 'auto',
};
class Vehicle {
spec() {
return `Type: ${this.vehicleType},
Color: ${this.color},
Doors: ${this.doors},
Transmission: ${this.transmission}`;
}
}
class VehicleBuilder {
constructor(vehicle = new Vehicle()) {
this.vehicle = vehicle;
return this;
}
ofType(vehicleType) {
this.vehicle.vehicleType = vehicleType;
return this;
}
addColor(color) {
this.vehicle.color = color;
return this;
}
addDoors(doors) {
this.vehicle.doors = doors;
return this;
}
addTransmission(transmission) {
this.vehicle.transmission = transmission;
return this;
}
build() {
return this.vehicle;
}
}
const vehicleBuilder = new VehicleBuilder();
const vehicle = vehicleBuilder
.ofType(VEHICLE_TYPE.CAR)
.addColor(VEHICLE_COLOR.SILVER)
.addDoors(4)
.addTransmission(VEHICLE_TRANSMISSION.AUTO)
.build();
console.log(vehicle.spec());
The code above has a Vehicle (the Director) and a builder object: VehicleBuilder. The Vehicle's construct method accepts a Builder instance which it then takes through a series of assembly steps: ofType, addColor, and addTransmission. The Builder's get method returns the newly assembled product (Vehicle).
Usually it is the last step (build) that returns the newly created object which makes it easy for a Builder to participate in fluent interfaces in which multiple method calls, separated by dot operators, are chained together.
Prototype
The Prototype is a creational design pattern that is used when the type of objects to create is determined by a prototypical instance, which is cloned to produce new objects.
The Prototype Pattern creates new objects that are initialized with values it copied from a prototype. JavaScript being a prototypal language uses this pattern for creating new objects and their prototypes.
An example of where the Prototype pattern is useful is the initialization of business objects with values that match the default values in the database. The prototype object holds the default values that are copied over into a newly created business object.
const VEHICLE_TYPE = {
CAR: 'car',
VAN: 'van',
};
class Vehicle {
constructor(vehicleType, model) {
this.vehicleType = vehicleType;
this.model = model;
}
spec() {
console.log(`Type: ${this.vehicleType}, model: ${this.model}`);
}
clone() {
return new Vehicle(this.vehicleType, this.model);
}
setVehicleType(vehicleType) {
this.vehicleType = vehicleType;
}
setModel(model) {
this.model = model;
}
}
const car = new Vehicle(VEHICLE_TYPE.CAR, 'X3');
car.spec();
const van = car.clone();
van.spec();
van.setVehicleType(VEHICLE_TYPE.VAN);
van.setModel('X10');
van.spec();
The Prototype pattern delegates the cloning process to the actual objects that are being cloned. The pattern declares a common interface for all objects that support cloning. This interface lets you clone an object without coupling your code to the class of that object. Usually, such an interface contains just a single clone
method.
Singleton
The Singleton is a creational design pattern that restricts the instantiation of a class to one “single” instance. This is useful when exactly one object is needed to coordinate actions across the system.
Singletons limit the need for global variables which is particularly important in JavaScript because it reduces the associated risk of name collisions and namespace pollution. The Module pattern is JavaScript's manifestation of the Singleton pattern.
For example, let's create a Singleton Vehicle class:
const VEHICLE_TYPE = {
CAR: 'car',
VAN: 'van',
};
class Vehicle {
constructor(vehicleType) {
const instance = this.constructor.instance;
if (instance) {
return instance;
}
this.vahicleType = vehicleType;
this.constructor.instance = this;
}
spec() {
console.log(this.vahicleType);
}
}
const car1 = new Vehicle(VEHICLE_TYPE.CAR);
const car2 = new Vehicle(VEHICLE_TYPE.VAN);
console.log(car1 === car2); // true
console.log(car1.spec()); // car
console.log(car2.spec()); // car (not van!)
Singleton is a manifestation of a common JavaScript pattern called the Module pattern. The module is the basis of all popular JavaScript libraries and frameworks (React, Vue, Angular, etc.)
Creational Design Patterns
Behavioral Design Patterns focus on improving the communication between disperate objects in the system.
Chain of Responsibility
The Chain of Responsibility is a behavioral design pattern consisting of a source of command objects and a series of processing objects.
It provides a chain of loosely coupled objects one of which can satisfy a request. This pattern is essentially a linear search for an object that can handle a particular request.
The example below demonstrates a solution for dispensing money from an ATM machine. It breaks down the combination of bank notes ($100, $50, $20, $10, \$5) that satisfies the request.
class ATM {
constructor(amount) {
this.amount = amount;
console.log(`Request: $${amount}\n`);
}
request(bill) {
const count = Math.floor(this.amount / bill);
this.amount -= count * bill;
console.log(`Dispense: ${count} $${bill} bills`);
return this;
}
}
const atm = new ATM(425);
atm.request(100).request(50).request(20).request(10).request(5);
An example of the chain of responsibility pattern is event-bubbling in which an event propagates through a series of nested controls one of which may choose to handle the event.
Command
The Command pattern is a behavioral design pattern in which an object is used to encapsulate all information needed to perform an action or trigger an event at a later time.
This information includes:
- The method name
- The object that owns the method and
- Values for the method parameters.
The Command pattern encapsulates actions as objects. Command objects allow for loosely coupled systems by separating the objects that issue a request from the objects that actually process the request. These requests are called events and the code that processes the requests are called event handlers.
In our example we have a calculator with 6 operations: add, subtract, multiply, divide, undo and execute. Each operation is encapsulated by a Command object.
class Calculator {
constructor() {
this.value = 0;
this.history = [];
}
executeCommand(command) {
this.value = command.execute(this.value);
this.history.push(command);
}
undo() {
const command = this.history.pop();
this.value = command.undo(this.value);
}
add(value) {
this.value = this.value + value;
}
subtract(value) {
this.value = this.value - value;
}
multiply(value) {
this.value = this.value * value;
}
divide(value) {
this.value = this.value / value;
}
}
class AddCommand {
constructor(value) {
this.valueToAdd = value;
}
execute(currentValue) {
return currentValue + this.valueToAdd;
}
undo(currentValue) {
return currentValue - this.valueToAdd;
}
}
class SubtractCommand {
constructor(value) {
this.valueToSubtract = value;
}
execute(currentValue) {
return currentValue - this.valueToSubtract;
}
undo(currentValue) {
return currentValue + this.valueToSubtract;
}
}
class MultiplyCommand {
constructor(value) {
this.valueToMultiply = value;
}
execute(currentValue) {
return currentValue * this.valueToMultiply;
}
undo(currentValue) {
return currentValue / this.valueToMultiply;
}
}
class DivideCommand {
constructor(value) {
this.valueToDivide = value;
}
execute(currentValue) {
return currentValue / this.valueToDivide;
}
undo(currentValue) {
return currentValue * this.valueToDivide;
}
}
const calculator = new Calculator();
calculator.executeCommand(new AddCommand(10));
calculator.executeCommand(new MultiplyCommand(2));
calculator.executeCommand(new SubtractCommand(10));
calculator.executeCommand(new DivideCommand(2));
console.log(calculator.value); // 5
calculator.undo();
console.log(calculator.value); // 10
JavaScript's function objects and callbacks are native command objects. They can be passed around like objects.
Interpreter
The Interpreter is a behavioral design pattern that offers a scripting language for end-users to customize their solution.
Some applications are so complex that they require advanced configuration. You could offer a basic scripting language that allows the end-user to manipulate your application through simple instructions. The Interpreter pattern solves this particular problem by introducing a simple scripting language.
The example below is to build an interpreter which translates roman numerals to decimal numbers.
class Context {
constructor(input) {
this.input = input;
this.output = 0;
}
startsWith(str) {
return this.input.substr(0, str.length) === str;
}
}
class Expression {
constructor(name, one, four, five, nine, multiplier) {
this.name = name;
this.one = one;
this.four = four;
this.five = five;
this.nine = nine;
this.multiplier = multiplier;
}
interpret(context) {
if (context.input.length == 0) {
return;
} else if (context.startsWith(this.nine)) {
context.output += 9 * this.multiplier;
context.input = context.input.substr(2);
} else if (context.startsWith(this.four)) {
context.output += 4 * this.multiplier;
context.input = context.input.substr(2);
} else if (context.startsWith(this.five)) {
context.output += 5 * this.multiplier;
context.input = context.input.substr(1);
}
while (context.startsWith(this.one)) {
context.output += 1 * this.multiplier;
context.input = context.input.substr(1);
}
}
}
const roman = 'MCMLXXVII';
const context = new Context(roman);
const tree = [];
tree.push(new Expression('thousand', 'M', ' ', ' ', ' ', 1000));
tree.push(new Expression('hundred', 'C', 'CD', 'D', 'CM', 100));
tree.push(new Expression('ten', 'X', 'XL', 'L', 'XC', 10));
tree.push(new Expression('one', 'I', 'IV', 'V', 'IX', 1));
for (let i = 0, len = tree.length; i < len; i++) {
tree[i].interpret(context);
}
console.log(roman + ' = ' + context.output); // 1977
The Context object - maintains the input which is the roman numeral and the resulting output as it is being parsed and interpreted. The Expression object - represents the nodes in the grammar tree and it supports the interpret method.
Iterator
Iterator is a behavioral design pattern that accesses the elements of an object without exposing its underlying representation.
It is a common task in programming to traverse and manipulate a collection of objects. These collections may be stored as an array or perhaps something more complex, such as a tree or graph structure. In addition, you may need to access the items in the collection in a certain order, such as, front to back, back to front, depth-first (as in tree searches), skip evenly numbered objects, etc.
The Iterator
object maintains a reference to the collection and the current position. It also implements the 'standard' Iterator interface with methods like: first
, next
, hasNext
, reset
, and each
.
class Iterator {
constructor(items) {
this.index = 0;
this.items = items;
}
first() {
this.reset();
return this.next();
}
next() {
return this.items[this.index++];
}
hasNext() {
return this.index <= this.items.length;
}
reset() {
this.index = 0;
}
each(callback) {
for (let item = this.first(); this.hasNext(); item = this.next()) {
callback(item);
}
}
}
const items = ['Apple', 'Banana', 'Pear', 'Orange'];
const iter = new Iterator(items);
iter.each(function (item) {
console.log(item);
});
The each
method internally uses the for
loop which uses the first
, hasNext
, and next
methods to control the iteration. But to the client, the syntax has been greatly simplified.
Mediator
The Mediator is a behavioral design pattern that adds a third-party object to control the interaction between two objects. It allows loose coupling between classes by being the only class that has detailed knowledge of their methods.
The Mediator pattern provides central authority over a group of objects by encapsulating how these objects interact. This model is useful for scenarios where there is a need to manage complex conditions in which every object is aware of any state change in any other object in the group.
Let’s look at an example where we have four participants that are joining in a chat session by registering with a Chatroom
(the Mediator). Each participant is represented by a Participant
object. Participants send messages to each other and the Chatroom manages the routing.
class Participant {
constructor(name) {
this.name = name;
this.chatroom = null;
}
send(message, to) {
this.chatroom.send(message, this, to);
}
receive(message, from) {
console.log(from.name + ' -> ' + this.name + ': ' + message);
}
}
class Chatroom {
constructor() {
this.participants = {};
}
join(participant) {
this.participants[participant.name] = participant;
participant.chatroom = this;
}
send(message, from, to) {
if (to) {
// single message
to.receive(message, from);
} else {
// broadcast message
for (const key in this.participants) {
if (this.participants[key] !== from) {
this.participants[key].receive(message, from);
}
}
}
}
}
const michael = new Participant('Michael');
const jane = new Participant('Jane');
const paul = new Participant('Paul');
const emily = new Participant('Emily');
const chatroom = new Chatroom();
chatroom.join(michael);
chatroom.join(jane);
chatroom.join(paul);
chatroom.join(emily);
michael.send('Hey Jane, how was your weekend?');
jane.send('Hey Michael, it was great. How was yours?', michael);
paul.send('Hey peeps, how are we going?');
emily.send('Great Paul. Thank you', paul);
Another example of Mediator is that of a control tower on an airport coordinating arrivals and departures of aeroplanes.
Memento
The Memento pattern is a behavioral design pattern that restoration of an object to its previous state as well as provides temporary storage. The mechanism in which you store the object’s state depends on the required duration of persistence, which may vary.
The memento pattern is implemented with three objects: the originator, a caretaker and a memento.
Let’s look at them individually:
- Originator
- Implements interface to create and restore mementos of itself
- The object whose state is temporary being saved and restored
- Memento
- The internal state of the Originator object in some storage format
- CareTaker
- Responsible for storing mementos
- Just a repository; does not make changes to mementos
In the example code below, there are two people named John and Emily are being created using the Person
constructor function. Next, their mementos are created which are maintained by the LocalStorage
object.
class Person {
constructor(name) {
this.name = name;
}
hydrate() {
const memento = JSON.stringify(this);
return memento;
}
dehydrate(memento) {
const m = JSON.parse(memento);
this.name = m.name;
}
}
// CareTaker
class LocalStorage {
constructor() {
this.mementos = {};
}
add(key, memento) {
this.mementos[key] = memento;
}
get(key) {
return this.mementos[key];
}
}
const john = new Person('John Doe');
const emily = new Person('Emily Smith');
const localStorage = new LocalStorage();
// Save state
localStorage.add(1, john.hydrate());
localStorage.add(2, emily.hydrate());
// Assign false names
john.name = 'Michael Knight';
emily.name = 'Stephanie Mason';
// Restore original state
john.dehydrate(localStorage.get(1));
emily.dehydrate(localStorage.get(2));
console.log(john.name);
console.log(emily.name);
We assign John and Emily fake names before restoring them from their mementos. Following the restoration, we confirm that the person objects are back to their original state with valid names.
Observer
The Observer pattern is a behavioral design pattern that allows a number of observer objects to see an event. In another word, the Observer pattern offers a subscription model in which objects subscribe to an event and get notified when the event occurs.
The observer pattern in which an object, named the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods.
Let’s look at the participating objects individually:
- Subject
- maintains list of observers. Any number of Observer objects may observe a Subject
- implements an interface that lets observer objects subscribe or unsubscribe
- sends a notification to its observers when its state changes
Event
class in the example code
- Observers
- has a function signature that can be invoked when Subject changes (i.e. event occurs)
handleClick
function in the example code
In our example below, the Event
class represents the Subject. The handleClick
function is the subscribing Observer. This handler subscribes, unsubscribes, and then subscribes it while events are firing. It gets notified only of events #1 and #3.
const window = global; // because we run in Node.js not the browser
class Event {
constructor() {
this.handlers = []; // observers
}
subscribe(fn) {
this.handlers.push(fn);
}
unsubscribe(fn) {
this.handlers = this.handlers.filter(function (item) {
if (item !== fn) {
return item;
}
});
}
fire(eventDetails, context) {
const scope = context || window;
this.handlers.forEach(function (item) {
item.call(scope, eventDetails);
});
}
}
const handleClick = function (item) {
console.log('fired: ' + item);
};
const event = new Event();
event.subscribe(handleClick);
event.fire('event #1');
event.unsubscribe(handleClick);
event.fire('event #2');
event.subscribe(handleClick);
event.fire('event #3');
Notice that the fire
method accepts two arguments. The first one has details about the event and the second one is the context, that is, the this
value for when the event handlers are called. If no context is provided this
will be bound to the global object (window).
State
The State pattern is a behavioral software design pattern that allows an object to alter its behavior when its internal state changes. This pattern is close to the concept of finite-state machines.
The State pattern provides state-specific logic to a limited set of objects in which each object represents a particular state.
Our example is a traffic light with 3 different states: Red
, Yellow
and Green
, each with its own set of rules. The rules go like this: Say the traffic light is Red. After a delay the Red state changes to the Green state. Then, after another delay, the Green state changes to the Yellow state. After a very brief delay, the Yellow state is changed to Red. And on and on.
class Red {
constructor(light) {
this.light = light;
}
go() {
console.log('Red -> for 1 minute');
light.change(new Green(light));
}
}
class Yellow {
constructor(light) {
this.light = light;
}
go() {
console.log('Yellow -> for 10 seconds');
light.change(new Red(light));
}
}
class Green {
constructor(light) {
this.light = light;
}
go() {
console.log('Green -> for 1 minute');
light.change(new Red(light));
}
}
class TrafficLight {
constructor() {
this.count = 0;
this.currentState = new Red(this);
}
change(state) {
// Limits number of changes
if (this.count++ >= 10) return;
this.currentState = state;
this.currentState.go();
}
start() {
this.currentState.go();
}
}
const light = new TrafficLight();
light.start();
Two other examples where the State pattern is useful include: elevator logic which moves riders up or down depending on certain complex rules that attempt to minimize wait and ride times, and vending machines that dispense products when a correct combination of coins is entered.
Strategy
The Strategy pattern is a behavioral design pattern that enables selecting an algorithm at runtime. Instead of implementing a single algorithm directly, code receives run-time instructions as to which in a family of algorithms to use.
The Strategy pattern encapsulates alternative algorithms (or strategies) for a particular task. It allows a method to be swapped out at runtime by any other method (strategy) without the client realizing it. Essentially, Strategy is a group of algorithms that are interchangeable.
In this example, we have a product order that needs to be shipped from a warehouse to a customer. Different shipping companies are evaluated to determine the best price. This can be useful with shopping carts where customers select their shipping preferences and the selected Strategy returns the estimated cost.
class Shipping {
constructor() {
this.company = '';
}
setStrategy(company) {
this.company = company;
}
calculate(pkg) {
return this.company.calculate(pkg);
}
}
class UPS {
calculate({ weight }) {
const basePrice = 15.26;
const total = basePrice * weight;
return total;
}
}
class USPS {
calculate({ weight }) {
const basePrice = 21.21;
const total = basePrice * weight;
return total;
}
}
class Fedex {
calculate({ weight }) {
const basePrice = 17.54;
const total = basePrice * weight;
return total;
}
}
const package = { from: '82563', to: '10400', weight: 2 };
const ups = new UPS();
const usps = new USPS();
const fedex = new Fedex();
const shipping = new Shipping();
shipping.setStrategy(ups);
console.log('UPS Strategy: $' + shipping.calculate(package));
shipping.setStrategy(usps);
console.log('USPS Strategy: $' + shipping.calculate(package));
shipping.setStrategy(fedex);
console.log('Fedex Strategy: $' + shipping.calculate(package));
In JavaScript the Strategy pattern is widely used as a plug-in mechanism when building extensible frameworks.
Template Method
The Template Method is a method in a superclass, usually an abstract superclass, and defines the skeleton of an operation in terms of a number of high-level steps.
Template Methods are frequently used in libraries or general-purpose frameworks that will be used by other developers. An example is an object that fires a sequence of events in response to an action, for example, a process request. The object generates a 'preprocess' event, a 'process' event and a 'postprocess' event. The developer has the option to adjust the response immediately before the processing, during the processing and immediately after the processing.
class Database {
process() {
this.connect();
this.select();
this.disconnect();
return true;
}
}
class MySQL extends Database {
constructor() {
super();
}
}
const mySql = new MySQL();
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();
The template methods allow the client to change the database (MySQL Server, PostgreSQL, etc.) by adjusting (filling in the blanks) only the template methods. The rest, such as the order of the steps, stays the same for any datastore.
Visitor
The Visitor design pattern is a way of separating an algorithm from an object structure on which it operates. A practical result of this separation is the ability to add new operations to existing object structures without modifying the structures.
The Visitor pattern defines a new operation to a collection of objects without changing the objects themselves. The new logic resides in a separate object called the Visitor.
In this example, three employees are created with the Employee
constructor function. Each is getting a 10% salary raise and 2 more vacation days. Two visitor objects, ExtraSalary
and ExtraVacation
, make the necessary changes to the employee objects.
class Employee {
constructor(name, salary, vacationDays) {
this.name = name;
this.salary = salary;
this.vacationDays = vacationDays;
}
accept(visitor) {
visitor.visit(this);
}
getName() {
return this.name;
}
getSalary() {
return this.salary;
}
setSalary(salary) {
this.salary = salary;
}
getVacationDays() {
return this.vacationDays;
}
setVacationDays(days) {
this.vacationDays = days;
}
}
class Salary {
visit(employee) {
employee.setSalary(employee.getSalary() * 10.1);
}
}
class Vacation {
visit(employee) {
employee.setVacationDays(employee.getVacationDays() + 2);
}
}
const employees = [
new Employee('Joe', 100000, 12),
new Employee('Jane', 200000, 38),
new Employee('Mark', 150000, 41),
];
const visitorSalary = new Salary();
const visitorVacation = new Vacation();
for (let i = 0, len = employees.length; i < len; i++) {
const employee = employees[i];
employee.accept(visitorSalary);
employee.accept(visitorVacation);
console.log(
`emp.getName() earns $${employee.getSalary()} and gets ${employee.getVacationDays()} vacation days`
);
}
Structural Design Patterns
Adapter
The Adapter Pattern is a structural design pattern that allows the interface of an existing class to be used as another interface. It is often used to make existing classes work with others without modifying their source code.
The Adapter pattern translates one interface (an object‘s properties and methods) to another. Adapters allow programming components to work together that otherwise wouldn't because of mismatched interfaces. The Adapter pattern is also referred to as the Wrapper Pattern.
// old interface
class Shipping {
request(zipStart, zipEnd, weight) {
const baseCost = 24.38;
return baseCost * weight;
}
}
// new interface
class AdvancedShipping {
login(credentials) {
this.credentials = credentials;
}
setStart(start) {
this.start = start;
}
setDestination(destination) {
this.destination = destination;
}
calculate(weight) {
const baseCost = 22.49;
return baseCost * weight;
}
}
// adapter interface
class ShippingAdapter {
constructor(credentials) {
this.credentials = credentials;
const shipping = new AdvancedShipping();
shipping.login(credentials);
return {
request: function (zipStart, zipEnd, weight) {
shipping.setStart(zipStart);
shipping.setDestination(zipEnd);
return shipping.calculate(weight);
},
};
}
}
const shipping = new Shipping();
const credentials = { token: '49ab1-7re8' };
const adapter = new ShippingAdapter(credentials);
// original shipping object and interface
let cost = shipping.request('89801', '10210', 2);
console.log('Old cost: $' + cost);
// new shipping object with adapted interface
cost = adapter.request('89801', '10210', 2);
console.log('New cost: $' + cost);
The example code above shows an online shopping cart in which a shipping object is used to compute shipping costs. The old Shipping
object is replaced by a new and improved Shipping object that is more secure and offers better prices.
The new object is named AdvancedShipping
and has a very different interface which the client program does not expect. ShippingAdapter
allows the client program to continue functioning without any API changes by mapping (adapting) the old Shipping
interface to the new AdvancedShipping
interface.
Bridge
Bridge is a Structural design pattern that lets you split a large class or a set of closely related classes into two separate hierarchies — abstraction and implementation — which can be developed independently of each other.
The Bridge pattern allows two components, a client and a service, to work together with each component having its own interface. Bridge is a high-level architectural pattern and its main goal is to write better code through two levels of abstraction. It facilitates the very loose coupling of objects. It is sometimes referred to as a double Adapter pattern.
// input devices
class Gestures {
constructor(output) {
this.output = output;
}
tap() {
this.output.click();
}
swipe() {
this.output.move();
}
pan() {
this.output.drag();
}
pinch() {
this.output.zoom();
}
}
class Mouse {
constructor(output) {
this.output = output;
}
click() {
this.output.click();
}
move() {
this.output.move();
}
down() {
this.output.drag();
}
wheel() {
this.output.zoom();
}
}
// output devices
class Screen {
click() {
console.log('Screen select');
}
move() {
console.log('Screen move');
}
drag() {
console.log('Screen drag');
}
zoom() {
console.log('Screen zoom in');
}
}
class Audio {
click() {
console.log('Sound oink');
}
move() {
console.log('Sound waves');
}
drag() {
console.log('Sound screetch');
}
zoom() {
console.log('Sound volume up');
}
}
const screen = new Screen();
const audio = new Audio();
const hand = new Gestures(screen);
const mouse = new Mouse(audio);
hand.tap();
hand.swipe();
hand.pinch();
mouse.click();
mouse.move();
mouse.wheel();
The objective of the example is to show that with the Bridge pattern input and output devices can vary independently (without changes to the code); the devices are loosely coupled by two levels of abstraction.
Gestures
(finger movements) and the Mouse
are very different input devices, but their actions map to a common set of output instructions: click, move, drag, etc. Screen
and Audio
are very different output devices, but they respond to the same set of instructions. Of course, the effects are totally different, that is, video updates vs. sound effects. The Bridge pattern allows any input device to work with any output device.
Composite
The composite pattern describes a group of objects that are treated the same way as a single instance of the same type of object.
The Composite pattern allows the creation of objects with properties that are primitive items or a collection of objects. Each item in the collection can hold other collections themselves, creating deeply nested structures.
class Node {
constructor(name) {
this.children = [];
this.name = name;
}
add(child) {
this.children.push(child);
}
remove(child) {
const length = this.children.length;
for (let i = 0; i < length; i++) {
if (this.children[i] === child) {
this.children.splice(i, 1);
return;
}
}
}
getChild(i) {
return this.children[i];
}
hasChildren() {
return this.children.length > 0;
}
}
// recursively traverse a (sub)tree
function traverse(indent, node) {
console.log(Array(indent++).join('--') + node.name);
for (let i = 0, len = node.children.length; i < len; i++) {
traverse(indent, node.getChild(i));
}
}
const tree = new Node('root');
const left = new Node('left');
const right = new Node('right');
const leftLeft = new Node('leftLeft');
const leftRight = new Node('leftRight');
const rightLeft = new Node('rightLeft');
const rightRight = new Node('rightRight');
tree.add(left);
tree.add(right);
tree.remove(right);
tree.add(right);
left.add(leftLeft);
left.add(leftRight);
right.add(rightLeft);
right.add(rightRight);
traverse(1, tree);
In the example, a tree structure is created from Node
objects. Each node has a name and 4 methods: add
, remove
, getChild
, and hasChildren
. The methods are added to Node
's prototype. This reduces the memory requirements as these methods are now shared by all nodes. Node
is fully recursive and there is no need for separate Component or Leaf objects.
Decorator
The Decorator pattern is a design pattern that allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class.
The Decorator pattern extends (decorates) an object’s behavior dynamically. The ability to add new behavior at runtime is accomplished by a Decorator object which ‘wraps itself’ around the original object. Multiple decorators can add or override functionality to the original object.
class User {
constructor(name) {
this.name = name;
}
describe() {
console.log('User: ' + this.name);
}
}
class DecoratedUser {
constructor(user, street, city) {
this.user = user;
this.name = user.name; // ensures interface stays the same
this.street = street;
this.city = city;
}
say() {
console.log(`Decorated User: ${this.name}, ${this.street}, ${this.city}`);
}
}
const user = new User('Kelly');
user.describe();
const decorated = new DecoratedUser(user, 'Broadway', 'New York');
decorated.say();
In the example code, a User
object is decorated (enhanced) by a DecoratedUser
object. It extends the User with several address-based properties. The original interface must stay the same, which explains why user.name
is assigned to this.name
. Also, the say
method of DecoratedUser hides the say
method of User.
Façade
The Façade pattern is a structural design pattern commonly used in object-oriented programming. Analogous to a facade in architecture, a facade is an object that serves as a front-facing interface masking more complex underlying or structural code.
The Façade pattern provides an interface that shields clients from complex functionality in one or more subsystems. It is a simple pattern that may seem trivial but it is powerful and extremely useful. It is often present in systems that are built around a multi-layer architecture.
const APPROVED_LIST = ['John', 'Lisa'];
class Mortgage {
constructor(name) {
this.name = name;
}
applyFor(amount) {
// access multiple subsystems
let 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`;
}
}
class Bank {
verify(name, amount) {
if (APPROVED_LIST.includes(name) && amount < 200000) {
return true;
}
return false;
}
}
class Credit {
get(name) {
if (APPROVED_LIST.includes(name)) {
return true;
}
return false;
}
}
class Background {
check(name) {
if (APPROVED_LIST.includes(name)) {
return true;
}
return false;
}
}
const mortgage = new Mortgage('John');
const result = mortgage.applyFor(100000);
console.log(result);
The Mortgage
object is the Façade in the example code. It presents a simple interface to the client with only a single method: applyFor
. But underneath this simple API lies considerable complexity.
The applicant's name is passed into the Mortgage constructor function. Then the applyFor
method is called with the requested loan amount. Internally, this method uses services from 3 separate subsystems that are complex and possibly take some time to process; they are Bank
, Credit
, and Background
.
Based on several criteria (bank statements, credit reports, and criminal background) the applicant is either accepted or denied for the requested loan.
Flyweight
A flyweight is an object that minimizes memory usage by sharing as much data as possible with other similar objects.
The Flyweight pattern conserves memory by sharing large numbers of fine-grained objects efficiently. Shared flyweight objects are immutable, that is, they cannot be changed as they represent the characteristics that are shared with other objects.
class Flyweight {
contructor(make, model, processor) {
this.make = make;
this.model = model;
this.processor = processor;
}
}
class FlyWeightFactory {
static flyweights = {};
static count = 0;
static get(make, model, processor) {
if (!FlyWeightFactory.flyweights[make + model]) {
FlyWeightFactory.flyweights[make + model] = new Flyweight(
make,
model,
processor
);
}
return FlyWeightFactory.flyweights[make + model];
}
static getCount() {
for (let f in FlyWeightFactory.flyweights) FlyWeightFactory.count++;
return FlyWeightFactory.count;
}
}
let count = 0;
class ComputerCollection {
computers = {};
add(make, model, processor, memory, tag) {
computers[tag] = new Computer(make, model, processor, memory, tag);
count++;
}
get(tag) {
return computers[tag];
}
getCount() {
return count;
}
}
class Computer {
constructor(make, model, processor, memory, tag) {
this.flyweight = FlyWeightFactory.get(make, model, processor);
this.memory = memory;
this.tag = tag;
this.getMake = function () {
return this.flyweight.make;
};
}
}
const computers = new ComputerCollection();
computers.add('Apple', 'MacBook Pro', 'Intel', '5G', 'Y755P');
computers.add('Apple', 'MacBook Pro', 'Intel', '6G', 'X997T');
computers.add('Apple', 'MacBook Pro', 'Intel', '2G', 'U8U80');
computers.add('Apple', 'MacBook Pro', 'Intel', '2G', 'NT777');
computers.add('Apple', 'MacBook Pro', 'Intel', '2G', '0J88A');
computers.add('HP', 'Envy', 'Intel', '4G', 'CNU883701');
computers.add('HP', 'Envy', 'Intel', '2G', 'TXU003283');
console.log('Computers: ' + computers.getCount());
console.log('Flyweights: ' + FlyWeightFactory.getCount());
In the above example code, we are building computers. Many models, makes, and processor combinations are common, so these characteristics are factored out and shared by Flyweight objects.
Proxy
The Proxy pattern is a structural design pattern. A proxy, in its most general form, is a class functioning as an interface to something else.
The Proxy pattern provides a surrogate or placeholder object for another object and controls access to this other object.
class GeoCoder {
getLatLng(address) {
if (address === 'Amsterdam') {
return '52.3700° N, 4.8900° E';
} else if (address === 'London') {
return '51.5171° N, 0.1062° W';
} else if (address === 'Paris') {
return '48.8742° N, 2.3470° E';
} else if (address === 'Berlin') {
return '52.5233° N, 13.4127° E';
} else {
return '';
}
}
}
class GeoProxy {
constructor() {
const geocoder = new GeoCoder();
const geocache = {};
return {
getLatLng: function (address) {
if (!geocache[address]) {
geocache[address] = geocoder.getLatLng(address);
}
console.log(address + ': ' + geocache[address]);
return geocache[address];
},
getCount: function () {
const count = 0;
for (let code in geocache) {
count++;
}
return count;
},
};
}
}
const geo = new GeoProxy();
// geolocation requests
geo.getLatLng('Paris');
geo.getLatLng('London');
geo.getLatLng('London');
geo.getLatLng('London');
geo.getLatLng('London');
geo.getLatLng('Amsterdam');
geo.getLatLng('Amsterdam');
geo.getLatLng('Amsterdam');
geo.getLatLng('Amsterdam');
geo.getLatLng('London');
geo.getLatLng('London');
console.log('\nCache size: ' + geo.getCount());
Summary
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.
The repository for all the code snippets can be found here.
In this article, we have explored common creational design patterns in JavaScript including the following:
Factory Method Abstract Factory Builder Prototype Singleton Chain of responsibility Command Interpreter Iterator Mediator Memento Observer State Strategy Template Method Visitor Adapter Bridge Composite Decorator Façade Flyweight Proxy