Using inheritance, we can statically extend the functionality of a Dart class. But what if we want more flexibility than that? We may want to add behavior to an object dynamically, perhaps as a user makes attribute selections. The Decorator pattern can help! With this pattern, we can attach new behavior or attributes to an object by wrapping it in a special decorator that shares the object's interface. The Decorator pattern is a way to extend objects using composition as opposed to inheritance, which is the approach that works most naturally in a Flutter application.

About structural design patterns: Structural patterns help us shape the relationships between the objects and classes we create. These patterns are focused on how classes inherit from each other, how objects can be composed of other objects, and how objects and classes interrelate. In this series of articles, you'll learn how to build large, comprehensive systems from simpler, individual modules and components. The patterns assist us in creating flexible, loosely coupled, interconnecting code modules to complete complex tasks in a manageable way.
The code for this article was tested with Dart 2.8.4 and Flutter 1.17.5.

Let's look at a simple example of the Decorator pattern in action.

A Decorator example

To demonstrate the pattern, we'll use shape models, the classic workhorse of design pattern examples. First, we need to define an interface for shapes, and we'll throw in a few sample shape classes:

abstract class Shape {
  String draw();
}

class Square implements Shape {
  String draw() => "Square";
}

class Triangle implements Shape {
  String draw() => "Triangle";
}

Remember, in Dart there are no explicit interfaces, but every class exports its interface for implementation. We define the Shape interface using an abstract class, so that it can't be directly instantiated. In this simplified example, the draw() method will return a string appropriate to the shape's type. Because Square and Triangle implement Shape, they are required to provide an implementation of the draw() method. This means client code can create variables of type Shape that can be assigned any of the shape classes.

With the shape models in place, we can define a decorator interface and a couple of sample decorators:

abstract class ShapeDecorator implements Shape {
  final Shape shape;

  ShapeDecorator(this.shape);

  String draw();
}

class GreenShapeDecorator extends ShapeDecorator {
  GreenShapeDecorator(Shape shape) : super(shape);

  @override
  String draw() => "Green ${shape.draw()}";
}

class BlueShapeDecorator extends ShapeDecorator {
  BlueShapeDecorator(Shape shape) : super(shape);

  @override
  String draw() => "Blue ${shape.draw()}";
}

Once again, we use an abstract class to define the interface all shape decorators should adhere to. Additionally, ShapeDecorator implements Shape, so all classes that extend ShapeDecorator are interface compatible with shape classes. This is key, because it means we can instantiate a decorator, pass it a shape, and then use the decorator as though it were the shape. That's the essence of the Decorator pattern.

Each decorator class inherits everything from ShapeDecorator and overrides the unimplemented draw() method. It's optional but recommended to include the @override metatag when overriding inherited methods. Note that the shape classes do not override draw(); they must implement draw(), but they are not overriding an inherited method when they do since they don't inherit anything. Decorator constructors take a reference to the shape they'll be decorating, and they pass it along to the abstract superclass's constructor to be stored in the shape property. The overridden draw() methods in the decorator classes prepend their respective decoration onto the decorated shape's string representation.

Here's an example of using the shape decoration system:

final square = Square();
print(square.draw());

final greenSquare = GreenShapeDecorator(square);
print(greenSquare.draw());

final blueGreenSquare = BlueShapeDecorator(greenSquare);
print(blueGreenSquare.draw());

First, we create a square and print the output from its draw() method, which will be the shape's name alone. To create a green square, we construct an instance of GreenShapeDecorator and pass it the square. When we draw the green square, we see that the square has been decorated. One of the strengths of the Decorator pattern is the ability to apply as many decorators as we wish to any object, so we can add some blue to the green square, resulting in a square with both color decorations.

Conclusion

You can see that this pattern provides a flexible way to add attributes or behavior to an object at runtime, piecemeal, as an alternative to creating new classes to cover every combination of traits an object may need.

To read more about structural design patterns in Dart, check out these related articles: