Design Patterns in JavaScript with Examples: From A to Z
Design patterns are proven solutions to common problems that arise during software development. In JavaScript, especially in the Node.js environment, understanding these patterns helps write cleaner, more modular, and scalable code. In this article, we will break down the main categories of patterns with clear examples.
1. Creational Patterns
These patterns manage the object creation process, making the system independent of how objects are instantiated.
1.1. Singleton
Ensures a class has only one instance and provides a global point of access to it. In Node.js, modules are cached by default, which simplifies the implementation.
class DatabaseConnection { constructor() { if (DatabaseConnection.instance) { return DatabaseConnection.instance; } this.connection = null; DatabaseConnection.instance = this; }
connect() { if (!this.connection) { this.connection = { status: 'connected' }; console.log('New DB connection'); } return this.connection; }}
const db1 = new DatabaseConnection();const db2 = new DatabaseConnection();console.log(db1 === db2); // true1.2. Factory Method
Defines an interface for creating an object but lets subclasses alter the type of created object. It is ideal for creating different types of loggers.
class LoggerFactory { createLogger(type) { switch (type) { case 'console': return new ConsoleLogger(); case 'file': return new FileLogger(); default: throw new Error('Unknown logger type'); } }}
class ConsoleLogger { log(message) { console.log(message); }}
class FileLogger { log(message) { console.log(`[File]: ${message}`); }}
const factory = new LoggerFactory();const logger = factory.createLogger('console');logger.log('Test message');2. Structural Patterns
Describe ways to combine classes and objects to form larger structures.
2.1. Decorator
Dynamically adds new responsibilities to an object. In JavaScript, decorators are often implemented via higher-order functions or prototypes.
class Coffee { cost() { return 10; } description() { return 'Coffee'; }}
function withMilk(coffee) { coffee.cost = () => coffee.cost() + 5; coffee.description = () => coffee.description() + ', with milk'; return coffee;}
function withSugar(coffee) { coffee.cost = () => coffee.cost() + 2; coffee.description = () => coffee.description() + ', with sugar'; return coffee;}
let myCoffee = new Coffee();myCoffee = withMilk(myCoffee);myCoffee = withSugar(myCoffee);console.log(`${myCoffee.description()}: ${myCoffee.cost()} rub.`); // Coffee, with milk, with sugar: 17 rub.2.2. Adapter
Converts the interface of one class into another interface expected by the client. It is especially useful when integrating third-party libraries.
class OldAPI { getUserData() { return { name: 'Ivan', age: 25, email: 'ivan@example.com' }; }}
class NewAPI { fetchUser() { return { fullName: 'Ivan Petrov', yearsOld: 26, contact: 'ivan@example.com' }; }}
class UserAdapter { constructor(api) { this.api = api; }
getUserData() { const data = this.api.fetchUser(); return { name: data.fullName, age: data.yearsOld, email: data.contact }; }}
const adapter = new UserAdapter(new NewAPI());console.log(adapter.getUserData()); // { name: 'Ivan Petrov', age: 26, email: 'ivan@example.com' }3. Behavioral Patterns
Define algorithms