Decorator Design Pattern In Dart/Flutter

Introduction

The Decorator design pattern is one of the twenty-three well-known GoF design patterns describing how to solve recurring design problems to design flexible and reusable object-oriented software. If you would like to dive deep into the design pattern series, check my previous articles. In this article, we'll cover the decorator design pattern, which is a structural design pattern often referred to as a wrapper. We are going to cover the following topics,

  1. What is the Decorator's Design Pattern?
  2. An Example of a Decorator Design Pattern?
  3. When to use?
  4. Pitfalls
  5. Conclusion

Let's get started without any further delay.

What is the Decorator's design pattern?

According to the book "Design Patterns, Elements of Reusable Object-Oriented Software", Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

Let’s try to understand this with a real-life example that can make more sense; imagine you're at a coffee shop and order a plain coffee. Here, coffee acts as a class object. Now, you want to enhance your coffee. You ask for some milk to be added. The barista pours some milk into your coffee. Here, the milk acts as a decorator, enhancing your coffee without changing it.

Later, you decide you want some sugar, too. The barista adds sugar to your coffee. Sugar is another decorator. It enhances your coffee further without changing the coffee or the milk.

In this scenario, your coffee is the object, and milk and sugar are the decorators. So, in simple words, the Decorator design pattern adds new functionality to objects without altering their structure.

An example of a Decorator design pattern in a dart

// Abstract Coffee class
abstract class Coffee {
  String get description;
  double get cost;
}

// PlainCoffee class
class PlainCoffee implements Coffee {
  @override
  String get description => 'Plain Coffee';

  @override
  double get cost => 10.0;
}

// CoffeeDecorator class
class CoffeeDecorator implements Coffee {
  final Coffee coffee;
  CoffeeDecorator(this.coffee);

  @override
  String get description => coffee.description;

  @override
  double get cost => coffee.cost;
}

// MilkDecorator class
class ExtraMilkDecorator extends CoffeeDecorator {
  ExtraMilkDecorator(Coffee coffee) : super(coffee);

  @override
  String get description => coffee.description + ' + Extra Milk';

  @override
  double get cost => coffee.cost + 2.0;
}

// SugarDecorator class
class ExtraSugarDecorator extends CoffeeDecorator {
  ExtraSugarDecorator(Coffee coffee) : super(coffee);

  @override
  String get description => coffee.description + ' + Extra Sugar';

  @override
  double get cost => coffee.cost + 1.0;
}

void main() {
  // Order a latte coffee
  Coffee coffee = PlainCoffee();
  print("${coffee.description}: \$${coffee.cost}");

  // Add milk
  coffee = ExtraMilkDecorator(coffee);
  print("${coffee.description}: \$${coffee.cost}");

  // Add sugar
  coffee = ExtraSugarDecorator(coffee);
  print("${coffee.description}: \$${coffee.cost}");
}

Plain Coffee is a basic coffee. ExtraMilkDecorator and ExtraSugarDecorator are like asking the barista to add extra milk or sugar. They can change the coffee's description and cost without altering the original PlainCoffee.

When to use?

  • When you want to dynamically and transparently add new responsibilities to an object.?
  • It's not practical to extend functionality by subclassing because there are too many independent extensions.
  • When you want to add responsibilities to objects in stages or layers.?
  • When you want to keep a class focused on a single responsibility but also need to add optional features.?

Pitfalls

  • Overusing the Decorator pattern can lead to a complex codebase, which may be challenging to maintain.
  • Creating numerous small classes for each new feature creates a more extensive class hierarchy.
  • It requires many small classes, each very similar to the others, which can overly complicate the design.

Conclusion

The Decorator design pattern is one of the popular design patterns used to dynamically add new functionality to an object without changing its structure. It offers a flexible alternative to subclassing, particularly when dealing with many independent extensions. However, using this pattern judiciously is important, as overuse can lead to a complex and hard-to-maintain codebase. Despite these challenges, when used correctly, the Decorator pattern can significantly enhance the modularity and readability of your code. If you find my articles useful, connect with me on LinkedIn, give your feedback, and keep motivating me to create more content like this.


Similar Articles