The Decorator Design Pattern is a structural design pattern that allows you to dynamically add behavior or responsibilities to an object without altering its code. The key idea is to create a "wrapper" around an object that adds additional functionality while keeping the original object intact. This is typically done by implementing the same interface or base class and wrapping the original object.
Key Concepts:
- Component:
The interface or base class that defines the basic functionality.
- ConcreteComponent:
A class that implements the basic functionality (the object being
decorated).
- Decorator:
A base class or interface that wraps the Component and adds additional
behavior.
- ConcreteDecorator:
A class that extends the Decorator and implements the additional
functionality.
Example of Decorator Pattern in JavaScript
Let's say we have a basic Coffee class and we want to add
different kinds of "decorations" to it, such as milk, sugar, and
chocolate. Each decorator adds something to the coffee's description or cost.
Without Decorators (Inflexible Approach):
class Coffee {
cost() {
return 5;
}
description() {
return 'Plain
coffee';
}
}
class MilkCoffee {
constructor(coffee)
{
this.coffee
= coffee;
}
cost() {
return this.coffee.cost()
+ 1; // Adds cost of milk
}
description() {
return this.coffee.description()
+ ', with milk';
}
}
class SugarCoffee {
constructor(coffee)
{
this.coffee
= coffee;
}
cost() {
return this.coffee.cost()
+ 0.5; // Adds cost of sugar
}
description() {
return this.coffee.description()
+ ', with sugar';
}
}
let myCoffee = new Coffee();
console.log(myCoffee.description()); // Plain coffee
console.log(myCoffee.cost()); // 5
myCoffee = new MilkCoffee(myCoffee); // Decorate with
Milk
console.log(myCoffee.description()); // Plain coffee,
with milk
console.log(myCoffee.cost()); // 6
myCoffee = new SugarCoffee(myCoffee); // Decorate with
Sugar
console.log(myCoffee.description()); // Plain coffee,
with milk, with sugar
console.log(myCoffee.cost()); // 6.5
While this approach works, it creates a lot of subclassing (MilkCoffee,
SugarCoffee, etc.) and becomes increasingly difficult to extend with additional
features like adding chocolate or adjusting the size of the coffee.
With the Decorator Pattern (Flexible Approach):
// Base Component
class Coffee {
cost() {
return 5;
}
description() {
return 'Plain
coffee';
}
}
// Decorator Base Class
class CoffeeDecorator {
constructor(coffee)
{
this.coffee
= coffee;
}
cost() {
return this.coffee.cost();
}
description() {
return this.coffee.description();
}
}
// Concrete Decorators
class MilkDecorator extends CoffeeDecorator {
cost() {
return this.coffee.cost()
+ 1; // Adds milk cost
}
description() {
return this.coffee.description()
+ ', with milk';
}
}
class SugarDecorator extends CoffeeDecorator {
cost() {
return this.coffee.cost()
+ 0.5; // Adds sugar cost
}
description() {
return this.coffee.description()
+ ', with sugar';
}
}
class ChocolateDecorator extends CoffeeDecorator {
cost() {
return this.coffee.cost()
+ 1.5; // Adds chocolate cost
}
description() {
return this.coffee.description()
+ ', with chocolate';
}
}
// Usage
let myCoffee = new Coffee();
console.log(myCoffee.description()); // Plain coffee
console.log(myCoffee.cost()); // 5
myCoffee = new MilkDecorator(myCoffee); // Decorate with
Milk
console.log(myCoffee.description()); // Plain coffee,
with milk
console.log(myCoffee.cost()); // 6
myCoffee = new SugarDecorator(myCoffee); // Decorate with
Sugar
console.log(myCoffee.description()); // Plain coffee,
with milk, with sugar
console.log(myCoffee.cost()); // 6.5
myCoffee = new ChocolateDecorator(myCoffee); // Decorate
with Chocolate
console.log(myCoffee.description()); // Plain coffee,
with milk, with sugar, with chocolate
console.log(myCoffee.cost()); // 8
Explanation:
- Coffee:
The base Component that defines the basic functionality of a coffee (cost
and description).
- CoffeeDecorator:
The abstract Decorator that holds a reference to a Coffee object and
provides the same interface (cost() and description()). Concrete
decorators will extend this class and add specific functionality.
- MilkDecorator,
SugarDecorator, ChocolateDecorator: These are the ConcreteDecorators
that add specific features to the coffee, such as milk, sugar, or
chocolate. Each decorator modifies the behavior of the cost() and description()
methods.
Advantages of the Decorator Pattern:
- Flexibility:
You can dynamically add new behavior to an object at runtime without
altering its structure. For example, you can mix and match decorators to
create different variations of an object.
- Single
Responsibility Principle: The functionality is added through
decorators, which keep the codebase cleaner. Each decorator has a single
responsibility: adding one piece of functionality to the object.
- Open-Closed
Principle: You can extend the functionality of the object by creating
new decorators rather than modifying the object itself, adhering to the
open-closed principle of object-oriented design.
- Composition
over inheritance: The decorator pattern avoids deep inheritance
hierarchies by allowing new functionality to be added through composition
rather than subclassing.
When to Use the Decorator Pattern:
- When
you need to add responsibilities or features to objects dynamically
without affecting other objects of the same class.
- When
subclassing would result in an explosion of new classes (for example,
multiple combinations of features).
- When
you want to keep the core logic of a class clean and add additional
behavior step by step.
- When
you want to extend functionality in a flexible and reusable way without
modifying existing code.
No comments:
Post a Comment