These are some of the ways a code can be written to minimize bugs and improve the quality and the readability of the code.
Mainly, 3 types of Design Patterns are there -
- Creational
- Structural
- Behavioral
Creational Patterns
Singleton
Only one instance of the storage class is created, which is being used by everyone in the application.
In the context of JavaScript, this can be achieved simply by having a global object.
const all = {
name: "Sanjeet",
age: 27
}
Prototype
It’s another way of creating clones in our application code. Multiple objects which have got similar properties shared via a prototype.
Prototypal Inheritance is a good example of this, wherein new objects are created via Object.create
function which takes in a prototype object.
const zombie = {
eatBrains() {
console.log("Eating brains...");
}
}
const chad = Object.create(zombie, { name: "Chad" })
Now, if print out chad
alone - console.log(chad)
, we won’t be able to see eatBrains()
function there, but can be accessed via Object.getPrototypeOf()
function.
JavaScript, when not able to find something in an object, goes recursively through the prototype chain to find the requested property. Hence, chad.eatBrains()
will work smoothly.
Factory
This simply states that, instead of you creating new objects, the responsibility of creating new objects lies in the hands of a factory function, which does the necessary computation and logic to obtain the right kind of object.
One example of this can be -
const iosB = new IOSButton();
const andB = new AndroidButton();
const button = (platform === "ios") ? iosB : andB;
Instead of this, we can have a factory function which determines which platform is it, and then returns the correct object back -
const factory = {
createButton() {
if (platform === "ios") return new IOSButton();
return new AndroidButton();
}
}
const button = factory.createButton();
Builder
Usually, its the constructor’s job to assign initial values to the variables/properties of a class, and usually looks like -
class Bike {
constructor(makeYear, model, kms, insured) {
this.makeYear = makeYear;
this.model = model;
this.kms = kms;
this.insured = insured;
}
}
const apache = new Bike('2007', 'Apache RTR 160', 18000, true);
Here while creating the new object apache
, we are passing direct values to the constructor, which can lead to a bit of confusion as to which property is getting which value.
A way we can fix this -
class Bike {
constructor(model) {
this.model = model;
}
addKms(kms) {
this.kms = kms;
return this;
}
addMakeYear(makeYear) {
this.makeYear = makeYear;
return this;
}
isInsured(insured) {
this.insured = insured;
return this;
}
}
const apache = new Bike("Apache RTR 160").addKms(18000).addMakeYear("2017").isInsured(true);
In a builder class, all methods which assign some value to the properties, the same instance is returned from the method.
Structural Patterns
Facade
This pattern also relates to the D of S.O.L.I.D principles which is Dependency Inversion.
The user does not need to know the internal complexities and low-level code of the plugins and dependencies used, instead, a facade can be placed in the middle of your high-level code, and the low-level code of your dependencies so that your high-level code doesn’t change whenever the logic or complexities in the dependencies change.
Example -
class PlumbingSystem {
turnOn() {
// low-level code which handles the complex
// operation
}
turnOff() {
// ...
}
}
class ElectricalSystem {
turnOn() {
// low-level code which handles the complex
// operation
}
turnOff() {
// ...
}
}
class House {
private plumbing = new PlumbingSystem();
private electrical = new ElectricalSystem();
turnOnSystems() {
plumbing.turnOn();
electrical.turnOn();
}
}
const house = new House();
here the user is just concerned with turning the systems ON in his/her house, which can be done simply by calling house.turnOnSystems()
.
Proxy
A Proxy, as the name suggests, is nothing but a substitute for the real thing. Proxies sits at front of the targets, and act as one, so that some extra validations and checks can be performed on it, before updating the real target.
In JavaScript, this is possible via Proxy constructor, which takes in the original object and the handler -
const original = {
name: "Sanjeet"
}
const proxy = new Proxy(original, {
get(target, key) {
// do anything with the target[key]
// then get it
return target[key]
}
set(target, key, value) {
// ... do whatever needs to happen before setting the
// actual target
return Reflect.set(target, key, value);
}
})
Reflect is a special operation, which updates the target with the provided value.
Now, values can be accessed and updated via proxy
object - proxy.name
.
Composite
Composite design pattern is a way of organizing objects. It helps us handle different objects in a similar way when they are put together to create a structure with parts or wholes. It helps us organize different objects that cumulatively resembles a tree-like structure.
There are 3 main elements of Composite design pattern -
- Component - This is an abstract class or an idea that everyone in the tree, whether it be the leaf or the composite, needs to implement. All the methods specified in the component needs to be implemented by the nodes in the tree.
- Composite - A node which has got some child nodes which can be leaf nodes or another composites. It implements the Component and dictate how its children are modified/updated.
- Leaf - A node which doesn’t has any children, and which represents the basic building block of the structure. It also implements all the methods of the Component.
An example of this would be a directory structure -
class Component {
print() {
// to be implemented
}
size() {
// to be implemented
}
}
We have defined a Component which relates all parts and wholes of a directory structure namely - Files and Folders.
In this case a File
is a Leaf node -
class File extends Component {
constructor(name, size) {
super();
this.name = name;
this.size = size;
}
print() {
console.log(`File name: ${this.name}; File size: ${this.size}`);
}
size() {
return this.size;
}
}
And a Folder
is a Composite node -
class Folder {
constructor(files = [], name) {
super();
this.files = files;
this.name = name;
}
print() {
console.log(`Folder Name: ${this.name}`);
}
size() {
let size = 0;
for (let file of this.files) {
size += file.size();
}
return size;
}
addAFile(file) {
this.files.push(file);
}
}
In this way, it becomes very much easy to add new elements to the mix, whether it be a Composite or a Leaf node.
Decorator
A decorator patterns enables an object to have added behaviors on top of the existing concrete behavior. It also has got some elements -
- Component - This is the abstract class that is implemented by the Concrete object which serves as a baseline for the other added features or decorators.
- Concrete - This is the object which needs to implement all methods of the Component interface. This is the base object on top of which added features/decorators will be added.
- Decorator - This is the abstract class for all objects who will add some functionality on top of the Concrete object. It contains a reference to the Component object, and tells how the added functionality will be implemented.
- Concrete Decorator - These are the objects which will implement the added behaviors on top of the basic behavior. This will implement the methods of the Decorator class.
A good example of this will be how Pizza works. The component of a basic Pizza class will look something like this -
class Pizza {
constructor(name, toppings = [], crust) {
this.name = name;
this.toppings = toppings;
this.crust = crust;
}
bake() {
console.log(`Baking ${this.name} pizza`);
}
}
The Concrete
class of Pizza which is nothing but the most basic Pizza will look something like -
class APizza extends Pizza{
constructor(name, toppings, crust) {
super(name, toppings, crust);
}
}
This is the most basic implementation of Pizza
class.
Then we will have a decorator which will specify how different objects can exhibit different behaviors on top of the basic one.
class PizzaDecorator extends Pizza {
constructor(pizza) {
super(pizza.name, pizza.toppings, pizza.crust)
}
bake() {
this.pizza.bake();
}
}
And now, finally we can have Concrete Decorators
which are the actual objects which add to the functionality.
class PizzaWithCheese extends PizzaDecorator {
constructor(pizza, cheeseInGrams) {
super(pizza);
this.cheeseInGrams = cheeseInGrams;
}
bake() {
super.bake();
console.log(`Adding ${cheeseInGrams}g cheese on top`);
}
}
And that’s how other variations can be created.
Behavioral Patterns
Iterator
This pattern is needed whenever a collection of data needs to be traversed. JavaScript has provided a utility Iterator for this purpose.
Iterators are entities in JavaScript, which has got a next()
function which provides the next value, and whether the traversal is done or not in an object - { done: true/false, value: <val> }
.
for(..of..)
loop can only be run on Iterators, or iterable collections.
One example of it would be a range function in JavaScript which we don’t get out of the box.
const range = (start, stop, step = 1) => {
let s = start;
return {
[Symbol.iterator]() {
return this;
},
next() {
if (s < stop) {
const objectToReturn = { done: false, value: s }
s = s + step;
return objectToReturn;
}
return { done: true, value: s }
}
}
}
for(let n of range(1, 5, 1)) {
console.log(n) // This will print 1, 2, 3, 4
}
The symbol iterator method -
[Symbol.iterator]() {
return this;
}
lets us use the range()
function inside for(..of..)
loop.
Observer
In this pattern, everyone is an observer and listens onto a common signal.
This is like a TV broadcast, where everyone is tuned to the same signal, and are able to view the same data worldwide.
Observer pattern represents One to Many relationships.
Common use case for such pattern in JavaScript are Subjects by Rxjs which can be subscribed by the observers.
import { Subject } from 'rxjs';
const news = new Subject();
// add subscribers
const tv1 = news.subscribe(v => console.log(v + 'via TV station 1'));
const tv2 = news.subscribe(v => console.log(v + 'via TV station 2'));
const tv3 = news.subscribe(v => console.log(v + 'via TV station 3'));
// sending the event
news.next("Breaking news: ");
Mediator
A mediator is someone who handles all the inputs to it, process it, and provides different kinds of output accordingly.
It represents Many to Many relationships.
Examples of such mediators are the middlewares commonly used in express
, which intercepts the incoming request.
const app = express();
function logger(req, res, next) {
console.log('Request type:', req.method);
next();
}
app.use(logger);
State
This pattern is applicable where objects behaves differently for a finite number of states.
This pattern again resembles O of S.O.L.I.D principles, i.e. Open/Closed Principle, which essentially warns us to use switch
statements.
Instead of switch, classes can be used for each particular state.
Example -
class HappyState {
think() {
console.log("I am happy")
}
}
class SadState {
think() {
console.log("I am sad")
}
}
class NeutralState {
think() {
console.log("I am neutral")
}
}
class User {
constructor() {
this.state = new HappyState();
}
think() {
console.log(this.state.think())
}
changeState(state) {
this.state = state;
}
}
Here, without using a switch case, the state of the user can be changed via changeState
method of the user.
Strategy
It’s same as state, but in this case, object behaves differently in the way it handles the data.
class EvenNumbers {
perform(arr = []) {
// ... print out array with only even numbers
}
}
class OddNumbers {
perform() {
// ... print out array with only odd numbers
}
}
class User {
constructor() {
this.strategy = new EvenNumbers();
}
perform(arr = []) {
console.log(this.strategy.perform(arr))
}
changeStrategy(strategy) {
this.strategy = strategy;
}
}
Now, the user can change the strategy of how the data passed to it is handled, without using any switch
statement.