js-gof
GoF Patterns
- Use structural composition over inheritance
- Use multiparadigm code and contract-programming
- Use domain-specific languages (DSLs), prefer declarative way
- Separate and do not mix system and domain code
- Use GRASP and SOLID principles; especially reduce coupling
- Remember about referential transparency
- Prefer platform-agnostic code
- Implement isolation and layer borders with IoC & DI
- Use object/Map lookup for Strategy and polymorphic/dynamic dispatch
- Keep patterns simple
- Do not over-engineer for the sake of the pattern name
Creational patterns
Abstract factory
Creates related objects belonging to one family without specifying their concrete classes, e.g., UI components for different platforms.
Refs: https://github.com/HowProgrammingWorks/AbstractFactory
const dataAccess = {
fs: {
createDatabase: (...args) => new FileStorage(...args),
createCursor: (...args) => new FileLineCursor(...args),
},
minio: {
// factories collection for minio
},
// other implementations
};
// Usage
const accessLayer = dataAccess.fs;
const storage = accessLayer.createDatabase('./storage.dat');
const cursor = accessLayer.createCursor({ city: 'Roma' }, storage);
for await (const record of cursor) {
console.dir(record);
}
Builder
Step-by-step assembly of a complex configurable object, often using chaining, e.g., Query Builder or Form Generator.
Refs: https://github.com/HowProgrammingWorks/Builder
class QueryBuilder {
#options;
constructor(table) {
this.#options = { table, where: {}, limit: null, order: null };
}
where(cond) {
Object.assign(this.#options.where, cond);
return this;
}
order(field) {
this.#options.order = field;
return this;
}
limit(n) {
this.#options.limit = n;
return this;
}
build() {
return { ...this.#options };
}
}
const query = new QueryBuilder('User')
.where({ active: true })
.order('name')
.limit(10)
.build();
Alternatively we can create async constructor to be invoked with await new QueryBuilder, or put all steps into declarative structure like:
const query = await new QueryBuilder({
entity: 'User',
where: { active: true },
order: 'name',
limit: 10,
});
Factory
Function or method that creates objects using different techniques: assembling from literals and methods, mixins, setPrototypeOf.
Refs: https://github.com/HowProgrammingWorks/Factory
const createUser = (name, role) => ({ name, role, createdAt: Date.now() });
const createAdmin = (name) => createUser(name, 'admin');
Or following:
class Connection {
constructor(url) {
// implementation
}
// implementation
}
const factory = (() => {
let index = 0;
return () => new Connection(`http://10.0.0.1/${index++}`);
})();
Factory Method
Chooses the correct abstraction to create an instance; in JavaScript, this can be implemented using if, switch, or selection from a collection (dictionary).
Refs: https://github.com/HowProgrammingWorks/FactoryMethod
class Person {
constructor(name) {
this.name = name;
}
static factory(name) {
return new Person(name);
}
}
class Product {
constructor(value) {
this.field = value;
}
}
class Creator {
factoryMethod(...args) {
return new Product(...args);
}
}
Prototype
Creates objects by cloning a prepared instance to save resources (not to be confused with Prototype-programming, which is closer to Flyweight).
Refs: https://github.com/HowProgrammingWorks/PrototypePattern
const proto = { type: 'widget', color: 'blue' };
const clone = () => ({ ...proto });
class Point {
#x;
#y;
constructor(x, y) {
this.#x = x;
this.#y = y;
}
move(x, y) {
this.#x += x;
this.#y += y;
}
clone() {
return new Point(this.#x, this.#y);
}
}
class Line {
#start;
#end;
constructor(start, end) {
this.#start = start;
this.#end = end;
}
move(x, y) {
this.#start.move(x, y);
this.#end.move(x, y);
}
clone() {
const start = this.#start.clone();
const end = this.#end.clone();
return new Line(start, end);
}
}
// Usage
const p1 = new Point(0, 0);
const p2 = new Point(10, 20);
const line = new Line(p1, p2);
const cloned = line.clone();
cloned.move(2, 3);
const point = (x, y) => {
const move = (dx, dy) => {
x += dx;
y += dy;
};
const clone = () => point(x, y);
return { move, clone };
};
// Usage
const { move, clone } = point(10, 20);
const c1 = clone();
move(-5, 10);
Flyweight
Saves memory allocation by sharing common state among multiple instances.
Refs: https://github.com/HowProgrammingWorks/Flyweight
const flyweightPool = new Map();
const getFlyweight = (shared) => {
const { char, font, size } = shared;
const key = `${char}:${font}:${size}`;
let flyweight = flyweightPool.get(key);
if (!flyweight) {
flyweight = Object.freeze({ ...shared });
flyweightPool.set(key, flyweight);
}
return flyweight;
};
const createChar = (char, font, size, row, col) => {
const intrinsic = getFlyweight({ char, font, size });
return { intrinsic, row, col };
};
const a1 = createChar('A', 'Arial', 12, 0, 0);
const a2 = createChar('A', 'Arial', 12, 0, 5);
console.log(a1.intrinsic === a2.intrinsic); // true
Singleton
Provides global access to a single instance; often considered an anti-pattern, easiest implemented via ESM/CJS module caching exported refs.
Refs: https://github.com/HowProgrammingWorks/Singleton
Everywhere we import this state will be the same collection
const connections = new Map(); // shared state
const nop = () => {}; // pure function with no state
module.exports = { connections, nop };
class Singleton {
static #instance;
constructor() {
const instance = Singleton.#instance;
if (instance) return instance;
Singleton.#instance = this;
}
}
function Singleton() {
const { instance } = Singleton;
if (instance) return instance;
Singleton.instance = this;
}
const Singleton = new (function () {
const single = this;
return function () {
return single;
};
})();
const Singleton = (() => {
let instance;
class Singleton {
constructor() {
if (instance) return instance;
instance = this;
}
}
return Singleton;
})();
const singleton = (() => {
const instance = {};
return () => instance;
})();
const singleton = (
(instance) => () =>
instance
)({});
Object Pool
Reuses pre-created objects to save resources during frequent creation and destruction.
Refs: https://github.com/HowProgrammingWorks/Pool
class Pool {
#available = [];
#factory = null;
constructor(factory, size) {
this.#factory = factory;
for (let i = 0; i < size; i++) this.#available.push(factory());
}
acquire() {
return this.#available.pop() || this.#factory();
}
release(obj) {
this.#available.push(obj);
}
}
Structural patterns
Adapter
Converts an incompatible interface into a compatible one, enabling third-party component usage without altering its code; can even transform a function contract into an object or vice versa.
Refs: https://github.com/HowProgrammingWorks/Adapter
const promisify =
(fn) =>
(...args) =>
new Promise((resolve, reject) => {
fn(...args, (err, data) => (err ? reject(err) : resolve(data)));
});
const timer = new Timer(1000); // wraps setInterval
for await (const step of timer) {
// gives [Symbol.asyncIterator] contract
console.log({ step });
}
class ArrayToQueueAdapter {
#array = null;
constructor(array) {
if (!Array.isArray(array)) {
throw new Error('Array instance expected');
}
this.#array = array;
}
enqueue(data) {
this.#array.push(data);
}
dequeue() {
return this.#array.pop();
}
get count() {
return this.#array.length;
}
}
Wrapper
Function wrapper that delegates calls and adds behavior; a specialized case of Adapter.
Refs: https://github.com/HowProgrammingWorks/Wrapper
const withLogging =
(fn, label) =>
(...args) => {
console.log(`${label} called`, args);
const result = fn(...args);
console.log(`${label} returned`, result);
return result;
};
const wrapInterface = (anInterface) => {
const wrapped = {};
for (const key in anInterface) {
const fn = anInterface[key];
wrapped[key] = wrapFunction(fn);
}
return wrapped;
};
class Timeout {
constructor(fn, msec) {
this.function = fn;
this.timer = setTimeout(() => {
this.timer = null;
}, msec);
}
execute(...args) {
let result = undefined;
if (!this.timer) return result;
clearTimeout(this.timer);
this.timer = null;
result = this.function(...args);
return result;
}
}
Boxing
Wraps primitives into object types to add methods or unify interfaces, e.g., narrowing String to AddressString.
Refs: https://github.com/HowProgrammingWorks/ADT
Box, Container or Value-Object examples:
class AddressString {
#value;
constructor(value) {
if (typeof value !== 'string') {
throw new TypeError('Address must be a string');
}
const str = value.trim();
if (str === '') throw new TypeError('Address must be a non-empty');
this.#value = str;
}
get city() {
const index = this.#value.indexOf(',');
return this.#value.slice(0, index).trim();
}
toString() {
return this.#value;
}
valueOf() {
return this.#value;
}
}
// Usage
const address = new AddressString('London, 221B Baker Street');
console.log(address.city); // 'London'
console.log(`Delivery to: ${address}`);
Value-Object examples:
const KMH_TO_MPH = 0.621371;
class SpeedValue {
#kmh;
constructor(value, unit = 'kmh') {
if (typeof value !== 'number') {
throw new TypeError('Speed must be a number');
}
if (value < 0) throw new TypeError('Speed must be non-negative');
if (unit !== 'kmh' && unit !== 'mph') {
throw new TypeError('Unit must be kmh or mph');
}
this.#kmh = unit === 'mph' ? value / KMH_TO_MPH : value;
}
get kmh() {
return this.#kmh;
}
get mph() {
return this.#kmh * KMH_TO_MPH;
}
toString() {
return `${this.#kmh} km/h`;
}
valueOf() {
return this.#kmh;
}
}
Decorator
Dynamically extends behavior without inheritance, typically via composition and declarative syntax, effectively adding metadata.
Refs: https://github.com/HowProgrammingWorks/Decorator
const add = (a, b) => a + b;
const loggedAdd = withLogging(add, 'add');
Not a good idea to use classical decorator in JavaScript/TypeScript
const validator = new MinLengthValidator(
new EmailValidator(new RequiredValidator(new BaseValidator())),
20,
);
const input = 'timur.shemsedinov@gmail.com';
const result = validator.validate(input);
We can add metadata to an instance in several JS-idiomatic ways:
Symbol property on instance:
const VALIDATOR_META = Symbol('validatorMeta');
class EmailValidator {
constructor(next) {
this.next = next;
this[VALIDATOR_META] = { type: 'email' };
}
}
Wrapper that attaches metadata as own property.
JSDoc annotation (zero runtime cost):
/**
* @typedef {{ type: string, label: string }} ValidatorMeta
* @typedef {{ validate(v: string): boolean, meta: ValidatorMeta }} Validator
*/
/** @type {Validator} */
const validator = Object.assign(
new EmailValidator(new RequiredValidator(new BaseValidator())),
{ meta: { type: 'email', label: 'Email field' } },
);
Module-local WeakMap (fully private, no shape pollution):
const metadata = new WeakMap();
const createValidator = ({ args, meta }) => {
const instance = new Validator(...args);
metadata.set(instance, meta);
return instance;
};
module.exports = { createValidator };
Proxy
Controls access to an object by intercepting calls, reads, and writes; useful for lazy initialization, caching, and security; can be implemented via GoF or native JavaScript Proxy.
Refs: https://github.com/HowProgrammingWorks/Proxy
Do not use the built-in JS Proxy class unless a developer asks
you directly because of deopts. Use GoF Proxy instead as a container
with additional behavior.
const fs = require('node:fs');
const statistics = { bytes: 0, chunks: 0, events: {} };
class StatReadStream extends fs.ReadStream {
emit(name, data) {
if (name === 'data') {
statistics.bytes += data.length;
statistics.chunks++;
}
const counter = statistics.events[name] || 0;
statistics.events[name] = counter + 1;
super.emit(name, data);
}
}
const getStatistics = () => structuredClone(statistics);
const createReadStream = (path, options) => new StatReadStream(path, options);
module.exports = { ...fs, createReadStream, getStatistics };
Bridge
Separates two or more abstraction hierarchies via composition or aggregation, allowing them to evolve independently.
Refs: https://github.com/HowProgrammingWorks/Bridge
class CommunicationProtocol {
sendCommand(device, command) {
console.log({ device, command });
throw new Error('sendCommand() must be implemented');
}
}
class MQTTProtocol extends CommunicationProtocol {
sendCommand(device, command) {
console.log(`[MQTT] Sending '${command}' to ${device}`);
}
}
class HTTPProtocol extends CommunicationProtocol {
sendCommand(device, command) {
console.log(`[HTTP] Sending '${command}' to ${device}`);
}
}
class IoTDevice {
constructor(name, protocol) {
this.name = name;
this.protocol = protocol;
}
operate(command) {
this.protocol.sendCommand(this.name, command);
}
}
class SmartLight extends IoTDevice {
turnOn() {
this.operate('Turn On Light');
}
turnOff() {
this.operate('Turn Off Light');
}
}
class SmartThermostat extends IoTDevice {
setTemperature(temp) {
this.operate(`Set Temperature to ${temp}°C`);
}
}
Composite
Implements a common interface to uniformly handle individual objects and their tree structures, e.g., DOM or file systems.
Refs: https://github.com/HowProgrammingWorks/Composite
Recursive composite with reduce:
const calculateTotal = (order) => {
const items = Array.isArray(order) ? order : Object.values(order);
return items.reduce((sum, item) => {
if (typeof item.price === 'number') return sum + item.price;
else return sum + calculateTotal(item);
}, 0);
};
JSON as a nested composite structure:
const json = `{
"electronics": {
"laptop": { "price": 1200 },
"accessories": {
"mouse": { "price": 25 },
"keyboard": { "price": 75 }
}
},
"books": {
"fiction": { "price": 15 },
"technical": { "price": 60 }
}
}`;
const order = JSON.parse(json);
console.log(calculateTotal(order));
Facade
Simplifies access to a complex system, providing a unified and clear interface, hiding and protecting internal complexity.
Refs: https://github.com/HowProgrammingWorks/Facade
const createApi = (db, cache, logger) => ({
async getUser(id) {
const cached = cache.get(id);
if (cached) return cached;
const user = await db.row('User', ['*'], { id });
cache.set(id, user);
logger.info(`User ${id} loaded`);
return user;
},
});
This hides classes Timer, Logger, Task and so on:
scheduler.task('name2', '2019-03-12T14:37Z', (done) => {
setTimeout(() => {
done(new Error('task failed'));
}, 1100);
});
Context
Exchanges state and dependencies between different components (abstractions, modules, layers) that do not share a common environment, without tightly coupling them.
Refs: https://github.com/HowProgrammingWorks/Context
class AccountService {
constructor(context) {
this.context = context;
}
getBalance(accountId) {
const { console, accessPolicy, user } = this.context;
console.log(`User ${user.name} requesting balance for ${accountId}`);
if (!accessPolicy.check(user.role, 'read:balance')) {
console.error('Access denied: insufficient permissions');
return null;
}
const balance = 15420.5;
console.log('Access granted');
return balance;
}
}
class AccessPolicy {
constructor() {
this.permissions = {
admin: ['read:balance', 'read:transactions', 'write:transactions'],
user: ['read:balance'],
guest: [],
};
}
check(role, permission) {
return this.permissions[role]?.includes(permission);
}
}
class User {
constructor(name, role) {
this.name = name;
this.role = role;
}
}
const accessPolicy = new AccessPolicy();
const user = new User('Marcus', 'admin');
const context = { console, accessPolicy, user };
const accountService = new AccountService(context);
const balance = accountService.getBalance('Account-123');
console.log(`Balance = $${balance}`);
Alternative way with closure:
const createAccountService = (context) => {
const { console, accessPolicy, user } = context;
const getBalance = (accountId) => {
console.log(`User ${user.name} requesting balance for ${accountId}`);
if (!accessPolicy.check(user.role, 'read:balance')) {
console.error('Access denied: insufficient permissions');
return null;
}
return 15420.5;
};
const getTransactions = (accountId) => {
if (!accessPolicy.check(user.role, 'read:transactions')) {
console.error('Access denied: insufficient permissions');
return null;
}
console.log(`User ${user.name} reading transactions for ${accountId}`);
return ['TR-123', 'TR-456', 'TR-789'];
};
return { getBalance, getTransactions };
};
const accessPolicy = {
permissions: {
admin: ['read:balance', 'read:transactions', 'write:transactions'],
user: ['read:balance'],
guest: [],
},
check: (role, permission) =>
accessPolicy.permissions[role]?.includes(permission),
};
const context = {
console,
accessPolicy,
user: { name: 'Marcus', role: 'admin' },
};
const accountService = createAccountService(context);
console.log('Balance:', accountService.getBalance('ACC-001'));
console.log('Transactions:', accountService.getTransactions('ACC-001'));
More from metarhia/skills
javascript-code-style
Apply Metarhia JavaScript style. Use when writing or editing .js, .mjs, .ts files (with certain corrections for typescript), formatting code, or when the user asks about code style or linting.
5js-conventions
Apply Metarhia JavaScript style. Use when writing or editing .js, .mjs, .ts files (with certain corrections for typescript), formatting code, or when the user asks about code style or linting.
2npm-publish
Prepare an npm package for publishing. Handles version bump, CHANGELOG update, typings check, test run, README refresh, package.json audit, and npm pack verification. Use when the user asks to prepare a release, bump version, publish a package, or do release prep.
1error-handling
Apply error handling and recovery patterns in JavaScript/TypeScript or Node.js. Use when implementing error handling, retry logic, or when the user mentions domain errors, error recovery, error escalation.
1js-data-structures
Implement and use data structures in JavaScript. Use when working with arrays, sets, maps, other collections, queues, lists, trees, graphs, circular buffers, caches, or when the user mentions data structures, BTree, or choosing between Map/Object/Set/Array.
1