Skip to main content

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 and Coffee <|.. CoffeeDecorator : implements represent that both SimpleCoffee and CoffeeDecorator classes implement the Coffee interface.
  • CoffeeDecorator <|-- MilkCoffee : extends and CoffeeDecorator <|-- SugarCoffee : extends represent that MilkCoffee and SugarCoffee classes extend the CoffeeDecorator class.
  • CoffeeDecorator o-- Coffee : - has a indicates that the CoffeeDecorator has a composition relationship with Coffee.
  • class Coffee, class SimpleCoffee, class CoffeeDecorator, class MilkCoffee, and class SugarCoffee represent classes or interfaces, and their methods are listed within the curly braces {}.
  • <<interface>> denotes that Coffee 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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

TypeScript Course Instructor Image
TypeScript Course Instructor Image

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 DETAILS

What 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.

YouTube @cloudaffle