Though the decorator design is not considered one of the most important design patterns to master as a programmer, I definitely had a lot of fun learning it because of the peculiar effect it creates when implemented. This article will slightly differ from other articles in this series like Factory Method, Builder, Adapter Design Pattern where I attempt to strictly define the more comprehensive implementation of these seemingly straightforward design patterns. Rather I will simply be sharing my learnings from studying the Decorator Pattern on Refactoring Guru using an example in C++.
Decorator design is a creational pattern that allows for the enhancement of interfaces. The enhanced object forms a stack like structure with layers and layers of additional attributes added on following the Last in First Out (LIFO) principles. When using the decorated object, each layer and each added attribute is recursively expanded, forming a Russian nested doll like behavior. I truly think it is fascinating about the concept of enhancing and extending an object by wrapping a decorator over the top, but at the core the wrapped object’s fundamental characteristics has not changed.
Suppose we’re building a sandwich shop where the user has the ability to choose the base of the sandwich, then add sides and condiments onto it. The base of the sandwich has a basic version, which uses regular bread and meat, where as the deluxe version uses organic bread and meat, along with cheese and lettuce in it. After the customer choose the base, and selects the additional sides and condiments, the sandwich shop will return the total cost and the various ingredients that went into sandwich.
A starting point to design this class would be to have a Sandwich base class, and extend it into deluxe and basic Sandwich child classes. We obviously wouldn’t want to continue to derive more subclasses for the various sides and condiments otherwise the number of subclasses will just continue to increase. This is when the principle, favor composition over inheritance, comes in to play.
Instead, a better approach would be to extend the Basic or the Deluxe Sandwich classes by adding attributes to it. For example, Sandwich class has a container field called Sides and another called Condiments. This works well, but adding or removal components of the class requires working with the container making it less flexible and extendable, and arguably less elegant.
The core idea of the decorator is that we can use this class to enhance the Sandwich classes. We can have a CondimentDecorator and a SideDecorator that will be used to wrap sandwich classes to enhance and extend its functionality. The following graph compares the two approach.
Before getting into the specific implementation, let’s make sure we understand the definitions and UML of the Decorator pattern. According to Refactoring Guru:
Decorator is a structural design pattern that lets you attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors.
The idea is that the new behavior is decorated by wrapping old objects into it. Additionally, for the client code, invoking a decorated object or a original object should make no difference.
The component interface is what the client interacts with, and the concrete components, implements the interface. In our sandwich shop example, the sandwich is the component, and the concrete components are the basic and the deluxe sandwiches. These are the building blocks that we will start with.
The Base Decorator also implements the Component Interface. Which may be odd to visualize because why would a CondimentDecorator implement the Sandwich. This will all make sense in a second. Finally, the ConcreteDecorators are the specific decorators we want to use to decorate the Concrete Component such as CondimentDecorator or SideDecorator.
You will notice that the BaseDecorator not only implements the Component Interface, but also has a instance of Component (Aggregation). The instance of the Component is what the Decorator wraps inside.
We will implement the Component Interface and the Concrete Components first in our sandwich shop:
class SandwichOrder{
public:
virtual int GetCost() = 0;
virtual std::string GetIngredient() = 0;
};
class BasicSandwich : public SandwichOrder{
public:
int cost = 5;
int GetCost() {
return cost;
}
std::string GetIngredient() {
return "Basic Bread and Meat";
}
};
class DeluxeSandwich : public SandwichOrder{
public:
int cost = 8;
int GetCost() {
return cost;
}
std::string GetIngredient() {
return "Oraganic Bread, Organic Meat, Cheese, Veggie";
}
};
The Basic Sandwich starts at $5 and contains only basic bread and meat, while the Deluxe Sandwich starts at $8 and contains orgranic bread and meat, cheese, and veggie. The customer can continue to add sides and condiments and the client code will ultimately return the cost and the ingredients of the sandwich to the customer.
Currently, the client (simply a function in this example) can only produce two types of sandwiches:
void ServeOrder(SandwichOrder* order) {
std::cout << "Total Cost: " << order->GetCost() << std::endl;
std::cout << "Ingredients: " << order->GetIngredient() << std::endl;
}
int main () {
SandwichOrder* sandwich1 = new BasicSandwich;
ServeOrder(sandwich1);
SandwichOrder* sandwich2 = new DeluxeSandwich;
ServeOrder(sandwich2);
return 0;
}
// Output
// Total Cost: 5
// Ingredients: Basic Bread and Meat
// Total Cost: 8
// Ingredients: Oraganic Bread, Organic Meat, Cheese, Veggie
Now, let’s create our BasicDecorator which implements the SandwichOrder Interface and also contains an instance of the object being wrapped (wrappee).
class Decorator : public SandwichOrder {
public:
SandwichOrder* order_;
Decorator(SandwichOrder* order) : order_{order} {}
int GetCost() {
return order_->GetCost();
}
std::string GetIngredient() {
return order_->GetIngredient();
}
};
The wrappee, SandwichOrder pointer, will be initialized in construction, and the Decorator overrides the GetCost() and the GetIngredient() virtual functions by invoking the wrappee’s calls to those methods. Seems like right now, the Decorator is just a empty wrapper that relays the method calls to the next level. If we replace the client call with wrapper, the output should still remain the same as previous
void ServeOrder(SandwichOrder* order) {
std::cout << "Total Cost: " << order->GetCost() << std::endl;
std::cout << "Ingredients: " << order->GetIngredient() << std::endl;
}
int main () {
SandwichOrder* sandwich1 = new BasicSandwich;
SandwichOrder* decorator1 = new Decorator(sandwich1);
ServeOrder(decorator1);
SandwichOrder* sandwich2 = new DeluxeSandwich;
SandwichOrder* decorator2 = new Decorator(sandwich2);
ServeOrder(decorator2);
return 0;
}
// Output
// Total Cost: 5
// Ingredients: Basic Bread and Meat
// Total Cost: 8
// Ingredients: Oraganic Bread, Organic Meat, Cheese, Veggie
At this point, the potential of the Decorator pattern is starting to reveal. We can wrap layers and layers of different decorators around objects, as long as they follow the SandwichOrder interface.
Now, let’s extend the Decorator class to form a decorator for adding condiments. We can assume that adding a condiment is $1 additional charge per condiment, and we also want to be sure to add that to the ingredients list. Since the Client’s call to GetIngredient needs a std::string to display, we want to return a “mega” std::string that contains all the ingredients in the sandwich. We can designate each layer of added Decorator to return the additional condiment they added, and combine it with the Ingredients that is contained in the wrapped object to get the aggregated composition. Starting to sound a little bit like recursion?
class CondimentDecorator : public Decorator {
public:
std::string condiment_;
int cost = 1;
CondimentDecorator(std::string condiment, SandwichOrder* order) :
condiment_{condiment}, Decorator(order) {}
int GetCost() {
return cost + Decorator::GetCost();
}
std::string GetIngredient() {
return condiment_ + " " + order_->GetIngredient();
}
};
The CondimentDecorator takes two arguments during construction. The first is the type of condiment to be added, and the second one is the wrapped Sandwich which is passed into the Decorator constructor. With this implemented, let’s try adding two different condiments to our BasicSandwich.
int main () {
SandwichOrder* sandwich1 = new BasicSandwich;
SandwichOrder* decorated1 = new CondimentDecorator("mayo", sandwich1);
SandwichOrder* decorated2 = new CondimentDecorator("mustard", decorated1);
ServeOrder(decorated2);
return 0;
}
// Output
// Total Cost: 7
// Ingredients: mustard mayo Basic Bread and Meat
Now we can also implement our SideDecorator class to add the different sides to our SandwichOrder. Additional sides are $2 per side, and again, we want to return the overall ingredients and total cost at the end.
class SideDecorator : public Decorator {
public:
std::string side_;
int cost = 2;
SideDecorator(std::string side, SandwichOrder* order) :
side_{side}, Decorator(order) {}
int GetCost() {
return cost + Decorator::GetCost();
}
std::string GetIngredient() {
return side_ + " " + order_->GetIngredient();
}
};
Now, we can experiment with different combinations of sides and condiments added in different orders, and watch them recursively unfold!
int main () {
SandwichOrder* sandwich1 = new DeluxeSandwich;
SandwichOrder* decorated1 = new CondimentDecorator("mayo", sandwich1);
SandwichOrder* decorated2 = new SideDecorator("pickle", decorated1);
SandwichOrder* decorated3 = new CondimentDecorator("mustard", decorated2);
SandwichOrder* final_order = new SideDecorator("onion", decorated3);
ServeOrder(final_order);
return 0;
}
// Output
// Total Cost: 14
// Ingredients: onion mustard pickle mayo Oraganic Bread, Organic Meat, Cheese, Veggie
What was so fascinating to me is the implementation of recursion in the decorator pattern and the flexibility of adding new decorators. You could also dynamically remove a decorator layer but you would need to implement a method in Decorator that returns the underlying wrapped object.
SandwichOrder* unwrap = dynamic_cast<SandwichOrder*>(final_order)->getWrapped();
Some other fun decorator projects you can try to recreate include:
- Text Formatting: start with a simple text processing example. Create a Text class that represents plain text. Then, implement decorators like BoldDecorator, ItalicDecorator, UnderlineDecorator and add different formatting styles to the text.
- Beverage Maker: Similar to the sandwich example, but imagine the different decorators you can add to it.
- ShapeDrawing: Suppose you’re building a graphics application. Start with a base Shape class that defines basic shape behavior. You can implement various shapes like circle, rectangle, triangle. Implement decorators like ColorDecorator and PatternDecorator that add visual enhancements to shapes without modifying their core behavior.