A case in point
Star Baz is a fast expanding coffee shop. Due to its rapid expansion, Star Baz decided to update its order system to meet the needs of different users. The initial design was as follows:
- Beverage is an abstract class from which all beverages served in the store must be inherited.
Cost()
Methods are abstract, and subclasses must define their own implementations;- Each subclass implementation
Cost()
To return the price of the drink; - called
description_
Is set by each subclass to describe the drink. usingGetDescription()
To return this description.
Generally speaking, there are house blends, DarkRoast, Decaf, Espresso.
Meanwhile, customers can buy any number of seasonings when buying coffee, such as Steamed Milk, Soy Milk, Mocha or covered Milk foam (Whip). Starbaz will charge different rates for the spices they add. So the ordering system has to take these ingredients into account.
Obviously, this is a maintenance nightmare. What if milk prices go up? What if you add another spice?
An improved
One problem with the above design is that each combination creates a class that can actually use variables to track whether or not a spice is added. For example: TODO: figure
Start from the basic category of Beverage, and add the instance variable to indicate whether there is a certain kind of seasoning (milk, soybean milk, mocha, milk foam, etc.).
Now Cost() in Beverage is no longer an abstract method. We provide a concrete implementation that lets it calculate the price of spices for various spices to be added. The subclass still overwrites Cost(), but it calls Cost() of the parent class to calculate the price of the basic drink plus seasoning.
Now add subclasses, each of which represents a drink. The Cost() of the superclass calculates the Cost of all seasonings, while the overridden Cost() of the subclass extends the functionality of the superclass to include additional ingredients of the specified beverage type.
Each Cost() method needs to calculate the price of the drink and then add the Cost of the seasoning by calling Cost() of the base class.
The implementation of Beverage is as follows:
class Beverage {
public:
std::string GetDescription(a) {
if (HasMilk()){
description_ += ", Milk";
}
if (HasSoy()) {
description_ += ", Soy";
}
if (HasWhip()) {
description_ += ", Whip";
}
if (HasMocha()) {
description_ += ", Mocha";
}
return description_;
}
virtual double Cost(a) {
double condiment_cost = 0;
if (has_milk_){
condiment_cost += milk_cost_;
}
if (has_soy_) {
condiment_cost += soy_cost_;
}
if (has_whip_) {
condiment_cost += whip_cost_;
}
if (has_mocha_) {
condiment_cost += mocha_cost_;
}
return condiment_cost;
}
void SetMilk(bool need){
has_milk_ = need;
}
void SetSoy(bool need){
has_soy_ = need;
}
void SetWhip(bool need){
has_whip_ = need;
}
void SetMocha(bool need){
has_mocha_ = need;
}
private:
double milk_cost_ = 0.10;
double whip_cost_ = 0.10;
double soy_cost_ = 0.15;
double mocha_cost_ = 0.20;
bool has_milk_ = false;
bool has_whip_ = false;
bool has_soy_ = false;
bool has_mocha_ = false;
protected:
std::string description_;
};
Copy the code
The realization of specific drinks:
class DarkRoast : public Beverage {
public:
DarkRoast() {
description_ = "Dark Roast";
}
double Cost(a) {
return Beverage::Cost() + 0.99; }};Copy the code
class Espresso : public Beverage {
public:
Espresso() {
description_ = "Espresso";
}
double Cost(a) {
return Beverage::Cost() + 1.99; }};Copy the code
To make coffee:
void offer(a) {
Beverage* beverage = new Espresso(a); std::cout << beverage->GetDescription() < <" costs $" << beverage->Cost() << std::endl;
Beverage* beverage2 = new DarkRoast(a); beverage2->SetMocha(true);
beverage2->SetWhip(true);
std::cout << beverage2->GetDescription() < <" costs $" << beverage2->Cost() << std::endl;
Beverage* beverage3 = new HouseBlend(a); beverage3->SetSoy(true);
beverage3->SetMocha(true);
beverage3->SetWhip(true);
std::cout << beverage3->GetDescription() < <" costs $" << beverage3->Cost() << std::endl;
}
Copy the code
Code result:
Espresso costs The $1.99
Dark Roast, Whip, Mocha costs The $1.29
HouseBlend, Soy, Whip, Mocha costs The $134.Copy the code
Question:
- Once the seasoning price changes, the base class code needs to be changed, and this affects all subclasses;
- As soon as there is a new spice, we need to add a new method and change the cost method in the superclass;
- New beverages may be developed in the future, for which certain flavorings may not be appropriate (e.g., iced Tea), but in this design approach, the Tea subclass will inherit those unsuitable methods, e.g
SetWhip(true)
(with milk foam); - What if the customer wants double mocha?
Open – close principle
Classes should be open for extension and closed for modification
- Open: You are welcome to extend our classes with any behavior you want to meet different needs;
- Closed: It took us a long time to get the code right and all the bugs worked out, so we can’t let you modify existing code. The code must be closed to prevent modification.
Decorator pattern
Now that we have realized that inheritance is not a perfect solution, we have encountered problems such as class explosion, rigid design, and the addition of new functionality to the base class that does not apply to all subclasses.
Here we take a different approach: we use the drink as the main body, and then we decorate the drink with seasonings at run time. For example, if a customer wants a mocha and a dark roast with milk foam, here’s what to do:
- Take a DarkRoast object
- Decorate it with Mocha objects;
- Decorate it with a Whip object;
- call
Cost()
And rely on the commission to add the price of spices.
Construct beverage order with decorator
-
Start with a DarkRoast object
-
The customer wants a Mocha, so he creates a Mocha object and wraps it around a DarkRoast object
-
Customers also want whips, so they need to create a Whip decorator and wrap Mocha objects with it
-
Now, it’s time to count the money for the customer. You can do this by calling Cost() on the outermost decorator object. The Cost() of the Whip will first entrust the object it decorates (that is, Mocha) to calculate the price, and then add the price of the foam.
Everything we know so far
- Decorator and decorator have the same supertype;
- You can decorate an object with one or more decorators;
- Since the decorator and the decorated object have the same supertype, the decorated object can be used instead of the original object whenever it needs to be wrapped;
- The decorator can add his own behavior before and after the entrusted behavior of the decorator to achieve a specific purpose;
- Objects can be decorated at any time, so they can be decorated dynamically and indefinitely with any decorator at run time.
Define decorator patterns
The decorator pattern dynamically attaches responsibility to objects. To extend functionality, decorators offer a more flexible alternative to inheritance.
The class diagram is as follows:
Corresponding to this example, there is the following class diagram:
The final code
Menu:
coffee | The price |
---|---|
HouseBlend | 0.89 |
Dark Roast | 0.99 |
Espresso | 1.99 |
Decat | 1.05 |
seasoning | The price |
---|---|
Milk | 0.10 |
Soy | 0.15 |
Mocha | 0.20 |
Whip | 0.10 |
Beverage Base:
class Beverage {
public:
virtual std::string GetDescription(a){
return description_;
}
virtual double Cost(a) = 0;
protected:
std::string description_ = "Unknown Beverage";
};
Copy the code
class CondimentDecorator : public Beverage {
public:
virtual std::string GetDescription(a) = 0;
};
Copy the code
Write the code for the drink:
class Espresso : public Beverage {
public:
Espresso(){
description_ = "Espresso";
}
double Cost(a) override {
return 1.99;
};
};
Copy the code
class HouseBlend : public Beverage {
public:
HouseBlend(){
description_ = "House Blend Coffee";
}
double Cost(a) override {
return 0.89;
};
};
Copy the code
Write the code for seasoning:
class Mocha : public CondimentDecorator {
public:
Mocha(Beverage* beverage){
this.beverage = beverage;
}
std::string GetDescription(a) override {
return beverage->GetDescription + ", Mocha";
}
double Cost(a) override {
return beverage->Cost() + 0.20; }};Copy the code
Coffee served:
void offer(a){
Beverage* beverage = new Espresso(a); std::cout << beverage->GetDescription() < <" costs " << beverage->Cost() < <"$";
Beverage* beverage2 = new DarkRoast(a); beverage2 =new Mocha(beverage2);
beverage2 = new Mocha(beverage2);
beverage2 = new Whip(beverage2);
std::cout << beverage2->GetDescription() < <" costs " << beverage2->Cost() < <"$";
Beverage* beverage3 = new HouseBlend(a); beverage3 =new Soy(beverage3);
beverage3 = new Mocha(beverage3);
beverage3 = new Whip(beverage3);
std::cout << beverage3->GetDescription() < <" costs " << beverage3->Cost() < <"$";
}
Copy the code
Code result:
Espresso costs The $1.99
DarkRoast, Mocha, Mocha, Whip costs The $1.49
House Blend Coffee, Soy, Mocha, Whip costs The $134.Copy the code
Review the problems with previous designs:
- Once the seasoning price changes, the base class code needs to be changed, and this affects all subclasses;
- As soon as there is a new spice, we need to add a new method and change the cost method in the superclass;
- New beverages may be developed in the future, for which certain flavorings may not be appropriate (e.g., iced Tea), but in this design approach, the Tea subclass will inherit those unsuitable methods, e.g
SetWhip(true)
(with milk foam); - What if the customer wants double mocha?
Application scenarios for the decorator pattern
The structure and characteristics of the decorator pattern have been explained before, and the application scenarios are described below. The decorator pattern is usually used in the following situations.
- When additional responsibilities need to be added to an existing class that cannot be extended by subclassing. For example, if the class is hidden or the class is the ultimate class or inherits a large number of subclasses.
- Inheritance is difficult to implement when you need to produce a lot of functionality by permutations and combinations of an existing set of basic functionality, and decorator patterns are fine.
- When the functional requirements of an object can be dynamically added or removed.
Extensions to the decorator pattern
The four roles contained in the decorator pattern are not required at all times, and the pattern can be simplified in some applications, such as the following two cases.
-
If there is only one concrete component and no abstract component, you can have the abstract decoration inherit the concrete component.
-
If there is only one concrete decoration, you can combine the abstract decoration with the concrete decoration.