• 装饰模式(Decorator Pattern)


    目录

    定义

    类结构

    示例

    代码实现

    示例分析

    使用和不使用装饰模式比对   

    场景

    不使用装饰模式

    代码示例

    使用装饰模式

    代码示例

    分析

    图示分析比对

    适用场景

    动态地添加或修改对象的功能

    避免使用继承导致的类爆炸

    保持类的简单性和单一责任原则

    在运行时动态地添加或删除功能

    经典使用方案

    Java I/O库中的输入输出流

    GUI界面组件

     Web开发中的过滤器


    定义

    装饰模式(Decorator Pattern)是一种结构型设计模式,它允许通过将对象包装在装饰器类的实例中来动态地添加新的行为和责任。这种模式可以在不修改现有代码的情况下,灵活地扩展对象的功能。

    类结构

    1. Component(组件): 定义了一个抽象接口,用于具体组件和装饰器共享。
    2. ConcreteComponent(具体组件): 实现了Component接口的具体类,是被装饰的对象。
    3. Decorator(装饰器): 也实现了Component接口,并持有一个Component对象的引用,这是装饰的核心。
    4. ConcreteDecorator(具体装饰器): 扩展了Decorator类,负责具体的装饰操作。

    示例

    考虑一个咖啡店的场景,有不同种类的咖啡,你可以选择添加不同的配料,比如牛奶、糖和巧克力。使用装饰模式可以动态地为咖啡添加不同的配料,而不需要修改咖啡类的代码。

    代码实现

    1. #include
    2. #include
    3. // Step 1: Component(组件)
    4. class Coffee {
    5. public:
    6. virtual std::string getDescription() const = 0;
    7. virtual double cost() const = 0;
    8. virtual ~Coffee() = default;
    9. };
    10. // Step 2: ConcreteComponent(具体组件)
    11. class SimpleCoffee : public Coffee {
    12. public:
    13. std::string getDescription() const override {
    14. return "简单咖啡";
    15. }
    16. double cost() const override {
    17. return 5.0;
    18. }
    19. };
    20. // Step 3: Decorator(装饰器)
    21. class CoffeeDecorator : public Coffee {
    22. protected:
    23. Coffee* coffee;
    24. public:
    25. CoffeeDecorator(Coffee* c) : coffee(c) {}
    26. std::string getDescription() const override {
    27. return coffee->getDescription();
    28. }
    29. double cost() const override {
    30. return coffee->cost();
    31. }
    32. virtual ~CoffeeDecorator() {
    33. if(coffee)
    34. {
    35. delete coffee;
    36. }
    37. }
    38. };
    39. // Step 4: ConcreteDecorator(具体装饰器)
    40. class MilkDecorator : public CoffeeDecorator {
    41. public:
    42. MilkDecorator(Coffee* c) : CoffeeDecorator(c) {}
    43. std::string getDescription() const override {
    44. return coffee->getDescription() + ",加牛奶";
    45. }
    46. double cost() const override {
    47. return coffee->cost() + 2.0;
    48. }
    49. ~MilkDecorator () override {}
    50. };
    51. class SugarDecorator : public CoffeeDecorator {
    52. public:
    53. SugarDecorator(Coffee* c) : CoffeeDecorator(c) {}
    54. std::string getDescription() const override {
    55. return coffee->getDescription() + ",加糖";
    56. }
    57. double cost() const override {
    58. return coffee->cost() + 1.0;
    59. }
    60. ~SugarDecorator () override {}
    61. };
    62. int main() {
    63. // 创建一个简单咖啡
    64. Coffee* myCoffee = new SimpleCoffee();
    65. std::cout << "描述:" << myCoffee->getDescription() << ",价格:" << myCoffee->cost() << "元" << std::endl;
    66. // 使用装饰器动态添加牛奶
    67. myCoffee = new MilkDecorator(myCoffee);
    68. std::cout << "描述:" << myCoffee->getDescription() << ",价格:" << myCoffee->cost() << "元" << std::endl;
    69. // 使用装饰器再添加糖
    70. myCoffee = new SugarDecorator(myCoffee);
    71. std::cout << "描述:" << myCoffee->getDescription() << ",价格:" << myCoffee->cost() << "元" << std::endl;
    72. // 释放内存
    73. delete myCoffee;
    74. return 0;
    75. }

    示例分析

    在这个示例中,Coffee是组件,SimpleCoffee是具体组件,CoffeeDecorator是装饰器,MilkDecoratorSugarDecorator是具体装饰器。通过不同的组合,我们可以动态地扩展咖啡的描述和价格,而无需修改原始的咖啡类。这就是装饰模式

    使用和不使用装饰模式比对   

    场景

    还是咖啡店的例子。咖啡店提供多种咖啡,如浓缩咖啡(Espresso)、拿铁(Latte)、美式咖啡(Americano)等。顾客可以根据自己的口味选择添加不同的配料,如奶泡(Foam)、糖浆(Syrup)、焦糖(Caramel)等。

    不使用装饰模式

    如果不使用装饰模式,而是为每一种可能的咖啡组合创建一个新的类,那么需要的子类个数将是咖啡类型个数和装饰类型个数的乘积。这是因为对于每一种咖啡,都可以添加或不添加每一种装饰,所以组合的数量是两者之积。

    假设咖啡类型个数是 m,装饰的类型个数是 n,那么需要的类个数将是 m * n+m+1。(不考虑多个组合)

    例如,如果有 3 种咖啡(Espresso, Latte, Americano)和 3 种装饰(Foam, Syrup, Caramel),那么需要的子类个数将是 3 * 3 = 9 个,即:

    • EspressoWithFoam
    • EspressoWithSyrup
    • EspressoWithCaramel
    • LatteWithFoam
    • LatteWithSyrup
    • LatteWithCaramel
    • AmericanoWithFoam
    • AmericanoWithSyrup
    • AmericanoWithCaramel

    如果考虑同时添加多种装饰的组合,那么子类的个数将会是指数级的增长,而不是简单的乘积关系。这是因为每种咖啡都可以独立地添加或不添加每种装饰,而且装饰之间也可以相互组合。

    假设有 m 种咖啡和 n 种装饰,每种咖啡都可以添加或不添加每种装饰,那么对于每种咖啡,都有 2^n 种可能的装饰组合(包括不添加任何装饰的情况)。因此,对于 m 种咖啡,总的类个数将是 m * 2^n+m+1。

    例如,如果有 3 种咖啡(Espresso, Latte, Americano)和 3 种装饰(Foam, Syrup, Caramel),那么对于每种咖啡,都有 2^3 = 8 种可能的装饰组合。因此,总的子类个数将是 3 * 8 = 24 个。这还不包括那些可能同时添加多种装饰并且咖啡种类也不同的组合。

    从上述所叙可以看出这样做会导致类的数量迅速增加,而且很难扩展和维护。

    代码示例
    1. class Coffee {
    2. public:
    3. virtual void prepare() = 0;
    4. };
    5. class Espresso : public Coffee {
    6. public:
    7. void prepare() override {
    8. // Prepare espresso
    9. }
    10. };
    11. class EspressoWithFoam : public Espresso {
    12. public:
    13. void prepare() override {
    14. // Prepare with foam
    15. }
    16. };
    17. // ...其他咖啡类

    使用装饰模式

    使用装饰模式,我们可以创建一个Coffee接口和多个实现该接口的类(如EspressoLatteAmericano),然后创建一个CoffeeDecorator类,它实现了Coffee接口并持有一个Coffee对象。这样,我们可以创建多个CoffeeDecorator子类来添加不同的配料。

    假设咖啡类型个数是 m,装饰的类型个数是 n,那么需要的类个数将是 m+ n+1(装饰子类)+1(基类)。

    例如,如果有 3 种咖啡(Espresso, Latte, Americano)和 3 种装饰(Foam, Syrup, Caramel),我们需要创建的类个数是1+3+3+1=8个,相对于上面不使用装饰模式的示例,使用了装饰模式后,常见的类个数少了很多。

    代码示例
    1. class Coffee {
    2. public:
    3. virtual void prepare() = 0;
    4. virtual ~Coffee() = default;
    5. };
    6. class Espresso : public Coffee {
    7. public:
    8. void prepare() override {
    9. // Prepare espresso
    10. }
    11. };
    12. class CoffeeDecorator : public Coffee {
    13. protected:
    14. Coffee* decoratedCoffee;
    15. public:
    16. CoffeeDecorator(Coffee* coffee) : decoratedCoffee(coffee) {}
    17. void prepare() override {
    18. decoratedCoffee->prepare();
    19. }
    20. virtual ~CoffeeDecorator() {
    21. delete decoratedCoffee;
    22. }
    23. };
    24. class FoamDecorator : public CoffeeDecorator {
    25. public:
    26. FoamDecorator(Coffee* coffee) : CoffeeDecorator(coffee) {}
    27. void prepare() override {
    28. // Add foam
    29. decoratedCoffee->prepare();
    30. }
    31. ~FoamDecorator () override {}
    32. };
    33. class SyrupDecorator : public CoffeeDecorator {
    34. public:
    35. SyrupDecorator(Coffee* coffee) : CoffeeDecorator(coffee) {}
    36. void prepare() override {
    37. // Add syrup
    38. decoratedCoffee->prepare();
    39. }
    40. ~SyrupDecorator () override {}
    41. };
    42. // 使用示例
    43. int main() {
    44. Coffee* myCoffee = new Espresso();
    45. myCoffee = new FoamDecorator(myCoffee);
    46. myCoffee = new SyrupDecorator(myCoffee);
    47. myCoffee->prepare(); // Prepares espresso with foam and syrup
    48. delete myCoffee; // Deletes the entire decoration chain
    49. return 0;
    50. }
    分析

    这里实现了灵活添加组件

    1. Coffee* myCoffee = new Espresso();
    2. myCoffee = new FoamDecorator(myCoffee);
    3. myCoffee = new SyrupDecorator(myCoffee);

    避免使用继承导致的类爆炸

     通过组件通过添加装饰类的方式,将装饰功能分离,而不需要使用继承或者组合的方式,解耦了功能的扩展,新的装饰添加时,不需要改动组件,只要添加新的装饰类即可,避免了需要不断继承带来的风险和大量继承类。保持类的简单性和单一责任原则

    图示分析比对

    不使用装饰模式

    使用装饰模式

    适用场景

    动态地添加或修改对象的功能

    当需要动态地为一个对象添加额外的功能,而且希望这些功能可以灵活组合时,装饰模式是一个很好的选择。这样可以避免使用大量子类来实现所有可能的组合,而是使用装饰器来动态地添加这些功能。


    避免使用继承导致的类爆炸

    经常会发现在类的层次结构中添加新功能导致的子类爆炸问题。装饰模式通过将功能分离到单独的装饰器类中,避免了这种情况的发生。


    保持类的简单性和单一责任原则

    使用装饰模式可以将一些复杂的功能分离到单独的装饰器类中,使得原始类保持简单和具有单一职责。

    在运行时动态地添加或删除功能

    装饰模式允许在运行时动态地添加或删除对象的功能,这对于某些情况下的配置和扩展非常有用。

    经典使用方案


    Java I/O库中的输入输出流

     Java中的输入输出流就是一个典型的装饰器模式的例子。基本的InputStream或OutputStream可以通过添加额外的功能,比如缓冲、加密或压缩等,而无需修改它们的代码。


    GUI界面组件

     在GUI编程中,经常需要动态地添加新的功能或外观到用户界面组件上。比如,一个简单的文本框可以通过装饰模式来添加滚动条、边框、背景色等功能,而无需修改原始文本框类的代码。


     Web开发中的过滤器

     在Web开发中,过滤器常常用于对请求或响应进行处理,比如身份验证、日志记录、数据压缩等。使用装饰模式可以轻松地添加新的过滤功能,同时保持代码的灵活性和可维护性。

  • 相关阅读:
    数字孪生行业相关政策梳理--智慧水利领域相关政策(可下载)
    5款开源、美观、强大的WPF UI组件库
    Maven仓库介绍
    数据结构:AVL树的实现和全部图解
    M1 安装CentOS 8安装完成后无法联网怎么办?
    OkHttp原理分析总结
    elasticsearch7.12 agg分组聚合分页同段同句查询
    硬之城携手阿里云 Serverless 应用引擎(SAE)打造低代码平台
    Keeplived练习
    离谱了!京东T7手写「并发编程知识手册」,从原理到项目实战详解
  • 原文地址:https://blog.csdn.net/weixin_40026739/article/details/136226138