ref –
- https://dart.academy/structural-design-patterns-for-dart-and-flutter-decorator/
- https://medium.com/flutter-community/flutter-design-patterns-16-decorator-bf0dd711f093
- Pizza Example
Decorator pattern provides a flexible way to add attributes or behavior to an object at runtime
…as an alternative to creating new classes to cover every combination of traits an object may need.
Here we create a square first. It has its basic methods and attributes for use.
1 2 |
final square = Square(); print(square.draw()); |
But what if we’re not satisfied with its basic functionalities and properties? Say we don’t just want to draw it. We also want to give it a color.
So we then create something like GreenShapeDecorator(square) to let it know that we want to color our square green:
1 2 |
final greenSquare = GreenShapeDecorator(square); print(greenSquare.draw()); |
..or blue
1 2 |
final blueGreenSquare = BlueShapeDecorator(greenSquare); print(blueGreenSquare.draw()); |
Extends an object’s functionality by wrapping it in an object of a Decorator class, leaving the original object intact without modification.
How to do it
Let’s create an abstract class called Shape. (It cannot be instantiated because we want real shapes to implement according to its interface)
It says you must implement draw function.
1 2 3 |
abstract class Shape { String draw(); } |
Hence, when we declare a Shape, for example Square or Triangle, it implements Shape,
and we
1 2 3 4 5 6 7 |
class Square implements Shape { String draw() => "Square"; } class Triangle implements Shape { String draw() => "Triangle"; } |
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.
In order to add attributes to our Shape, we first create an parent abstract class called ShapeDecorator
1) use constructor to reference a shape, and pass it in using constructor
2) Satisfy implements by declaring it again.
3) Use ‘implements’ to say we’re simply wrapping abstract Shape.
1 2 3 4 5 6 7 |
abstract class ShapeDecorator implements Shape { final Shape shape; ShapeDecorator(this.shape); String draw(); } |
We basically wrapped abstract Shape by passing in the Shape, and keeping the interface draw.
Now, we create concrete decorator class by extending our ShapeDecorator.
Let’s say we want to add color green. We over-ride the draw function and implement it, thus satisfying the interface ShapeDecorator. Inside its implementation, we use green to color the Shape.
1 2 3 4 5 6 |
class GreenShapeDecorator extends ShapeDecorator { GreenShapeDecorator(Shape shape) : super(shape); @override String draw() => "Green ${shape.draw()}"; } |
1) We use an abstract class to define the interface all shape decorators should adhere to.
2) 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.
Pizza Example
We first declare an interface. It says, all pizzas that implements this must have a description property, a function to get the description, and a function to get the price.
1 2 3 4 5 |
abstract class Pizza { late String description; String getDescription(); double getPrice(); } |
We then have to create a concrete base where it implements our Pizza interface. In order to conform to the interface, we use declaration for properties and override for functions. Being a concrete implementation, don’t forget the constructor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class PizzaBase extends Pizza { PizzaBase(String description) { this.description = description; } @override String getDescription() { return description; } @override double getPrice() { return 3.0; } } |
So, as long as a class implements an interface that is Pizza (or extends from Pizza), we can have a reference of type Pizza and point to the instantiation of that class. This is shown in the picture above.
Now that our base is complete, we are to do the same for an interface for decorating our Pizza base.
1 2 3 |
abstract class PizzaDecorator extends Pizza { } |
Because PizzaDecorator needs to be abstract and needs to extend from abstract Pizza, we simply use extends
It just means we extend the abstract properties from Pizza.
We have a property where its of type Pizza. This means any property that implements Pizza is valid here.
This is because we are going to use it in our overriden getDescription and getPrice functions.
Also, a constructor that injects an instance of pizza is required.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
abstract class PizzaDecorator extends Pizza { final Pizza pizza; PizzaDecorator(this.pizza); @override String getDescription() { return pizza.getDescription(); } @override String getPrice() { return pizza.getPrice(); } } |
As you can see, we have Pizza pizzaInt, which can reference either our PizzaBase object or a Sauce object. This is because at the top of the abstract class, we have Pizza.
Also notice that in abstract PizzaDecorator, we have a Pizza pizza reference that points to PizzaBase.
On the right side, we instantiate a PizzaBase object. We have a reference pizzaInst point to it.
1 |
Pizza pizzaInst = PizzaBase('Pizza Margherita'); |
We then pass the reference inside of Sauce.
1 |
pizzaInst = Sauce(pizzaInst); |
Sauce is depicted on the left side. Its abstract class Decorator has a property Pizza pizza, which points to abstract Pizza. This means, it can point to PizzaBase, Sauce, or any condiments (which must extend from abstract Pizza).
Notice getDescription.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import '../pizza.dart'; import '../pizza_decorator.dart'; class Sauce extends PizzaDecorator { Sauce(Pizza pizza) : super(pizza) { description = 'Sauce'; } @override String getDescription() { var sauceDescr = pizza.getDescription(); return '$sauceDescr \n- $description'; } } |
To get the whole pizza’s description of the name, and toppings, it first looks at reference pizza, which is pointing to the PizzaBase. It calls its getDescription, which gets “Pizza Margherita”.
1 |
print(pizzaInst.getDescription()); |
Then we return the interpolated string with our own description.
Thus
“Pizza Margherita” <-- PizzaBase's getDescription "- Sauce" <-- Sauce's getDescription
If were to keep going with toppings like Mozzarella
1 |
pizzaInst = Mozzarella(pizzaInst); |
it would be the same concept. Mozzarella’s Pizza pizza reference would point to Sauce.
When we call getDescription, it would call
Mozzarella’s
1 2 3 4 5 |
@override String getDescription() { var sauceDescr = pizza.getDescription(); --> calls Sauce return '$sauceDescr \n- $description'; } |
Sauce’s
1 2 3 4 5 |
@override String getDescription() { var sauceDescr = pizza.getDescription(); --> calls PizzaBase return '$sauceDescr \n- $description'; } |
PizzaBase
1 2 3 4 |
@override String getDescription() { return description; } |
Thus, in the end the output would be:
1 2 3 4 5 6 7 8 9 10 11 12 |
PizzaBase's getDescription: "Pizza Margherita" <-- from PizzaBase's description property Sauce's getDescription: "Pizza Margherita" "- Sauce" <-- from Sauce's description property Mozzarella's getDescription: "Pizza Margherita" "- Sauce" "- Mozzarella" <-- from Mozzarella's description property |