When it comes to writing clean, maintainable, and efficient code, design patterns play a crucial role in the world of software development. Design patterns are reusable solutions to common problems that developers face while designing and building software systems. They provide a structured approach to solving specific challenges, making it easier to create code that is not only robust but also easier to understand and maintain.
In Object-Oriented Programming (OOP), design patterns serve as guidelines for structuring your code in a way that promotes flexibility, reusability, and scalability. They encapsulate best practices and design principles that have evolved, and distilled into proven solutions.
Design patterns can be categorized into three main groups:
Creational Patterns: These patterns focus on object creation mechanisms, trying to create objects in a manner suitable for the situation. They abstract the instantiation process, making it more flexible and independent of the system.
Structural Patterns: Structural patterns deal with object composition, forming relationships between objects to create larger, more complex structures. They help to define how objects and classes can be combined to form new structures and provide new functionality.
Behavioral Patterns: Behavioral patterns are concerned with communication between objects, defining how they interact and distribute responsibilities. These patterns help you design systems where objects collaborate in a more flexible and efficient manner.
Here’s a list of some common design patterns in each category:
Observer Pattern: Defines a one-to-many dependency between objects, so when one object changes state, all its dependents are notified and updated automatically.
Strategy Pattern: Defines a family of algorithms, encapsulates each one, and makes them interchangeable.
Command Pattern: Encapsulates a request as an object, thereby allowing for parameterization of clients with queues, requests, and operations.
State Pattern: Allows an object to alter its behavior when its internal state changes, wrapping the behavior in separate classes.
Chain of Responsibility Pattern: Passes the request along a chain of handlers, allowing each handler to decide either to process the request or to pass it to the next handler in the chain.
Visitor Pattern: This represents an operation to be performed on the elements of an object structure, enabling you to define new operations without changing the classes of the elements.
In this blog, we will delve into each of these design patterns, providing explanations, real-world use cases, and JavaScript code examples to help you understand and implement them effectively in your projects.
The Singleton Pattern is a creational design pattern that ensures a class has only one instance and provides a global point of access to that instance. This pattern is especially useful when you want to limit the number of instances of a class in your application and control access to a single shared instance.
In JavaScript, implementing the Singleton Pattern is relatively straightforward, thanks to the flexibility of the language. Let’s dive into a simple example of how to create a Singleton in JavaScript.
// Singleton instance
let instance = null;
class Singleton {
constructor() {
if (!instance) {
instance = this;
// Your initialization code here
} else {
return instance;
}
} // Your methods and properties here
}// Usage
const singletonA = new Singleton();
const singletonB = new Singleton();
console.log(singletonA === singletonB); // Output: true (both variables reference the same instance)
In this example, we create a Singleton class with a constructor that checks if an instance already exists. If an instance doesn’t exist, it creates one and assigns it to the instance variable. Subsequent calls to the constructor return the existing instance, ensuring that there’s only one instance of the Singleton class.
The Singleton Pattern is useful in various scenarios, including:
While the Singleton Pattern can be beneficial, it’s essential to use it judiciously. Overusing the Singleton pattern can lead to tightly coupled code and global state, which may make your application harder to maintain and test. Therefore, it’s crucial to weigh the pros and cons and apply the pattern where it genuinely adds value to your codebase.
The Factory Pattern and the Abstract Factory Pattern are creational design patterns that deal with the creation of objects, but they do so in different ways and serve distinct purposes. Let’s explore each of these patterns and see how they can be implemented in JavaScript.
The Factory Pattern is a creational pattern that provides an interface for creating objects but allows subclasses to alter the type of objects that will be created. It encapsulates the object creation process, making it more flexible and decoupled from the client code.
// Product class
class Product {
constructor(name) {
this.name = name;
}
}
// Factory for creating products
class ProductFactory {
createProduct(name) {
return new Product(name);
}
}// Usage
const factory = new ProductFactory();
const productA = factory.createProduct('Product A');
const productB = factory.createProduct('Product B');console.log(productA.name); // Output: 'Product A'
console.log(productB.name); // Output: 'Product B'
In this example, the ProductFactory is responsible for creating instances of the Product class. It abstracts the creation process, allowing you to create different types of products by extending the factory.
The Abstract Factory Pattern is another creational pattern that provides an interface for creating families of related or dependent objects without specifying their concrete classes. It allows you to create sets of objects that work together harmoniously.
// Abstract Product classes
class Button {
render() {}
}
class Checkbox {
render() {}
}// Concrete Product classes
class MacButton extends Button {
render() {
return 'Render Mac button';
}
}class MacCheckbox extends Checkbox {
render() {
return 'Render Mac checkbox';
}
}class WindowsButton extends Button {
render() {
return 'Render Windows button';
}
}class WindowsCheckbox extends Checkbox {
render() {
return 'Render Windows checkbox';
}
}// Abstract Factory interface
class GUIFactory {
createButton() {}
createCheckbox() {}
}// Concrete Factories
class MacFactory extends GUIFactory {
createButton() {
return new MacButton();
} createCheckbox() {
return new MacCheckbox();
}
}class WindowsFactory extends GUIFactory {
createButton() {
return new WindowsButton();
} createCheckbox() {
return new WindowsCheckbox();
}
}// Usage
function createUI(factory) {
const button = factory.createButton();
const checkbox = factory.createCheckbox(); return { button, checkbox };
}const macUI = createUI(new MacFactory());
console.log(macUI.button.render()); // Output: 'Render Mac button'
console.log(macUI.checkbox.render()); // Output: 'Render Mac checkbox'const windowsUI = createUI(new WindowsFactory());
console.log(windowsUI.button.render()); // Output: 'Render Windows button'
console.log(windowsUI.checkbox.render()); // Output: 'Render Windows checkbox'
In this example, we have two concrete factories, MacFactory and WindowsFactory, each capable of creating a set of related UI components (buttons and checkboxes) for their respective platforms. The createUI function allows you to create a cohesive UI for a specific platform using the appropriate factory.
When to Use Which Pattern:
The Builder Pattern is a creational design pattern that separates the construction of a complex object from its representation, allowing the same construction process to create different representations. This pattern is especially useful when you have an object with a large number of properties, and you want to simplify the creation of instances while maintaining flexibility.
In JavaScript, the Builder Pattern is often implemented using a builder class or object that guides the step-by-step construction of the complex object. Let’s dive into an example to understand how it works.
// Product class with multiple properties
class Product {
constructor() {
this.name = '';
this.price = 0;
this.color = 'white';
// ... other properties
}
// Additional methods can be defined here
}// Builder for creating Product instances
class ProductBuilder {
constructor() {
this.product = new Product();
} setName(name) {
this.product.name = name;
return this; // Return the builder for method chaining
} setPrice(price) {
this.product.price = price;
return this;
} setColor(color) {
this.product.color = color;
return this;
} // Other methods to set additional properties build() {
return this.product; // Return the fully constructed product
}
}// Usage
const builder = new ProductBuilder();const productA = builder
.setName('Product A')
.setPrice(99.99)
.setColor('blue')
.build();const productB = builder
.setName('Product B')
.setPrice(49.99)
.build();console.log(productA);
console.log(productB);
In this example, we have a Product class with multiple properties. The ProductBuilder class helps create instances of Product by providing methods to set each property step by step. Method chaining allows you to set multiple properties in a fluent and readable way. Finally, the build method returns the fully constructed Product instance.
The Builder Pattern is beneficial in various scenarios, including:
While the Builder Pattern offers many advantages, it’s important to note that it adds complexity to your codebase, especially if the objects being constructed are relatively simple. Therefore, it’s essential to evaluate whether the complexity introduced by the Builder is justified for your specific use case.
The Prototype Pattern is a creational design pattern that allows you to create new objects by copying an existing object, known as the prototype. It promotes the creation of objects without specifying the exact class of object to create. This pattern is particularly useful when you want to create instances of complex objects efficiently.
In JavaScript, the Prototype Pattern is closely related to the built-in prototype
property and the Object.create()
method. Let's explore how to implement and use the Prototype Pattern in JavaScript.
// Prototype object
const vehiclePrototype = {
init(make, model) {
this.make = make;
this.model = model;
},
getDetails() {
return `${this.make} ${this.model}`;
},
};
// Create new instances using the prototype
const car1 = Object.create(vehiclePrototype);
car1.init('Toyota', 'Camry');const car2 = Object.create(vehiclePrototype);
car2.init('Honda', 'Civic');console.log(car1.getDetails()); // Output: 'Toyota Camry'
console.log(car2.getDetails()); // Output: 'Honda Civic'
In this example, we define a vehiclePrototype
object with methods and properties common to all vehicles. We use Object.create()
to create new instances (car1 and car2) based on this prototype. These instances inherit the properties and methods from the prototype, allowing you to create new objects with shared behavior efficiently.
The Prototype Pattern is valuable in various scenarios, including:
While the Prototype Pattern is useful, it has some considerations:
The Object Pool Pattern is a creational design pattern that manages a pool of reusable objects to minimize the overhead of object creation and destruction. It’s especially useful when creating and destroying objects is expensive or resource-intensive. The Object Pool Pattern helps improve performance and resource utilization by recycling and reusing objects instead of creating new ones from scratch.
In JavaScript, you can implement the Object Pool Pattern using arrays or custom pool management classes. Let’s explore how this pattern works with a simple example.
class ObjectPool {
constructor(maxSize) {
this.maxSize = maxSize;
this.pool = [];
}
create() {
if (this.pool.length < this.maxSize) {
// Create a new object and add it to the pool
const obj = { /* Your object initialization code here */ };
this.pool.push(obj);
return obj;
} else {
// Pool is full, cannot create more objects
console.log('Pool is full. Cannot create more objects.');
return null;
}
} reuse() {
if (this.pool.length > 0) {
// Reuse an object from the pool
return this.pool.pop();
} else {
// Pool is empty, no objects available for reuse
console.log('Pool is empty. No objects available for reuse.');
return null;
}
} release(obj) {
// Release an object back to the pool for reuse
this.pool.push(obj);
}
}// Usage
const pool = new ObjectPool(5); // Create a pool with a maximum size of 5 objectsconst obj1 = pool.create();
const obj2 = pool.create();
const obj3 = pool.create();pool.release(obj2); // Release obj2 back to the pool for reuseconst obj4 = pool.reuse(); // Reuse an object from the pool (obj2)
In this example, we create an ObjectPool class that manages a pool of objects. The create method creates new objects when the pool is not full, the reuse method retrieves an object from the pool for reuse, and the release method returns an object to the pool for future use.
The Object Pool Pattern is useful in various scenarios, including:
While the Object Pool Pattern offers performance benefits, it’s important to consider the following:
The Adapter Pattern is a structural design pattern that allows objects with incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces, making them compatible without changing their source code. This pattern is especially useful when you need to integrate or use existing code that doesn’t quite fit with the requirements of your application.
In JavaScript, the Adapter Pattern can be implemented using classes or functions that wrap or adapt the incompatible interface. Let’s explore how to implement and use the Adapter Pattern in JavaScript with a practical example.
Suppose you have an existing class called OldSystem
with a method called legacyRequest
:
class OldSystem {
legacyRequest() {
return 'Data from the legacy system';
}
}
Now, you want to use this legacy system in your modern application that expects a different interface. You can create an adapter class or function like this:
class Adapter {
constructor(oldSystem) {
this.oldSystem = oldSystem;
}
newRequest() {
const legacyData = this.oldSystem.legacyRequest();
// Adapt the data or perform any necessary transformations
return `Adapted: ${legacyData}`;
}
}
Now, you can use the Adapter class to make the legacy system compatible with your modern application:
const oldSystem = new OldSystem();
const adapter = new Adapter(oldSystem);
const result = adapter.newRequest();
console.log(result); // Output: 'Adapted: Data from the legacy system'
In this example, the Adapter class wraps the OldSystem and provides a new interface, newRequest, which is compatible with your modern application.
The Adapter Pattern is valuable in various scenarios, including:
While the Adapter Pattern provides flexibility and compatibility, it’s essential to consider a few points:
The Decorator Pattern is a structural design pattern that allows you to add new behaviors or responsibilities to objects dynamically without altering their existing code. It is a powerful way to extend the functionality of objects by wrapping them with decorator objects. This pattern promotes the principle of “open for extension, but closed for modification,” making it easy to add new features to objects without changing their core implementation.
In JavaScript, the Decorator Pattern can be implemented using classes and object composition. Let’s explore how to implement and use the Decorator Pattern in JavaScript with a practical example.
Suppose you have a base class Coffee
:
class Coffee {
cost() {
return 5; // Base cost of a regular coffee
}
}
Now, you want to add decorators to your coffee to customize it with additional options, such as milk and sugar:
javascript
Copy code
class MilkDecorator {
constructor(coffee) {
this.coffee = coffee;
} cost() {
return this.coffee.cost() + 2; // Adding the cost of milk
}
}class SugarDecorator {
constructor(coffee) {
this.coffee = coffee;
} cost() {
return this.coffee.cost() + 1; // Adding the cost of sugar
}
}
You can then create decorated coffee instances like this:
const regularCoffee = new Coffee();
const coffeeWithMilk = new MilkDecorator(regularCoffee);
const coffeeWithMilkAndSugar = new SugarDecorator(coffeeWithMilk);
console.log(regularCoffee.cost()); // Output: 5
console.log(coffeeWithMilk.cost()); // Output: 7
console.log(coffeeWithMilkAndSugar.cost()); // Output: 8
In this example, we have the Coffee class representing a base coffee. The MilkDecorator and SugarDecorator classes are decorators that wrap a coffee object and add the cost of milk and sugar, respectively, to the base cost.
The Decorator Pattern is valuable in various scenarios, including:
While the Decorator Pattern is versatile, it’s important to keep a few considerations in mind:
The Proxy Pattern is a structural design pattern that provides a surrogate or placeholder for another object to control access to it. It acts as an intermediary or wrapper around the target object, allowing you to add additional behaviors, control access, or delay object creation. The Proxy Pattern is useful in various scenarios, such as implementing lazy loading, access control, and logging.
In JavaScript, proxies can be created using the built-in Proxy
object. Let's explore how to implement and use the Proxy Pattern in JavaScript with practical examples.
Suppose you have a resource-intensive object that you want to load lazily only when it’s needed. You can use a proxy to achieve lazy loading:
class ExpensiveResource {
constructor() {
console.log('Creating an expensive resource...');
}
fetchData() {
console.log('Fetching data...');
}
}class LazyResourceProxy {
constructor() {
this.resource = null;
} fetchData() {
if (!this.resource) {
this.resource = new ExpensiveResource();
}
this.resource.fetchData();
}
}// Usage
const lazyResource = new LazyResourceProxy();
// The actual resource is created and data is fetched only when needed
lazyResource.fetchData();
In this example, the LazyResourceProxy acts as a surrogate for the ExpensiveResource, creating the actual resource only when the fetchData method is called for the first time.
You can also use proxies to control access to objects and their properties:
const user = {
username: 'john_doe',
password: 'secret123',
};
const userProxy = new Proxy(user, {
get(target, property) {
if (property === 'password') {
throw new Error('Access denied to password.');
}
return target[property];
},
});console.log(userProxy.username); // Output: 'john_doe'
console.log(userProxy.password); // Throws an error: 'Access denied to password.'
In this example, the proxy intercepts the get operation and restricts access to the password property.
The Proxy Pattern is valuable in various scenarios, including:
When using the Proxy Pattern, keep the following considerations in mind:
The Composite Pattern is a structural design pattern that allows you to compose objects into tree-like structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions of objects uniformly. The Composite Pattern is particularly useful when you need to work with complex structures made up of smaller, related objects while maintaining a consistent interface.
In JavaScript, you can implement the Composite Pattern using classes or objects that share a common interface, enabling you to build hierarchical structures. Let’s explore how to implement and use the Composite Pattern in JavaScript with practical examples.
Suppose you’re building a graphic design application that needs to work with both simple shapes and complex compositions of shapes (e.g., groups). You can use the Composite Pattern to represent this hierarchy:
// Component interface
class Graphic {
draw() {}
}
// Leaf class (represents simple shapes)
class Circle extends Graphic {
constructor() {
super();
// Circle-specific properties and methods
} draw() {
// Draw a circle
}
}// Composite class (represents groups of shapes)
class Group extends Graphic {
constructor() {
super();
this.graphics = [];
} add(graphic) {
this.graphics.push(graphic);
} draw() {
// Draw each graphic in the group
this.graphics.forEach((graphic) => graphic.draw());
}
}// Usage
const circle1 = new Circle();
const circle2 = new Circle();
const group = new Group();group.add(circle1);
group.add(circle2);group.draw(); // Draws both circles in the group
In this example, the Graphic class serves as the component interface. The Circle class represents simple shapes, while the Group class represents compositions of shapes. Both Circle and Group classes implement the draw method, allowing you to treat them uniformly when rendering.
The Composite Pattern is valuable in various scenarios, including:
When working with the Composite Pattern, consider the following:
The Bridge Pattern is a structural design pattern that separates an object’s abstraction from its implementation. It allows you to create a bridge between the two, enabling them to vary independently. This pattern is particularly useful when you want to avoid a permanent binding between an abstraction and its implementation, making your code more flexible and maintainable.
In JavaScript, the Bridge Pattern can be implemented using classes and objects that provide an abstract interface for the abstraction and different concrete implementations for various platforms or features. Let’s explore how to implement and use the Bridge Pattern in JavaScript with practical examples.
Suppose you’re building a drawing application that can render shapes on different platforms, such as web browsers and mobile devices. You can use the Bridge Pattern to separate the drawing shapes (abstraction) from the rendering logic (implementation):
// Abstraction
class Shape {
constructor(renderer) {
this.renderer = renderer;
}
draw() {
// Delegating the drawing to the specific renderer
this.renderer.renderShape(this);
}
}// Implementor interface
class Renderer {
renderShape(shape) {}
}// Concrete Implementors
class WebRenderer extends Renderer {
renderShape(shape) {
console.log(`Drawing on the web: ${shape.constructor.name}`);
}
}class MobileRenderer extends Renderer {
renderShape(shape) {
console.log(`Drawing on mobile: ${shape.constructor.name}`);
}
}// Concrete Abstractions (Shapes)
class Circle extends Shape {
constructor(renderer) {
super(renderer);
}
}class Square extends Shape {
constructor(renderer) {
super(renderer);
}
}// Usage
const webRenderer = new WebRenderer();
const mobileRenderer = new MobileRenderer();const circle = new Circle(webRenderer);
const square = new Square(mobileRenderer);circle.draw(); // Output: Drawing on the web: Circle
square.draw(); // Output: Drawing on mobile: Square
In this example, the Shape class represents the abstraction (shapes to be drawn), and the Renderer class represents the implementor interface (platform-specific rendering logic). Different concrete implementors (WebRenderer and MobileRenderer) provide rendering logic for web and mobile platforms, respectively. The Circle and Square classes are concrete abstractions representing shapes.
The Bridge Pattern is valuable in various scenarios, including:
When using the Bridge Pattern, consider the following:
The Flyweight Pattern is a structural design pattern that aims to reduce memory consumption and improve performance by sharing common parts of objects. It achieves this by separating an object’s intrinsic state (shared and immutable) from its extrinsic state (unique and context-dependent). This pattern is particularly useful when you have a large number of similar objects and want to minimize the memory footprint.
In JavaScript, you can implement the Flyweight Pattern using classes or objects to represent shared intrinsic state and individual extrinsic state. Let’s explore how to implement and use the Flyweight Pattern in JavaScript with practical examples.
Suppose you are developing a text editor that needs to display a large amount of text. Instead of creating a separate object for each character, you can use the Flyweight Pattern to share character objects when they have the same intrinsic properties (e.g., font and size):
class Character {
constructor(char, font, size) {
this.char = char;
this.font = font;
this.size = size;
}
render() {
console.log(`Rendering character "${this.char}" in ${this.font}, size ${this.size}`);
}
}class CharacterFactory {
constructor() {
this.characters = {};
} getCharacter(char, font, size) {
const key = `${char}-${font}-${size}`;
if (!this.characters[key]) {
this.characters[key] = new Character(char, font, size);
}
return this.characters[key];
}
}// Usage
const factory = new CharacterFactory();const charA1 = factory.getCharacter('A', 'Arial', 12);
const charA2 = factory.getCharacter('A', 'Arial', 12);
const charB = factory.getCharacter('B', 'Times New Roman', 14);charA1.render(); // Output: Rendering character "A" in Arial, size 12
charA2.render(); // Output: Rendering character "A" in Arial, size 12 (shared instance)
charB.render(); // Output: Rendering character "B" in Times New Roman, size 14
In this example, the Character class represents individual characters with intrinsic properties like the character itself, font, and size. The CharacterFactory class ensures that characters with the same intrinsic properties are shared rather than duplicated.
The Flyweight Pattern is valuable in various scenarios, including:
When using the Flyweight Pattern, consider the following:
The Observer Pattern is a behavioral design pattern that establishes a one-to-many dependency between objects. It allows one object (the subject or observable) to notify multiple observers (listeners) about changes in its state or data. This pattern is commonly used for implementing distributed event handling systems, where one object’s state changes trigger actions in other dependent objects.
In JavaScript, you can implement the Observer Pattern using custom classes or built-in features like event listeners and the addEventListener
method. Let's explore how to implement and use the Observer Pattern in JavaScript with practical examples.
Suppose you’re building a weather application, and you want different parts of the UI to update when the weather conditions change. You can use a custom implementation of the Observer Pattern:
class WeatherStation {
constructor() {
this.observers = [];
}
addObserver(observer) {
this.observers.push(observer);
} removeObserver(observer) {
const index = this.observers.indexOf(observer);
if (index !== -1) {
this.observers.splice(index, 1);
}
} notifyObservers() {
this.observers.forEach((observer) => {
observer.update(this);
});
} setWeatherData(weatherData) {
this.weatherData = weatherData;
this.notifyObservers();
}
}class WeatherDisplay {
update(weatherStation) {
console.log(`Current weather: ${weatherStation.weatherData}`);
}
}// Usage
const weatherStation = new WeatherStation();
const display1 = new WeatherDisplay();
const display2 = new WeatherDisplay();weatherStation.addObserver(display1);
weatherStation.addObserver(display2);weatherStation.setWeatherData('Sunny'); // Both displays update with the new weather data
In this example, the WeatherStation acts as the subject that notifies observers (display objects) when the weather data changes. Observers subscribe to the subject using the addObserver method and implement the update method to react to changes.
JavaScript also provides a built-in way to implement the Observer Pattern using event listeners:
class NewsPublisher {
constructor() {
this.subscribers = [];
}
subscribe(subscriber) {
this.subscribers.push(subscriber);
} unsubscribe(subscriber) {
const index = this.subscribers.indexOf(subscriber);
if (index !== -1) {
this.subscribers.splice(index, 1);
}
} publishNews(news) {
this.subscribers.forEach((subscriber) => {
subscriber(news);
});
}
}// Usage
const publisher = new NewsPublisher();const subscriber1 = (news) => {
console.log(`Subscriber 1 received news: ${news}`);
};const subscriber2 = (news) => {
console.log(`Subscriber 2 received news: ${news}`);
};publisher.subscribe(subscriber1);
publisher.subscribe(subscriber2);publisher.publishNews('Breaking News: Important Announcement');
In this example, the NewsPublisher acts as the subject, and subscribers (functions) are added using the subscribe method. The publishNews method notifies subscribers by invoking their functions with the news.
The Observer Pattern is valuable in various scenarios, including:
When using the Observer Pattern, consider the following:
The Strategy Pattern is a behavioral design pattern that allows you to define a family of interchangeable algorithms, encapsulate each one, and make them interchangeable. It enables clients to choose the appropriate algorithm dynamically at runtime. This pattern promotes flexibility and reusability by separating the algorithm’s behavior from the context that uses it.
In JavaScript, you can implement the Strategy Pattern using objects or functions to represent different strategies and a context object that can switch between these strategies. Let’s explore how to implement and use the Strategy Pattern in JavaScript with practical examples.
Suppose you are developing an e-commerce application, and you want to calculate discounts for different types of customers. You can use the Strategy Pattern to encapsulate discount strategies:
// Discount Strategies
const regularCustomerDiscount = (amount) => amount * 0.1; // 10% discount
const premiumCustomerDiscount = (amount) => amount * 0.2; // 20% discount
// Context
class ShoppingCart {
constructor(discountStrategy) {
this.items = [];
this.discountStrategy = discountStrategy;
} addItem(item) {
this.items.push(item);
} calculateTotal() {
const subtotal = this.items.reduce((total, item) => total + item.price, 0);
return subtotal - this.discountStrategy(subtotal);
}
}// Usage
const regularCustomerCart = new ShoppingCart(regularCustomerDiscount);
const premiumCustomerCart = new ShoppingCart(premiumCustomerDiscount);regularCustomerCart.addItem({ name: 'Item 1', price: 50 });
premiumCustomerCart.addItem({ name: 'Item 2', price: 100 });console.log(`Regular Customer Total: $${regularCustomerCart.calculateTotal()}`); // Output: $45 (after 10% discount)
console.log(`Premium Customer Total: $${premiumCustomerCart.calculateTotal()}`); // Output: $80 (after 20% discount)
In this example, we define two discount strategies as functions (regularCustomerDiscount and premiumCustomerDiscount). The ShoppingCart class takes a discount strategy as a parameter and calculates the total price based on the chosen strategy.
The Strategy Pattern is valuable in various scenarios, including:
When using the Strategy Pattern, consider the following:
The Command Pattern is a behavioral design pattern that turns a request or simple operation into a standalone object. It allows you to parameterize objects with different requests, delay or queue a request’s execution, and support undoable operations. This pattern decouples the sender of a request from its receiver, making it easy to extend and maintain code.
In JavaScript, you can implement the Command Pattern using objects or classes to represent commands and invokers that execute those commands. Let’s explore how to implement and use the Command Pattern in JavaScript with practical examples.
Suppose you are developing a remote control application for a smart home, and you want to create a flexible way to control various devices.
You can use the Command Pattern:
// Command interface
class Command {
execute() {}
}
// Concrete Commands
class LightOnCommand extends Command {
constructor(light) {
super();
this.light = light;
} execute() {
this.light.turnOn();
}
}class LightOffCommand extends Command {
constructor(light) {
super();
this.light = light;
} execute() {
this.light.turnOff();
}
}// Receiver (Device)
class Light {
turnOn() {
console.log('Light is on.');
} turnOff() {
console.log('Light is off.');
}
}// Invoker (Remote Control)
class RemoteControl {
constructor() {
this.commands = [];
} addCommand(command) {
this.commands.push(command);
} executeCommands() {
this.commands.forEach((command) => {
command.execute();
});
}
}// Usage
const livingRoomLight = new Light();
const kitchenLight = new Light();const livingRoomLightOn = new LightOnCommand(livingRoomLight);
const livingRoomLightOff = new LightOffCommand(livingRoomLight);
const kitchenLightOn = new LightOnCommand(kitchenLight);
const kitchenLightOff = new LightOffCommand(kitchenLight);const remoteControl = new RemoteControl();remoteControl.addCommand(livingRoomLightOn);
remoteControl.addCommand(kitchenLightOff);remoteControl.executeCommands();
// Output: "Light is on." (for living room)
// Output: "Light is off." (for kitchen)
In this example, the Command Pattern is used to encapsulate the actions of turning lights on and off. The RemoteControl serves as the invoker, and concrete commands (e.g., LightOnCommand and LightOffCommand) encapsulate the actions to be executed.
The Command Pattern is valuable in various scenarios, including:
When using the Command Pattern, consider the following:
The State Pattern is a behavioral design pattern that allows an object to change its behavior when its internal state changes. It encapsulates states as separate classes and delegates the behavior to the current state object. This pattern helps manage complex state transitions and promotes the “open-closed” principle, making it easy to add new states without modifying existing code.
In JavaScript, you can implement the State Pattern using classes to represent states and a context object that delegates its behavior to the current state. Let’s explore how to implement and use the State Pattern in JavaScript with practical examples.
Suppose you are developing a vending machine that dispenses different products. The vending machine’s behavior depends on its current state, such as “Ready,” “Dispensing,” or “Sold Out.” You can use the State Pattern to model this behavior:
// State interface
class VendingMachineState {
insertMoney() {}
ejectMoney() {}
selectProduct() {}
dispenseProduct() {}
}
// Concrete States
class ReadyState extends VendingMachineState {
constructor(machine) {
super();
this.machine = machine;
} insertMoney() {
console.log('Money inserted.');
this.machine.setState(this.machine.getDispensingState());
} selectProduct() {
console.log('Please insert money first.');
}
}class DispensingState extends VendingMachineState {
constructor(machine) {
super();
this.machine = machine;
} dispenseProduct() {
console.log('Product dispensed.');
this.machine.setState(this.machine.getReadyState());
}
}class VendingMachine {
constructor() {
this.readyState = new ReadyState(this);
this.dispensingState = new DispensingState(this);
this.currentState = this.readyState;
} setState(state) {
this.currentState = state;
} getReadyState() {
return this.readyState;
} getDispensingState() {
return this.dispensingState;
} insertMoney() {
this.currentState.insertMoney();
} selectProduct() {
this.currentState.selectProduct();
} dispenseProduct() {
this.currentState.dispenseProduct();
}
}// Usage
const vendingMachine = new VendingMachine();vendingMachine.selectProduct(); // Output: "Please insert money first."
vendingMachine.insertMoney(); // Output: "Money inserted."
vendingMachine.dispenseProduct(); // Output: "Product dispensed."
In this example, the State Pattern is used to manage the behavior of a vending machine. States like “Ready” and “Dispensing” are represented as separate classes, and the context (vending machine) delegates its behavior to the current state.
The State Pattern is valuable in various scenarios, including:
When using the State Pattern, consider the following:
The Chain of Responsibility Pattern is a behavioral design pattern that helps you build a chain of objects to handle a request. Each object in the chain has the opportunity to process the request or pass it to the next object in the chain. It decouples the sender of a request from its receivers and allows multiple handlers to be in the chain. This pattern promotes flexibility and extensibility by enabling you to add or modify handlers without affecting the client code.
In JavaScript, you can implement the Chain of Responsibility Pattern using objects or classes that represent handlers and a client that initiates requests. Each handler has a reference to the next handler in the chain. Let’s explore how to implement and use the Chain of Responsibility Pattern in JavaScript with practical examples.
Suppose you are developing an order processing system, and you want to handle orders based on their total amount. You can use the Chain of Responsibility Pattern to create a chain of handlers, each responsible for processing orders within a certain price range:
// Handler interface
class OrderHandler {
constructor() {
this.nextHandler = null;
}
setNextHandler(handler) {
this.nextHandler = handler;
} handleOrder(order) {
if (this.canHandleOrder(order)) {
this.processOrder(order);
} else if (this.nextHandler) {
this.nextHandler.handleOrder(order);
} else {
console.log('No handler can process this order.');
}
} canHandleOrder(order) {}
processOrder(order) {}
}// Concrete Handlers
class SmallOrderHandler extends OrderHandler {
canHandleOrder(order) {
return order.amount <= 100;
} processOrder(order) {
console.log(`Processing small order for ${order.amount}`);
}
}class MediumOrderHandler extends OrderHandler {
canHandleOrder(order) {
return order.amount <= 500;
} processOrder(order) {
console.log(`Processing medium order for ${order.amount}`);
}
}class LargeOrderHandler extends OrderHandler {
canHandleOrder(order) {
return order.amount > 500;
} processOrder(order) {
console.log(`Processing large order for ${order.amount}`);
}
}// Client
class Order {
constructor(amount) {
this.amount = amount;
}
}// Usage
const smallOrderHandler = new SmallOrderHandler();
const mediumOrderHandler = new MediumOrderHandler();
const largeOrderHandler = new LargeOrderHandler();smallOrderHandler.setNextHandler(mediumOrderHandler);
mediumOrderHandler.setNextHandler(largeOrderHandler);const order1 = new Order(80);
const order2 = new Order(250);
const order3 = new Order(600);smallOrderHandler.handleOrder(order1); // Output: "Processing small order for 80"
smallOrderHandler.handleOrder(order2); // Output: "Processing medium order for 250"
smallOrderHandler.handleOrder(order3); // Output: "Processing large order for 600"
In this example, the Chain of Responsibility Pattern is used to handle orders of different amounts. Handlers like SmallOrderHandler, MediumOrderHandler, and LargeOrderHandler each determine if they can process an order based on the order’s amount. If they can, they process it; otherwise, they pass the order to the next handler in the chain.
The Chain of Responsibility Pattern is valuable in various scenarios, including:
When using the Chain of Responsibility Pattern, consider the following:
The Visitor Pattern is a behavioral design pattern that allows you to separate an algorithm from the object structure it operates on. It provides a way to add new operations to objects without modifying their classes, making it easy to extend functionality for complex object hierarchies. This pattern is especially useful when you have a set of distinct elements and want to perform various operations on them without modifying their code.
In JavaScript, you can implement the Visitor Pattern using functions or classes to represent visitors that visit elements within an object structure. Let’s explore how to implement and use the Visitor Pattern in JavaScript with practical examples.
Suppose you are developing a content management system where you have different types of content elements like articles, images, and videos. You want to perform various operations, such as rendering and exporting, on these elements without modifying their classes. You can use the Visitor Pattern:
// Element interface
class ContentElement {
accept(visitor) {}
}
// Concrete Elements
class Article extends ContentElement {
accept(visitor) {
visitor.visitArticle(this);
}
}class Image extends ContentElement {
accept(visitor) {
visitor.visitImage(this);
}
}class Video extends ContentElement {
accept(visitor) {
visitor.visitVideo(this);
}
}// Visitor interface
class Visitor {
visitArticle(article) {}
visitImage(image) {}
visitVideo(video) {}
}// Concrete Visitors
class RendererVisitor extends Visitor {
visitArticle(article) {
console.log(`Rendering article: ${article.title}`);
} visitImage(image) {
console.log(`Rendering image: ${image.caption}`);
} visitVideo(video) {
console.log(`Rendering video: ${video.title}`);
}
}class ExportVisitor extends Visitor {
visitArticle(article) {
console.log(`Exporting article: ${article.title}`);
} visitImage(image) {
console.log(`Exporting image: ${image.caption}`);
} visitVideo(video) {
console.log(`Exporting video: ${video.title}`);
}
}// Usage
const elements = [new Article('Article 1'), new Image('Image 1'), new Video('Video 1')];
const renderer = new RendererVisitor();
const exporter = new ExportVisitor();elements.forEach((element) => {
element.accept(renderer);
element.accept(exporter);
});
In this example, we have content elements like Article, Image, and Video, and we want to perform rendering and exporting operations on them without modifying their classes. We achieve this by implementing visitor classes like RendererVisitor and ExportVisitor that visit the elements and perform the desired operations.
The Visitor Pattern is valuable in various scenarios, including:
When using the Visitor Pattern, consider the following:
In this comprehensive exploration of design patterns in JavaScript, we’ve delved into various patterns that empower developers to create flexible, maintainable, and efficient code. Each design pattern addresses specific problems and provides elegant solutions to common software design challenges.
We began by understanding the fundamental concept of design patterns and categorized them into three major groups: creational, structural, and behavioral patterns. Within each category, we examined popular design patterns and showcased their practical implementations in JavaScript.
Here’s a brief recap of the key design patterns we covered:
Creational Patterns: These patterns focus on object creation mechanisms, including the Singleton Pattern for ensuring a single instance of a class, Factory and Abstract Factory Patterns for creating objects with flexible factories, Builder Pattern for constructing complex objects step by step, Prototype Pattern for cloning objects, and Object Pool Pattern for efficient object reuse.
Structural Patterns: These patterns deal with object composition, providing ways to build complex structures from simpler components. We explored the Adapter Pattern for adapting interfaces, the Decorator Pattern for adding behavior to objects dynamically, the Proxy Pattern for controlling access to objects, the Composite Pattern for composing objects into tree structures, the Bridge Pattern for separating abstraction from implementation, and the Flyweight Pattern for minimizing memory usage by sharing common state.
Behavioral Patterns: These patterns are concerned with the interaction and communication between objects. We covered the Observer Pattern for implementing distributed event handling systems, the Strategy Pattern for encapsulating interchangeable algorithms, the Command Pattern for turning requests into standalone objects, the State Pattern for managing object behavior based on internal state, the Chain of Responsibility Pattern for building a chain of handlers for processing requests, and the Visitor Pattern for separating algorithms from object structures.
Design patterns are valuable tools in a developer’s toolkit, enabling the creation of scalable and maintainable codebases. Understanding and applying these patterns in your JavaScript projects allows you to write more efficient, adaptable, and robust software.
Remember that design patterns are not one-size-fits-all solutions, and their applicability depends on the specific requirements and challenges of your project. Carefully consider when and how to apply them to achieve the best results.
As you continue to grow as a JavaScript developer, mastering these design patterns will empower you to tackle complex software design challenges with confidence and creativity. Whether you’re building web applications, game engines, or any other software, design patterns will be your allies in crafting elegant and maintainable code. Happy coding!
Also published here.