There are common patterns you can use in Javascript that will improve your code substantially. These are techniques to simplify and better structure your code in a way that makes it reusable, modular, easier to consume and test, and will speed up your code and your coding process.
您可以在 Javascript 中使用一些常见的模式,这些模式会大大改善您的代码.这些技术可以简化和更好地构造代码,从而使其可重用,模块化,易于使用和测试,并加快代码和编码过程的速度.
Types of Patterns
There are three(3) types of patterns. Creational, structural, and behavioral.
- Creational— Addresses problems related to creating objects.
- Structural— Addresses the relationship between entities and how together they can compose a larger structure.
- Behavioral— Addresses how objects communicate and interact with each other.
创建型模式- 处理对象的创建,根据实际情况使用合适的方式创建对象.常规的对象创建方式可能会导致设计上的问题,或增加设计的复杂度.创建型模式通过以某种方式控制对象的创建来解决问题.
结构型模式- 通过识别系统中组件间的简单关系来简化系统的设计.
行为型模式- 用于识别对象之间常见的交互模式并加以实现,如此,增加了这些交互的灵活性.
Factory Pattern
This is one of the most common creational d esign patterns and one of my favorites when it comes to creating objects. This pattern allows you to separate the implementation and creation of a particular object. It also allows for a controlled way to create objects abstracting away any complexity or the need to interact with a particular object or API directly.
Another thing it allows us to do is to create an object whose class or constructor is only known at runtime. If you ever used Express.js for Node you used its app factory( createApplication ) when you created the express app. The constructor that creates the app for you is created at runtime and the factory exposes just enough stuff for you to interact with and handles all the mess away from you.
const express = require('express');
const app = express();
This is what the express app factory looks like on the surface. It has its internal API and parts that take care of different things and when you request an app it then goes ahead and assembles your object — hence the name factory — and returns what you asked for. The advantage of that is you always have the same way to create the app and they can change the internal part however they please and you never know about it.
Observer Pattern
If you ever used RxJs you already felt the power of the observer pattern. What it allows you to do is, well, observe another object. With this pattern, you have the Observable which is the object that handles the observing which consumes an Observer which does all the notifications. Surprisingly, you can create an observable in a couple of lines:
function Observable(observer) {
this.subscribe = (...fns) => {
if (observer) {
const obs = new Observer(...fns);
observer(obs);
}
};
this.unsubscribe = () => {
observer = null;
};
}
where the Observer can look something like this:
function Observer(onNext, onError = () => {}, onComplete = () => {}) {
if (typeof onNext === 'object') {
onError = onNext.error || onError;
onComplete = onNext.complete || onComplete;
onNext = onNext.next || (() => {});
}
this.completed = false;
this.next = (val) => {
onNext(val);
};
this.error = (err) => {
this.completed = true;
this.next = () => {};
onError(err);
};
this.complete = () => {
this.completed = true;
this.next = () => {};
onComplete();
};
}
which then you use it like so:
const obs = new Observable((observer) => {
observer.next(10);
observer.next(20);
observer.complete(new Error());
observer.error(new Error());
});
obs.subscribe({
next(value) {
console.log('next', value);
},
error(err) {
console.log('err', err);
},
complete() {
console.log('complete');
},
});
obs.unsubscribe();
The RxJs team has a more elaborated implementation which also involves pipes/operators and other features like a scheduler and many more.
Module Pattern
If you ever used NodeJs then you already know the module pattern, same for many client libraries to build your app. The module pattern allows you to encapsulate functionally and organize code in modules — parts that make your application. In NodeJs, when you create a file and dump your code in there, on execution your code is wrapped in a module using the IIFE (Immediately Invoked Function Expression) that looks like this:
(function (require, global, exports, module, __dirname, __filename) {
// your Node code goes here
})();
…where require , global , module , exports , __dirname , and __filename are objects that are injected into your module that you can access to expose parts of your code, import parts from other code, and access global info of the environment.
In the browser you can create something like this:
const globalData = {
x: 20,
};
const myModule = (function (global) {
// <- access injections
// private stuff in the module
const val = 10 + global.x; // expose what you want
return {
prop: 12,
method() {
return val;
},
};
})(globalData); // <- inject into your module
console.log(myModule.prop); // prints 12
console.log(myModule.method()); // prints 30
The module pattern is perfect for grouping code for a specific feature and has control over what can be accessed from the outside. A module does not even have to return anything. You may pass it some object or data and it will execute it in its own ecosystem continuously which can be great.
Proxy Pattern
The proxy pattern introduces you to a new type of programming called metaprogramming. Javascript comes with the Proxy object which literally gives you a superpower and pushes you to the next level. It helps you control access to another object — called subject —by implementing the same interface. The Proxy helps you intercept operations to the subject which makes it great for:
- Validation— validate data to the subject to make sure it is valid before it reaches the subject;
- Security— ensures that any access to the subject is authorized and the one doing it has all necessary privileges;
- Caching— keep an internal cache to make future expensive operations do not go through the expensive calculation in the subject;
- Lazy initialization— ensure that expensive initialization of the subject is delayed for when it is actually needed;
- Debugging/Logging— intercept all the data in and out to create a realistic report of usage of the subject;
- Remote Object Access— makes remote objects appear local;
The proxy pattern deserves a post of its own and by far it is my favorite of all patterns and super fun to work with. It can be used with the third party API or object/library to ensure things are checked and handled before handed to the third party and the results coming out has a certain format your application needs. This is a great way to patch or extend third-party things without touching their code.
Let’s take a look at a simple validator to ensure the age of the person is set with a valid value anytime.
let validator = {
set: function (obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
if (value > 200) {
throw new RangeError('The age seems invalid');
}
}
// The default behavior to store the value
obj[prop] = value;
// Indicate success
return true;
},
};
const person = new Proxy({}, validator);
person.age = 100;
console.log(person.age); // 100
person.age = 'young'; // Throws an exception
person.age = 300; // Throws an exception
Facade Pattern
This is a simple pattern used to shield you from complex API and to unify multiple separate APIs. If you ever used jQuery then you used a Facade. jQueryis a facade for the complex DOM API and it simplifies all the complexity of working with DOM by exposing the same API which is easier to reason with.
So, facades let you abstract away any complexity of the subsystem allowing you to interact with it directly instead of the system. This is probably the most common pattern when it comes to libraries of the web. This pattern can help you turn powerful and complex things out there into something much simpler to consume.
// facade around the XMLHttpRequest API
// with support for Promise and POST
// JSON and Multipart Data
const request = (() => ({
post(url, data = {}) {
const req = new XMLHttpRequest();
if (data instanceof File) {
data = new FormData();
data.append('file', data, data.name);
} else {
data = JSON.stringify(data);
}
req.open('POST', url, true);
return new Promise((res, rej) => {
req.onload = (e) => {
if (req.status === 200) {
res(e);
} else {
rej(e);
}
};
req.onerror = rej;
req.onabort = rej;
req.onabort = rej;
req.ontimeout = rej;
req.send(data);
});
},
get() {
// code here
},
}))();
request
.post('https://www.domain.com/api/endpoint', { some: 'data' })
.then((e) => {
console.log('success', e);
})
.catch((e) => {
console.log('failed', e);
});
Iterator Pattern
Iterators are pretty much everywhere in Javascript. It is such a fundamental pattern that it is built-in into the language itself. You can use it to iterate anything from the array , dictionaries to tree data Structures . What is cool about it is it provides the same interface for you to iterate any data container instead of looping arrays and traversing trees and graphs.
// simple iterator that takes a
// function to get the next value
// which is called on every iteration
// and completes if the function
// returns null
function Iterator(getNextValue) {
this.next = () => {
const value = getNextValue();
if (value === null) {
return { done: true };
}
return { value, done: false };
};
this[Symbol.iterator] = function () {
return this;
};
}
Iterators are stateful objects since we have to keep track of the current item and you can use generators to implement them which makes them even more powerful.
A perfect example of this is let’s say you have to iterate over a list of 1 thousand numbers from 1 to 1000. The naive way would be actually create an array of actually 1 thousand items and iterate it. But with an iterator, you can calculate the next item when you are actually accessing which allows you to save on memory.
const thousandList = {};
// using generator to implement an iterator
thousandList[Symbol.iterator] = function* () {
let number = 0;
while (number < 1000) {
yield ++number;
}
};
for (const number of thousandList) {
console.log(number); // prints 1 to 1000
}
[...thousandList]; // [1, 2, 3, ..., 1000]
Another use case for iterators is, let’s say you have a tree and you have to pass it to something else in your application. So you decide to make a copy of the three beforehand to make sure whatever modifications do not affect the original tree.
The problem with cloning the tree is that it grows, and copying to pass the tree can become expensive. Instead, you can pass an iterator that allows the recipient to traverse the tree one node at a time where you only clone the node when they are accessing it and remove the need to clone the whole thing at once.
An even better solution would be to use a proxy to the above solution but iterators are super powerful as well and you rarely need to implement your own for native data structures as they all give an iterator option to use or are iterable.
Prototype Pattern
Prototype Oriented Programming is a type of Object-Oriented Programming and Javascript is a prototypical inheritance language. Way before class was introduced, the only way to do OOP was through prototype, and as matter of fact, the Javascript class is just syntactic sugar on top of prototype and constructor function. So why use this instead of classes then?
- Learn Javascript prototypical inheritance nature;
- Improve your ability to debug Javascript weird behaviors;
- Understand how everything works under the hood;
- Full control over the members of the object;
- Allows you to create library and frameworks more efficiently by manipulating lower-level code;
- Ability to extend from multiple sources for much better composability;
- Better control on what you are inheriting with Object.create second argument.
function Calculator() {
// total is public because we declared it on the "this"
this.total = 0;
// precision is private because is a local variable/constant
const precision = 2;
// to precision is a public function expression with access to
// private properties
this.toPrecision = (number) => Number(number.toFixed(precision));
// create a getter for the property "result"
Object.defineProperty(this, 'result', {
get() {
return this.total;
},
});
}
// create a static member
// only available on Calculator. It cannot be inherited
Calculator.PI = 3.14; // prototype methods
Calculator.prototype.add = function (x) {
this.total += this.toPrecision(x);
};
Calculator.prototype.subtract = function (x) {
this.total -= this.toPrecision(x);
};
Calculator.prototype.multiply = function (x) {
this.total *= this.toPrecision(x);
};
Calculator.prototype.divide = function (x) {
this.total /= this.toPrecision(x);
};
function ScienticCalculator() {
// this is equivalent to calling super()
// when you extend another class
// it will copy all properties from inside Calculator
// into ScienticCalculator
// You can call as many constructor functions
// to inherit properties from multiple ones
Calculator.call(this);
}
// make ScienticCalculator extend Calculator
// similar to what happens when you do
// "class Calculator extends ScienticCalculator"
ScienticCalculator.prototype = Object.create(Calculator.prototype);
ScienticCalculator.prototype.constructor = ScienticCalculator;
const calc = new Calculator();
const scientificCalc = new ScienticCalculator();
console.log(calc);
console.log(scientificCalc);
// checks logs below
Decorator Pattern
The decorator can push you into meta-programming which is a technique to use other programs as data to another. Both decorator and Proxy are a way to meta-program and you can use Proxy to implement a decorator. As a matter of fact, there are few ways to implement decorators:
- Composition — wrapping an object around another which it inherits from to implement new or changed things to it (inheritance);
- Object Augmentation — attach or change things directly on the object (monkey patching);
- Proxy Object — intercept and react to object actions in order to alter the behavior or result (Proxying).
Javascript allows for decorators which you can actually try through Typescript or by using the decorator’s Babel plugin as it is still in the early steps(proposal) . Amazingly, it does it through the manipulation of object properties definition (prototype) and descriptors.
// An example with no decorator Support
// through Object Augmentation
function logger(obj, prop, message = 'logger') {
let x = obj[prop];
if (typeof x === 'function') {
obj[prop] = (...args) => {
console.log(message, ...args);
x.call(obj, ...args);
};
} else if (obj.hasOwnProperty(prop)) {
Object.defineProperty(obj, prop, {
get() {
console.log(message + ' - get:', x);
return x;
},
set(val) {
console.log(message + ' - set:', val);
x = val;
},
});
}
}
class Calculator {
total = 0;
constructor() {
// decorate property and method
logger(this, 'total', 'total');
logger(this, 'add', 'add argument:');
}
add(x) {
this.total += x;
}
}
const calc = new Calculator();
calc.add(20);
// logs
// add argument: 20
// total - get: 0
// total - set: 20
Decorators, similarly to Proxy, allows you to add different behavior or information to things without directly changing them and can be very powerful. We are talking Pro level stuff here.
Composite Pattern
The composite pattern is awesome and even better if you are using Typescript since the types help give it more meaning and make it more obvious. When I made my video on creating a file system I used this pattern to give users the feeling they are interacting with one thing (the system) but it was actually composed of different parts that are simply implementing the same interface.
When you use React and are creating class components, notice that all of them are implementing the same interface — the Component — and you compose these different class components to shape your application. The Component is the interface that all your custom components implement and you are using composition to create your app.
MV* Frameworks
These are actually frameworks but they are a type of design pattern. I really believe in the proper usage of the MV* patterns when building things. I believe in the separation of concerns, sticking to the pattern, and independence of the parts. They are few different types of these and depending on the project, you may need to pick the right one.
- MVC — The Model View Controller lets the data(M) be manipulated by the controller(C) and the data(M) updates the UI(V). The users see the UI (V) and the interactions are handled by the controller(C).
- MVP — The Model View Presenter is a type of MVC. The difference is that the presenter(P) is the middle man. It controllers how they view renders and all the interaction and updates the data(M) accordingly and handles to update the UI with data changes.
- MVVM — The Model View View-Model is probably the most popular and modern and it is the pattern people use in the Frontend the most. It is actually very similar to the MVP in the way that the View-Model talks with the data but the view-model uses data binding and it is more event-driven which means it has no reference to the view and can be tested in isolation. In MVP the Presenter is known by the view and it is a more tightly couple setup.
- MVA — The Model View Adapter is another variation of the MVC pattern and they pretty much try to solve the same problem differently. The adapter sits in the middle and there is no connection between the model and the view.
Conclusion
There are so many patterns to use with your code that can drastically change the way your program works, scales, and can be maintained. The patterns can be seen as recipes which if you follow correctly, the chances of you building something great is high. If you ever asked “How I do this” it is probably a sign you not aware of patterns and learning patterns levels you up from the coder(API consumer level).
Patterns are also great if you are involved in implementing big solutions or are interested in architecting and designing systems.
My Youtube Channel : Before Semicolon
Website : beforesemicolon.com