Design Patterns in JavaScript: Advanced Level with Node.js Examples
Design patterns are proven architectural solutions for typical tasks. If you're already familiar with basic patterns (Singleton, Factory, Observer), it's time to dive deeper into more complex structures. In this article, we'll explore four advanced GoF patterns using JavaScript (Node.js) examples: Composite, Chain of Responsibility, State, and Memento. These patterns will help you write flexible, scalable, and maintainable code.
1. Composite
Purpose: Allows grouping objects into a tree structure and working with each object or group uniformly.
Example: A file system where a folder can contain files and other folders. We want to calculate the total size.
// Componentclass FileSystemComponent { getSize() { throw new Error('getSize() method must be implemented'); }}
// Leafclass File extends FileSystemComponent { constructor(name, size) { super(); this.name = name; this.size = size; }
getSize() { return this.size; }}
// Container (node)class Folder extends FileSystemComponent { constructor(name) { super(); this.name = name; this.children = []; }
add(component) { this.children.push(component); }
getSize() { return this.children.reduce((total, child) => total + child.getSize(), 0); }}
// Usageconst root = new Folder('root');const src = new Folder('src');const index = new File('index.js', 500);const utils = new File('utils.js', 300);
src.add(index);src.add(utils);root.add(src);root.add(new File('package.json', 200));
console.log(`Total size: ${root.getSize()} bytes`); // 1000 bytesAdvantages: Simplifies working with trees, hides hierarchy complexity from the client.
2. Chain of Responsibility
Purpose: Allows passing a request along a chain of handlers until one of them processes the request.
Example: HTTP request processing in Express.js (middleware).
class Handler { setNext(handler) { this.next = handler; return handler; }
handle(request) { if (this.next) { return this.next.handle(request); } return null; }}
class AuthHandler extends Handler { handle(request) { if (!request.token) { return 'Error: unauthorized'; } console.log('AuthHandler: check passed'); return super.handle(request); }}
class RoleHandler extends Handler { handle(request) { if (request.role !== 'admin') { return 'Error: insufficient permissions'; } console.log('RoleHandler: role confirmed'); return super.handle(request); }}
class LoggingHandler extends Handler { handle(request) { console.log(`LoggingHandler: request from ${request.user}`); return super.handle(request); }}
// Building the chainconst auth = new AuthHandler();const role = new RoleHandler();const log = new LoggingHandler();auth.setNext(role).setNext(log);
const request = { token: 'abc123', role: 'admin', user: 'Ivan' };const result = auth.handle(request);console.log(result); // null (all good)
const badRequest = { token: 'abc123', role: 'user', user: 'Petr' };console.log(auth.handle(badRequest)); // 'Error: insufficient permissions'Advantages: Reduces coupling, easy to add/remove handlers.
3. State
Purpose: Allows an object to change its behavior depending on its internal state.
Example: A document with states: Draft, Under moderation, Published.
class Docu