Advanced Design Patterns in JavaScript: From Theory to Practice
\n\nDesign patterns are proven solutions to common problems in software development. For a JavaScript developer working with Node.js, understanding advanced patterns opens the path to creating scalable, flexible, and easily maintainable systems. In this article, we will examine four key patterns: Observer, Proxy, Chain of Responsibility, and Strategy. Each will be illustrated with a practical example in Node.js.
\n\n1. Observer Pattern: Event-Driven Architecture in Node.js
\n\nObserver is one of the fundamental behavioral patterns. It defines a one-to-many dependency between objects: when one object (the subject) changes its state, all dependent objects (observers) are automatically notified and updated. In Node.js, this pattern is the foundation of the EventEmitter module.
Example: notification system for an online store
\nImagine we have a server that manages orders. When an order status changes, several subsystems need to be notified: logging, email sending, and analytics updates.
\n\nconst EventEmitter = require('events');\n\n// Subject\nclass OrderManager extends EventEmitter {\n constructor() {\n super();\n this.orders = [];\n }\n\n createOrder(orderId, items) {\n const order = { orderId, items, status: 'created', createdAt: new Date() };\n this.orders.push(order);\n this.emit('order:created', order);\n return order;\n }\n\n updateStatus(orderId, newStatus) {\n const order = this.orders.find(o => o.orderId === orderId);\n if (order) {\n order.status = newStatus;\n this.emit('order:statusChanged', order);\n }\n }\n}\n\n// Observers\nfunction logObserver(order) {\n console.log(`[LOG] Order ${order.orderId}: status changed to ${order.status} at ${new Date().toISOString()}`);\n}\n\nfunction emailObserver(order) {\n console.log(`[EMAIL] Sending notification for order ${order.orderId} with status ${order.status}`);\n}\n\nfunction analyticsObserver(order) {\n console.log(`[ANALYTICS] Recording event: order ${order.orderId} -> ${order.status}`);\n}\n\n// Usage\nconst manager = new OrderManager();\nmanager.on('order:created', logObserver);\nmanager.on('order:statusChanged', logObserver);\nmanager.on('order:statusChanged', emailObserver);\nmanager.on('order:statusChanged', analyticsObserver);\n\nmanager.createOrder('ORD-001', ['Book', 'Pen']);\nmanager.updateStatus('ORD-001', 'shipped');\n\n\nThis approach allows adding or removing observers without changing the subject's code, making the system flexible and easily extensible.
\n\n2. Proxy Pattern: Access Control and Caching
\n\nProxy provides a surrogate object that controls access to another object. In JavaScript, this pattern is implemented through the built-in Proxy object, which allows intercepting and redefining fundamental operations (property access, function calls, etc.).
Example: caching proxy for API requests
\nSuppose we have a service that makes expensive requests to an external API. We want to cache the results to avoid repeated calls.
\n\n// Real Subject\nclass ExternalApiService {\n async fetchUser(id) {\n console.log(`Fetching user ${id} from external API...`);\n // Simulating network delay\n await new Promise(resolve => setTimeout(resolve, 1000));\n return { id, name: `User ${id}`, email: `user${id}@example.com` };\n }\n}\n\n// Proxy\nclass CachingProxy {\n constructor(realService) {\n this.realService = realService;\n this.cache = new M