Introduction To The Decorator Pattern
The Decorator pattern is a structural design pattern that allows you to attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors.
Here's what each line represents:
Coffee <|.. SimpleCoffee : implements
andCoffee <|.. CoffeeDecorator : implements
represent that bothSimpleCoffee
andCoffeeDecorator
classes implement theCoffee
interface.CoffeeDecorator <|-- MilkCoffee : extends
andCoffeeDecorator <|-- SugarCoffee : extends
represent thatMilkCoffee
andSugarCoffee
classes extend theCoffeeDecorator
class.CoffeeDecorator o-- Coffee : - has a
indicates that theCoffeeDecorator
has a composition relationship withCoffee
.class Coffee
,class SimpleCoffee
,class CoffeeDecorator
,class MilkCoffee
, andclass SugarCoffee
represent classes or interfaces, and their methods are listed within the curly braces{}
.<<interface>>
denotes thatCoffee
is an interface.+
and-
before the method names represent public and private access specifiers, respectively.
The main idea behind this pattern is that it allows you to add new functionality to an object without altering its implementation. The new functionality is added by wrapping the original object inside a decorator object which has the same interface as the original object and adds its own behavior either before or after delegating to the original object.
Classic Implementation
We'll create a simple Coffee
interface and a basic implementation. Then we'll
add different kinds of decorators to add extra features like adding milk or
sugar.
// Step 1: Define the interface for the base object and concrete implementations
interface Coffee {
getCost(): number;
getDescription(): string;
}
class SimpleCoffee implements Coffee {
getCost() {
return 10;
}
getDescription() {
return "Simple coffee";
}
}
// Step 2: Define the base decorator
abstract class CoffeeDecorator implements Coffee {
protected coffee: Coffee;
constructor(coffee: Coffee) {
this.coffee = coffee;
}
getCost() {
return this.coffee.getCost();
}
getDescription() {
return this.coffee.getDescription();
}
}
// Step 3: Create specific decorators
class MilkCoffee extends CoffeeDecorator {
getCost() {
return super.getCost() + 2;
}
getDescription() {
return super.getDescription() + ", with milk";
}
}
class SugarCoffee extends CoffeeDecorator {
getCost() {
return super.getCost() + 1;
}
getDescription() {
return super.getDescription() + ", with sugar";
}
}
// Using decorators
let coffee: Coffee = new SimpleCoffee();
console.log(coffee.getCost());
// 10
console.log(coffee.getDescription());
// Simple coffee
coffee = new MilkCoffee(coffee);
console.log(coffee.getCost());
// 12
console.log(coffee.getDescription());
// Simple coffee, with milk
coffee = new SugarCoffee(coffee);
console.log(coffee.getCost());
// 13
console.log(coffee.getDescription());
// Simple coffee, with milk, with sugar
Here, SimpleCoffee
, MilkCoffee
, and SugarCoffee
all implement the Coffee
interface. MilkCoffee
and SugarCoffee
are decorators that add new
functionality to an existing Coffee
object. They achieve this by wrapping
a Coffee
object and adding their behavior to the methods of Coffee
.
When To Use The Decorator Pattern
The Decorator pattern is useful in various scenarios, typically related to extending behavior or adding responsibilities. Here are several situations or code smells that might indicate the need for a Decorator pattern
Large number of subclasses for minor variations: If you have a base class and you are creating numerous subclasses to alter or extend its behavior slightly, it could become cumbersome and hard to manage. A decorator can provide a more flexible way to add behavior.
Frequent changes in functionality: If you find that the behavior of classes need to change frequently, the decorator pattern can provide a better solution than continually altering the classes themselves.
Complicated code due to multiple features: If your code becomes increasingly complicated due to the need to add new features and it's difficult to understand the current functionality because of the multiple features, the decorator pattern could help. With decorator, you can break down the features into individual classes, making the code easier to read and maintain.
Mixing business logic with optional functionalities: If the core business logic is getting mixed up with optional functionalities which might change in future, the decorator pattern can be useful. By wrapping the optional functionalities as decorators around the main business logic, we can achieve clean separation and keep the core logic untouched.
Remember that using the Decorator pattern (or any design pattern) should be carefully considered, as unnecessary use can add complexity. The key is to recognize the appropriate situations that call for each design pattern and apply them judiciously.
Time To Transition From JavaScript To TypeScript
Level Up Your TypeScript And Object Oriented Programming Skills. The only complete TypeScript course on the marketplace you building TypeScript apps like a PRO.
SEE COURSE DETAILSWhat Can You Do Next 🙏😊
If you liked the article, consider subscribing to Cloudaffle, my YouTube Channel, where I keep posting in-depth tutorials and all edutainment stuff for software developers.