装饰模式(decorator-pattern)

Posted 白龙码~

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了装饰模式(decorator-pattern)相关的知识,希望对你有一定的参考价值。

装饰模式(decorator-pattern)

文章目录

一、手抓饼点餐系统

请设计一个手抓饼点餐系统,支持加配菜,比如里脊、肉松、火腿等,并提供接口,获取加了哪些配菜,和最后的总价。

听起来很简单嘛,代码敲起来:

"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_;
  

;

但这样一份代码有一个尴尬的问题:

  1. SteamedDumpling,即汤包类,没有重写抽象方法add_ingredient,虽然编译通过但是不能正常使用这个类。
  2. 但是,汤包在现实生活中,也不需要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_descriptionget_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;

  1. 首先,让breakfast指向一个Pancake,表示要点一个煎饼。

  2. 其次,new一个Ham,并将刚刚的基类指针传过去,表示要追加一个火腿。

  3. Tenderloin和PorkFloss同理。

  4. 加入我还要加一份肉松,那么可以添一句:

    std::shared_ptr<Breakfast> add_4_ingredient  = std::make_shared<PorkFloss>(add_3_ingredient.get());
    

四、装饰模式的优劣及应用场景

1. 优点

  1. 可以在不修改原先类的基础上给它追加一些新的特性,减少被装饰类的复杂程度。
  2. 相比于纯粹的继承体系,装饰可以通过用户主动的包装完成功能的复合叠加,而继承则需要老老实实地写多个类。假如有4个功能,则一共有2^4-1=15种复合情况,使用装饰模式只需要写4种装饰类,而继承则需要完成15个类(功能更多会导致指数爆炸!)

2.缺点

1. 会导致代码更加复杂,出现问题时排查难度加大。

3.应用场景

就像装饰模式的优点那样,如果你想动态地增加原有类的特性,那么装饰模式再合适不过了。

以Java的IO流举例:

InputStream中的FilterInputStream就是一个装饰基类,为其它的IO流类提供缓冲的功能。如果这里用继承,那么会有多少个类呢…想想都离谱!

以上是关于装饰模式(decorator-pattern)的主要内容,如果未能解决你的问题,请参考以下文章

装饰模式笔记

设计模式—— 十七:装饰器模式

代理模式、装饰者模式

装饰模式(Decorator Pattern)

装饰器模式

设计模式之-装饰器模式