装饰模式(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方法,但是有没有什么更优雅的解决方案呢?

我们即将引入装饰模式的概念。

三、装饰模式概要

image-20230119134802024

  • 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流举例:image-20230119184230465

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

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

白龙码~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值