Real World Design Patterns using NodeJs APIs
April 14, 2020 ยท View on GitHub
- Chain Of Responsibility
- Command
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
Creational
Abstract Factory
abstractFactory.js
const net = require('net');
const http = require('http');
// Abstract Product Class
class ApiRequest {
makeGetRequest(url) {
return Error(`Implement make Get Request for ${url}`);
}
}
// Product A
class tcpApiRequest extends ApiRequest {
makeGetRequest(url) {
// Handling simple get request without query params
return new Promise((resolve, reject) => {
const socket = net.createConnection({
host: "www.example.com",
port: "80"
});
socket.on('data', (data) => resolve(data.toString()));
socket.on('error', err => reject(err));
socket.end(`GET / HTTP/1.1\r\nHost: ${url}\r\n\r\n`);
});
}
}
// Product B
class httpApiRequest extends ApiRequest {
makeGetRequest(url) {
// Handling simple get request without query params
return new Promise((resolve, reject) => {
http.request(`http://${url}`, (res) => {
res.on('data', data => resolve(data.toString()));
res.on('error', err => reject(err));
}).end();
});
}
}
/**
* This is an abstract factory interface. Uses a static function to
* generate family of products.
*/
class ApiRequestFactory {
static createApiRequest(kind) {
// This can easily be extended to include HTTPS, HTTP2, HTTP3
switch(kind) {
case "tcp":
return new tcpApiRequest();
case "http":
return new httpApiRequest();
}
}
}
/**
* Use this in another class making the product class
* and client class isolated
*/
const availableOptions = ["tcp", "http"];
const apiRequest = ApiRequestFactory.createApiRequest(availableOptions[Math.floor(Math.random() * 2)]);
apiRequest.makeGetRequest("example.com")
.then(response => console.log(response))
.catch(err => console.log(err));
Builder
builder.js
const net = require('net');
/**
* This is general Outline on a basic Server
*/
class Server {
setHostname() { }
setPortNumer() { }
setOnConnection() { }
listen() { }
}
/**
* Server Builder is implementing the server
* using the net. New builder can easily be made to
* use server like exrpess etc
*/
class ServerBuilder extends Server {
constructor() {
super();
this._server = null;
this._hostname = "localhost";
this._port = 8080;
this._isHalfOpenedSockedAllowed = false;
this._isPauseOnConnect = false;
this._onConnectionCb = () => {};
}
setHostname(hostname) {
this._hostname = hostname;
return this;
}
setPortNumer(port) {
this._port = port;
return this;
}
setOnConnection(callback) {
this._onConnectionCb = callback;
return this;
}
setHalfOpenSocketAllowed() {
this._isHalfOpenedSockedAllowed = true;
return this;
}
setPauseOnConnect() {
this._isPauseOnConnect = true;
return this;
}
listen(callback) {
this._server = net.createServer({
allowHalfOpen: this._isHalfOpenedSockedAllowed,
pauseOnConnect: this._isPauseOnConnect
});
this._server.on('connection', this._onConnectionCb);
this._server.listen(this._port, this._hostname, callback);
return this;
}
}
// Director class will receive this builder class
let serverBuilder = new ServerBuilder();
// Director class can build server like with builder object
serverBuilder.setHostname("localhost")
.setPortNumer(8080)
.listen(() => console.log("server Started"));
Factory Method
factoryMethod.js
const net = require('net');
const http = require('http');
// Abstract Product Class
class ApiRequest {
makeGetRequest(url) {
return Error(`Implement make Get Request for ${url}`);
}
}
// Product A
class tcpApiRequest extends ApiRequest {
makeGetRequest(url) {
// Handling simple get request without query params
return new Promise((resolve, reject) => {
console.log("using tcp Request");
const socket = net.createConnection({
host: "www.example.com",
port: "80"
});
socket.on('data', (data) => resolve(data.toString()));
socket.on('error', err => reject(err));
socket.end(`GET / HTTP/1.1\r\nHost: ${url}\r\n\r\n`);
});
}
}
// Product B
class httpApiRequest extends ApiRequest {
makeGetRequest(url) {
// Handling simple get request without query params
return new Promise((resolve, reject) => {
console.log("using http Request");
http.request(`http://${url}`, (res) => {
res.on('data', data => resolve(data.toString()));
res.on('error', err => reject(err));
}).end();
});
}
}
// This is the class that would be using the Product
class ClientTcp {
main() {
/**
* Client/Director class is not directly making an object
* It uses a class function for doing it
*/
const apiRequest = this.makeGetRequest();
apiRequest.makeGetRequest("example.com")
.then(respone => console.log(respone))
.catch(err => console.log(err));
}
// Factory method
makeGetRequest() {
return new tcpApiRequest();
}
}
class ClientHTTP extends ClientTcp {
// Overriding factory method to use different object
makeGetRequest() {
return new httpApiRequest();
}
}
let c = new ClientHTTP();
c.main();
Prototype
prototype.js
class Server {
constructor(port) {
this._port = port;
}
listen() {
console.log("Listening on port");
}
clone() {
return new Server(this._port);
}
}
const server = new Server();
const newServer = server.clone();
newServer.listen();
Singleton
singleton.js
class Server {
constructor(port) {
this._port = port;
}
static init(port) {
if (typeof Server.instance === 'object') {
return Server.instance;
}
Server.instance = new Server(port);
return Server.instance;
}
static getInstance() {
if (typeof Server.instance === 'object') {
return Server.instance;
}
Server.instance = new Server(8080);
return Server.instance;
}
status() {
console.log("Server listening on port " + this._port);
}
}
/**
* Client calls init, and getInstance would give that instance
* always. Singleton is used for heavy single use objects like DB
*/
Server.init(1234);
Server.getInstance().status();
Structural
Adapter
adapter.js
// fs is the adaptee class
const fs = require('fs');
// delegator class
class fsDelegator {
read() {
return new Error("Please implement read method!");
}
}
/**
* Adapter class implements the delegate
* Converts fs callbacks to fs promisified
*/
class fsAdapter extends fsDelegator {
read(path) {
return new Promise((resolve, reject) => {
// eslint-disable-next-line node/prefer-promises/fs
fs.readFile(__dirname + "/" + path, (err, buf) => {
if(err) {
return reject(err);
}
resolve(buf.toString());
});
});
}
}
class Client {
constructor() {
this.setDelegate();
}
async reader() {
return this._fsDelegate.read("adapter.js");
}
setDelegate() {
this._fsDelegate = new fsAdapter();
}
}
const client = new Client();
client.reader().then(res => console.log("Reading " + res));
Bridge
bridge.js
/**
* ApiRequestFactory gives underlying implementation of
* api get request that we make
*/
const ApiRequestFactory = require("./lib");
class UpstreamFile {
getFileUpstream() { }
}
/**
* This is abstraction class that doesnt care about
* the implementation of api request.
*/
class Config extends UpstreamFile {
constructor(url, apiRequest) {
super();
this.url = url;
this.apiRequest = apiRequest;
}
getFileUpstream() {
this.apiRequest
.makeGetRequest(this.url)
.then(response => console.log(response))
.catch(err => console.log(err));
}
}
class Post extends UpstreamFile {
constructor(url, apiRequest) {
super();
this.url = url;
this.apiRequest = apiRequest;
}
getFileUpstream() {
this.apiRequest
.makeGetRequest(this.url)
.then(response => console.log(response))
.catch(err => console.log(err));
}
}
/**
* AbstractFactory is used to generate related implementation for these
* classes
*/
new Config("https://jsonplaceholder.typicode.com/todos/1", ApiRequestFactory.createApiRequest("tcp")).getFileUpstream();
new Post("jsonplaceholder.typicode.com/posts/1", ApiRequestFactory.createApiRequest("http")).getFileUpstream();
Composite
composite.js
class ContentBuffer {
getSize() { }
}
// Composite Class has children as filebuffer
class DirBuffer extends ContentBuffer {
constructor() {
super();
this.bufferList = [];
}
getSize() {
return this.bufferList.map(buf => buf.getSize()).reduce((a, b) => a + b);
}
addBuffer(buf) {
this.bufferList.push(buf);
}
}
// Leaf class
class FileBuffer extends ContentBuffer {
setBuffer(buf) {
this.buf = buf;
return this;
}
getSize() {
return this.buf.length || 0;
}
}
const file1 = new FileBuffer().setBuffer(Buffer.from("hello"));
const file2 = new FileBuffer().setBuffer(Buffer.from("hello2"));
const compositeObj = new DirBuffer();
compositeObj.addBuffer(file1);
compositeObj.addBuffer(file2);
console.log(compositeObj.getSize());
Decorator
decorator.js
/**
* Abtract component
*/
class Logger {
log() {
}
}
class BasicLogger extends Logger {
log(msg) {
console.log(msg);
}
}
// Decorator class 1
class DateDecorator extends Logger {
constructor(logger) {
super();
this._logger = logger;
}
log(msg) {
msg = "[" + new Date() + "] " + msg;
this._logger.log(msg);
}
}
// Decorator class 2
class ColorDecorator extends Logger {
constructor(logger) {
super();
this._logger = logger;
this.color = "\x1b[40m";
}
log(msg) {
msg = "\x1b[36m"+ msg + "\x1b[0m";
this._logger.log(msg);
}
}
/**
* Enhancing logger via decoratoe
*/
const basicLogger = new BasicLogger();
const colorDecorator = new ColorDecorator(basicLogger);
const dateDectorator = new DateDecorator(colorDecorator);
dateDectorator.log("Hello World");
Facade
facade.js
const ApiRequestFactory = require("./lib");
class ConfigFormatConvert {
static convert(config) {
return JSON.parse(config);
}
}
class ConfigCheck {
static configCheck(config) {
if(!config.body){
return new Error("biy doesnt exist");
}
return config;
}
}
/**
* Config facade handles all config subsystem
* Fetching converting then sanitizing
*/
class ConfigFacade {
static async getConfig() {
let config = await ApiRequestFactory
.createApiRequest("http")
.makeGetRequest('jsonplaceholder.typicode.com/posts/1');
config = ConfigFormatConvert.convert(config);
config = ConfigCheck.configCheck(config);
console.log(config);
}
}
ConfigFacade.getConfig();
Flyweight
flyweight.js
/**
* Custom error class.
* We shouldnt create new class everytime we encounter
* any error
*/
class CustomError extends Error {
constructor(code, errorMsg) {
super(errorMsg);
this.code = code;
this.message = errorMsg;
}
}
/**
* Returns the error associated with the code
* Reduces the number of objects in the system
*/
class CustomErrorFactor {
constructor() {
this.errorClasses = {};
}
static getInstance() {
if( typeof CustomErrorFactor.instace === 'object') {
return CustomErrorFactor.instace;
}
CustomErrorFactor.instace = new CustomErrorFactor();
return CustomErrorFactor.instace;
}
getErrorClass(code, msg) {
if(typeof this.errorClasses[code] === 'object') {
return this.errorClasses[code];
}
this.errorClasses[code] = new CustomError(code, msg);
return this.errorClasses[code];
}
}
console.log(CustomErrorFactor.getInstance().getErrorClass(1, "error1").message);
console.log(CustomErrorFactor.getInstance().getErrorClass(2, "error2").message);
Proxy
proxy.js
class DatabaseBase {
query() {
}
}
class Database extends DatabaseBase {
query(query) {
return "response" + query;
}
}
// Cached DB obje
class CachedDatabase extends DatabaseBase {
constructor() {
super();
this.cacheQuery = {};
}
query(query) {
if(this.cacheQuery[query]) {
return this.cacheQuery[query];
}
this.cacheQuery[query] = this.getDatabase().query("quer1");
return this.cacheQuery[query];
}
// Lazy initiallization of heavy obejct
getDatabase() {
if(typeof this._database === 'object')
return this._database;
this._database = new Database();
return this._database;
}
}
/**
* CachedDatabase is proxy object for original db
* Lazy initialization
* Access control
*/
const db = new CachedDatabase();
console.log(db.query("query1"));
Behavioral
Chain Of Responsibility
chainOfResponsibility.js
class ConfigCheck {
check() {
return true;
}
setNext(next) {
// Returning a handler from here will let us link handlers in a convenient
this._next = next;
return next;
}
}
// Chanin of commands for checking config
class AuthCheck extends ConfigCheck {
check(config) {
if (!config.key) return new Error("No key");
if (!config.password) return new Error("No password");
if (this._next)
return this._next.check(config);
else {
return super.check();
}
}
}
class URLCheck extends ConfigCheck {
check(config) {
if (!config.url) return new Error("No valid URL");
if (this._next)
return this._next.check(config);
else {
return super.check();
}
}
}
const urlChecker = new URLCheck();
const authChecker = new AuthCheck();
urlChecker.setNext(authChecker);
console.log(urlChecker.check({}));
const config = {
key: "abc",
password: "secret",
url: "valid url"
};
console.log(urlChecker.check(config));
Command
command.js
class Command {
execute() {
return new Error("Implement execute method!");
}
}
/**
* Simple Stream workflow
*/
class Stream {
constructor() {
this._handlers = {};
}
on(key, command) {
this._handlers[key] = command;
}
connect() {
// On sftream connect
if(this._handlers['connect']) {
this._handlers['connect'].execute();
}
// Do some other work
// disconnect the connection and call callback
if(this._handlers['disconnect']) {
this._handlers['disconnect'].execute();
}
}
}
// A command implementation
class ConnectCallback extends Command {
execute() {
console.log("executing connect callback");
}
}
class DisconnectCallback extends Command {
execute() {
console.log("executing disconnect callback");
}
}
const exampleStream = new Stream();
exampleStream.on('connect', new ConnectCallback());
exampleStream.on('disconnect', new DisconnectCallback());
exampleStream.connect();
Iterator
iterator.js
const fs = require('fs');
class Iterator {
getNext() {}
hasNext() {}
}
// Internally maintains list of files in the folder
class FileBufferIterator extends Iterator {
constructor(folderPath) {
super();
this.folderPath = folderPath;
this.currentPosition = 0;
this.init();
}
init() {
const folderPath = this.folderPath;
let fileList = fs.readdirSync(folderPath);
console.log(fileList);
fileList = fileList.map(fileName => folderPath + "/" + fileName);
this.fileBuffer = fileList.map(filePath => fs.readFileSync(filePath));
}
getNext() {
if(this.hasNext()) {
return this.fileBuffer[this.currentPosition++];
}
}
hasNext() {
return this.fileBuffer.length > this.currentPosition;
}
}
function totalSize(iterator) {
let size = 0;
while(iterator.hasNext()) {
size += iterator.getNext().length;
}
return size;
}
console.log(totalSize(new FileBufferIterator(__dirname)));
Mediator
mediator.js
const cluster = require('cluster');
if (cluster.isMaster) {
// Mediate between master class and forks
class MasterProcessMediator {
constructor() {
this.forks = [];
}
init() {
const worker = cluster.fork();
this.forks.push(worker);
//
worker.on('message',
(() => (message) => this.handleMessageFromWorker(worker.id, message))()
);
}
// handler for various workers
handleMessageFromWorker(workerId, message) {
console.log("Worker " + workerId + " says hi with msg " + message);
}
notifyAllWorker() {
this.forks.map(fork => fork.send("received Msg"));
}
}
const mediator = new MasterProcessMediator();
mediator.init();
mediator.notifyAllWorker();
} else {
process.on('message', (message) => {
console.log(cluster.worker.id + " " + message);
});
process.send("working");
setTimeout(() => process.kill(process.pid), 0);
}
Memento
memento.js
// Save board position
class Memento {
constructor(state) {
this.state = state;
}
getState() {
return this.state;
}
}
class TicTacToeBoard {
constructor(state=['', '', '', '', '', '', '', '', '']) {
this.state = state;
}
printFormattedBoard() {
let formattedString = '';
this.state.forEach((cell, index) => {
formattedString += cell ? ` ${cell} |` : ' |';
if((index + 1) % 3 == 0) {
formattedString = formattedString.slice(0,-1);
if(index < 8) formattedString += '\n\u2015\u2015\u2015 \u2015\u2015\u2015 \u2015\u2015\u2015\n';
}
});
console.log(formattedString);
}
insert(symbol, position) {
if(position > 8 || this.state[position]) return false; //Cell is either occupied or does not exist
this.state[position] = symbol;
return true;
}
createMemento() {
return new Memento(this.state.slice());
}
setMemento(memnto) {
this.state = memnto.getState();
}
}
const board = new TicTacToeBoard(['x','o','','x','o','','o','','x']);
board.printFormattedBoard();
const checkpoints = [];
checkpoints.push(board.createMemento());
board.insert('o', 2);
board.printFormattedBoard();
// Undo previous move
board.setMemento(checkpoints.pop());
board.printFormattedBoard();
Observer
observer.js
const fs = require('fs');
// Manages a Subject interactions with observer
class SubjectManager {
constructor() {
this._observers = {};
}
addObserver(eventType, observer) {
if(!this._observers[eventType]) {
this._observers[eventType] = [];
}
this._observers[eventType].push(observer);
}
removeObserver(eventType, observer) {
const idx = this._observers[eventType].indexOf(observer);
if(idx > -1){
this._observers[eventType].splice(idx);
}
}
notify(eventType, data) {
this._observers[eventType].forEach(observer => observer.update(data));
}
}
class FileManager {
constructor() {
this.subjectManager = new SubjectManager();
}
monitorFile(path) {
// eslint-disable-next-line node/no-unsupported-features/es-syntax
fs.watch(path, (data) => this.subjectManager.notify("change", {path, data}));
}
addObserver(eventType, observer) {
this.subjectManager.addObserver(eventType, observer);
}
removeObserver(eventType, observer) {
this.subjectManager.removeObserver(eventType, observer);
}
}
class LoggingObserver {
update({ data }) {
console.log(data);
}
}
class SizeChangeObserver {
constructor() {
this.size = 0;
}
update({ path }) {
const newSize = fs.statSync(path).size;
if(newSize > this.size) {
console.log("size increase to " + newSize);
this.size = newSize;
}
}
}
// Subject class
const fileManager = new FileManager();
// Adding observers
fileManager.addObserver("change", new LoggingObserver());
fileManager.addObserver("change", new SizeChangeObserver());
fileManager.monitorFile(process.argv[1]);
State
state.js
const net = require('net');
// Socket Object
// Internal State depends on underlying tcp connection
// Can be readt connect error, close
class Socket {
constructor() {
this.state = new ReadyState(this);
}
connect(url) {
this.state.connect(url);
}
setState(state) {
this.state = state;
}
printState() { console.log("State is ", this.state); }
}
class State {
connect() { }
}
class ReadyState extends State {
constructor(socket) {
super();
this.socket = socket;
}
connect(url) {
const connection = net.createConnection({
host: url,
port: "80"
});
connection.on('error', () => this.socket.setState(new ErrorState(this.socket)) );
connection.on('connect', () => this.socket.setState(new ConnectState(this.socket)));
connection.on('close', () => this.socket.setState(new CloseState(this.socket)));
}
}
class ErrorState extends State {
constructor(socket) {
super();
this.socket = socket;
}
connect() {
console.log("cannot connect in error state");
}
}
class ConnectState extends State {
constructor(socket) {
super();
this.socket = socket;
}
}
class CloseState extends State {
constructor(socket) {
super();
this.socket = socket;
}
}
const socket = new Socket();
const url = "www.example.com";
socket.connect(url);
socket.printState();
// After some time state changes to conenct
setTimeout(() => socket.printState(), 1000);
Strategy
strategy.js
/**
* ApiRequestFactory gives underlying startegies for Ali request
*/
const ApiRequestFactory = require("./lib");
class UpstreamFile {
getFileUpstream() { }
}
/**
* Here by default Stategy for API request is http
*/
class Config extends UpstreamFile {
constructor(url, apiRequest) {
super();
this.url = url;
this.apiRequest = apiRequest || ApiRequestFactory.createApiRequest("http");
}
getFileUpstream() {
this.apiRequest
.makeGetRequest(this.url)
.then(response => console.log(response))
.catch(err => console.log(err));
}
}
/**
* AbstractFactory is used to generate related implementation for these
* classes
*/
const config = new Config("jsonplaceholder.typicode.com/posts/1");
// Using default config http
config.getFileUpstream();
const config2 = new Config("https://jsonplaceholder.typicode.com/todos/1",
ApiRequestFactory.createApiRequest("tcp"));
// Get tcp strategy
config2.getFileUpstream();
Template Method
templateMethod.js
// Base template class which computes from buffer
class DataBuffer {
constructor(data) {
this.data = data;
}
sanitize(data) {
return data;
}
checkForErrors() {
return false;
}
compute() {
// Hook to check for errors
const isError = this.checkForErrors(this.data);
if(isError) {
return -1;
}
// Hook to sanitize buffer
const santizeBuffer = this.sanitize(this.data);
return santizeBuffer.byteLength;
}
}
class WordBuffer extends DataBuffer {
constructor(data) {
super(data);
}
// Gets only first word
sanitize(data) {
return Buffer.from(data.toString().split(" ")[0]);
}
}
const generalBuffer = new DataBuffer(Buffer.from('abc def'));
console.log(generalBuffer.compute());
// Uses Sanitize hook to remove all other words
const wordBuffer = new WordBuffer(Buffer.from('abc def'));
console.log(wordBuffer.compute());
Visitor
visitor.js
class ContentBuffer {
getSize() { }
}
// Composite Class has children as filebuffer
class DirBuffer extends ContentBuffer {
constructor() {
super();
this.bufferList = [];
}
getSize() {
return this.bufferList.map(buf => buf.getSize()).reduce((a, b) => a + b);
}
addBuffer(buf) {
this.bufferList.push(buf);
}
accept(visitor) {
this.bufferList.map(buf => buf.accept(visitor));
return visitor.visitDirBuffer(this);
}
}
// Leaf class
class FileBuffer extends ContentBuffer {
setBuffer(buf) {
this.buf = buf;
return this;
}
getSize() {
return this.buf.length || 0;
}
accept(visitor) {
visitor.visitFileBuffer(this);
}
}
// Implement all the alogorithm in visitor
class ByteCodeVisitor {
constructor() {
this.accumlate = 0;
}
visitDirBuffer() {
return this.accumlate;
}
visitFileBuffer(fileBuffer) {
this.accumlate += fileBuffer.buf.byteLength;
}
}
const file1 = new FileBuffer().setBuffer(Buffer.from("hello"));
const file2 = new FileBuffer().setBuffer(Buffer.from("hello2"));
const compositeObj = new DirBuffer();
compositeObj.addBuffer(file1);
compositeObj.addBuffer(file2);
console.log(compositeObj.getSize());
const visitor = new ByteCodeVisitor();
console.log(compositeObj.accept(visitor));