装饰模式(decorator-pattern)
文章目录
- 装饰模式(decorator-pattern)
- 一、手抓饼点餐系统
- 二、要求进阶
- 三、装饰模式概要
- 四、装饰模式的优劣及应用场景
- 1. 优点
- 2.缺点
- 3.应用场景
一、手抓饼点餐系统
请设计一个手抓饼点餐系统,支持加配菜,比如里脊、肉松、火腿等,并提供接口,获取加了哪些配菜,和最后的总价。
听起来很简单嘛,代码敲起来:
"ingredient.hpp"
,用来定义各种配料。
class Ingredient {
public:
virtual std::string get_description() = 0;
virtual double get_price() = 0;
protected:
std::string description_;
double price_;
};
class Ham : public Ingredient {
public:
Ham() {
description_ = "火腿";
price_ = 1.0;
}
std::string get_description() {
return description_;
}
double get_price() {
return price_;
}
};
class Tenderloin : public Ingredient {
public:
Tenderloin() {
description_ = "里脊肉";
price_ = 2.0;
}
std::string get_description() {
return description_;
}
double get_price() {
return price_;
}
};
class PorkFloss : public Ingredient {
public:
PorkFloss() {
description_ = "肉松";
price_ = 1.5;
}
std::string get_description() {
return description_;
}
double get_price() {
return price_;
}
};
"shredded_cake.hpp"
,定义手抓饼。
class ShreddedCake {
public:
ShreddedCake() : description_("饼, 单价3元\n"), total_price_(3.0) {}
void add_ingredient(Ingredient *ingredient) {
description_ += ingredient->get_description() + "\n";
total_price_ += ingredient->get_price();
}
std::string get_description() {
return description_;
}
double get_total_price() {
return total_price_;
}
private:
std::string description_;
double total_price_;
};
最后的测试:
int main() {
ShreddedCake shredded_cake;
std::shared_ptr<Ham> ham = std::make_shared<Ham>();
std::shared_ptr<Tenderloin> tenderloin = std::make_shared<Tenderloin>();
std::shared_ptr<PorkFloss> pork_floss = std::make_shared<PorkFloss>();
shredded_cake.add_ingredient(ham.get());
shredded_cake.add_ingredient(tenderloin.get());
shredded_cake.add_ingredient(pork_floss.get());
std::cout << "手抓饼配料: \n" << shredded_cake.get_description() << std::endl;
std::cout << "总价: " << shredded_cake.get_total_price() << std::endl;
return 0;
}
// output
手抓饼配料:
饼, 单价3元
火腿, 单价1元
里脊肉, 单价2元
肉松, 单价1.5元
总价: 7.5元
很简单的实现,对吧?
二、要求进阶
现在,店铺扩张了,不仅可以点手抓饼,还有煎饼、汤包啥的,咋办呢?
简单,写一个抽象Breakfast类,让各种早点继承它就ok了:
class BreakFast {
public:
virtual void add_ingredient(Ingredient *ingredient) = 0;
virtual std::string get_description() = 0;
virtual double get_total_price() = 0;
protected:
std::string description_;
double total_price_;
};
class ShreddedCake : public BreakFast {
public:
ShreddedCake() {
description_ = "饼, 单价3元\n";
total_price_ = 3.0;
}
void add_ingredient(Ingredient *ingredient) {
description_ += ingredient->get_description() + "\n";
total_price_ += ingredient->get_price();
}
std::string get_description() {
return description_;
}
double get_total_price() {
return total_price_;
}
};
class Pancake : public BreakFast {
public:
Pancake() {
description_ = "煎饼, 单价2元\n";
total_price_ = 2.0;
}
void add_ingredient(Ingredient *ingredient) {
description_ += ingredient->get_description() + "\n";
total_price_ += ingredient->get_price();
}
std::string get_description() {
return description_;
}
double get_total_price() {
return total_price_;
}
};
class SteamedDumpling : public BreakFast {
public:
SteamedDumpling() {
description_ = "汤包, 单价7元\n";
total_price_ = 7.0;
}
std::string get_description() {
return description_;
}
double get_total_price() {
return total_price_;
}
};
但这样一份代码有一个尴尬的问题:
SteamedDumpling
,即汤包类,没有重写抽象方法add_ingredient
,虽然编译通过但是不能正常使用这个类。- 但是,汤包在现实生活中,也不需要
add_ingredient
,即加配料。
怎么办呢?一个办法,就是为需要加配料的早餐添加add_ingredient
方法,但是有没有什么更优雅的解决方案呢?
我们即将引入装饰模式的概念。
三、装饰模式概要
- Abstract BaseClass
- 抽象基类
- 提供抽象方法Operation
- Concrete DerivedClass
- 派生于抽象基类,用来描述具体的对象特性
- 重写抽象方法Operation(纯虚方法)
- Decorator
- 装饰抽象类,派生于抽象基类,用于装饰Concrete DerivedClass
- Concrete Decorator
- 重写抽象方法Operation
- 可以添加自己私有的成员变量和成员函数,用于给Operation扩展新功能
听起来非常抽象,我们改写代码后进行分析:
"breakfast.hpp"
class Breakfast {
public:
virtual std::string get_description() = 0;
virtual double get_price() = 0;
protected:
std::string description_;
double price_;
};
class ShreddedCake : public Breakfast {
public:
ShreddedCake() {
description_ = "手抓饼, 单价3元";
price_ = 3.0;
}
std::string get_description() {
return description_ + "\n";
}
double get_price() {
return price_;
}
};
class Pancake : public Breakfast {
public:
Pancake() {
description_ = "煎饼, 单价2元";
price_ = 2.0;
}
std::string get_description() {
return description_ + "\n";
}
double get_price() {
return price_;
}
};
class SteamedDumpling : public Breakfast {
public:
SteamedDumpling() {
description_ = "汤包, 单价7元";
price_ = 7.0;
}
std::string get_description() {
return description_ + "\n";
}
double get_price() {
return price_;
}
};
这是早餐类的继承体系,基类是Breakfast,提供两个纯虚函数,派生出的三个子类重写这两个函数。
- Breakfast对应Abstract BaseClass
- ShreddedCake、Pancake和SteamedDumpling对应Concrete DerivedClass
"ingredient.hpp"
class Ingredient : public Breakfast {
protected:
Breakfast *breakfast_;
};
class Ham : public Ingredient {
public:
Ham(Breakfast *breakfast) {
breakfast_ = breakfast;
description_ = "火腿, 单价1元";
price_ = 1.0;
}
std::string get_description() {
return breakfast_->get_description() + description_ + "\n";
}
double get_price() {
return breakfast_->get_price() + price_;
}
};
class Tenderloin : public Ingredient {
public:
Tenderloin(Breakfast *breakfast) {
breakfast_ = breakfast;
description_ = "里脊肉, 单价2元";
price_ = 2.0;
}
std::string get_description() {
return breakfast_->get_description() + description_ + "\n";
}
double get_price() {
return breakfast_->get_price() + price_;
}
};
class PorkFloss : public Ingredient {
public:
PorkFloss(Breakfast *breakfast) {
breakfast_ = breakfast;
description_ = "肉松, 单价1.5元";
price_ = 1.5;
}
std::string get_description() {
return breakfast_->get_description() + description_ + "\n";
}
double get_price() {
return breakfast_->get_price() + price_;
}
};
这是配料类的继承体系,Ingredient继承自Breakfast,由于没有重写纯虚函数,因此也是抽象类。
派生出的三个子类重写了get_description
和get_price
两个纯虚函数。
- Ingredient对应装饰基类Decorator
- Tenderloin、PorkFloss对应Concrete Decorator,即具体的装饰子类。
- 由于是装饰类,因此它们可以有自己的方法和变量,其中不可或缺的是一个指向父类的Breakfast指针。
- 通过指向父类的Breakfast指针,装饰类可以获得父类的特性,并且将自己的新特性追加过去。
- 比如装饰类中重写
get_price
:先使用breakfast指针获取父类的价格,再将自身的价格追加过去。
- 比如装饰类中重写
"main.cpp"
int main() {
try {
std::shared_ptr<Breakfast> breakfast = std::make_shared<Pancake>();
std::shared_ptr<Breakfast> add_1_ingredient = std::make_shared<Ham>(breakfast.get());
std::shared_ptr<Breakfast> add_2_ingredient = std::make_shared<Tenderloin>(add_1_ingredient.get());
std::shared_ptr<Breakfast> add_3_ingredient = std::make_shared<PorkFloss>(add_2_ingredient.get());
std::cout << "配料: \n" << add_3_ingredient->get_description() << std::endl;
std::cout << "总价: " << add_3_ingredient->get_price() << "元" << std::endl << std::endl;
} catch (const std::exception &e) {
std::cout << e.what() << std::endl;
}
return 0;
}
-
首先,让breakfast指向一个Pancake,表示要点一个煎饼。
-
其次,new一个Ham,并将刚刚的基类指针传过去,表示要追加一个火腿。
-
Tenderloin和PorkFloss同理。
-
加入我还要加一份肉松,那么可以添一句:
std::shared_ptr<Breakfast> add_4_ingredient = std::make_shared<PorkFloss>(add_3_ingredient.get());
四、装饰模式的优劣及应用场景
1. 优点
- 可以在不修改原先类的基础上给它追加一些新的特性,减少被装饰类的复杂程度。
- 相比于纯粹的继承体系,装饰可以通过用户主动的包装完成功能的复合叠加,而继承则需要老老实实地写多个类。假如有4个功能,则一共有2^4-1=15种复合情况,使用装饰模式只需要写4种装饰类,而继承则需要完成15个类(功能更多会导致指数爆炸!)
2.缺点
1. 会导致代码更加复杂,出现问题时排查难度加大。
3.应用场景
就像装饰模式的优点那样,如果你想动态地增加原有类的特性,那么装饰模式再合适不过了。
以Java的IO流举例:
InputStream中的FilterInputStream就是一个装饰基类,为其它的IO流类提供缓冲的功能。如果这里用继承,那么会有多少个类呢…想想都离谱!