装饰模式

装饰模式(Decorator Pattern)是一种结构型设计模式,允许在不改变对象结构的情况下,动态地为对象添加额外的功能。下面是一个基于控件的例子,展示了如何使用装饰模式来增强控件的功能。

引入“装饰”设计模式的定义(实现意图):动态地给一个对象添加一些额外的职责,就增加功能来说,该模式相比生成子类更加灵活。

主要组成部分

  1. 组件接口(Component):定义一个接口,供具体组件和装饰者实现。这个接口通常包含一个或多个方法,供客户端调用。
  2. 具体组件(ConcreteComponent):实现组件接口的具体对象。它是被装饰的对象,提供基本的功能。
  3. 装饰者(Decorator):持有一个组件对象的引用,并实现组件接口,通常会在其方法中调用组件对象的方法。
  4. 具体装饰者(ConcreteDecorator):扩展装饰者的功能,添加新的行为。

例一:工作流程

  • Control(抽象控件类):定义了控件的基本接口。
  • ListCtrl(具体控件类):实现了 Control 接口,绘制普通的列表控件。
  • Decorator(抽象装饰器类):持有一个 Control 对象的引用,调用其 draw 方法。
  • BorderDec(边框装饰器类):在绘制控件的基础上,添加边框的绘制功能。
  • VerScrollBarDec(垂直滚动条装饰器类):在绘制控件的基础上,添加垂直滚动条的绘制功能。
  • HorScrollBarDec(水平滚动条装饰器类):在绘制控件的基础上,添加水平滚动条的绘制功能。
1. 定义组件接口
  • 创建一个抽象类 Control,定义一个纯虚函数 draw(),用于绘制控件。
//抽象的控件类
class Control
{
public:
    virtual void draw() = 0; //draw方法,用于将自身绘制到屏幕上
public:
    virtual ~Control() {} //做父类时析构函数应该为虚函数
};
2. 实现具体组件
  • 创建一个具体类 ListCtrl,继承自 Control,实现 draw() 方法,提供基本的控件绘制功能。
//列表控件类
class ListCtrl :public Control
{
public:
    virtual void draw()
    {
        cout << "绘制普通的列表控件!" << endl;//具体可以用DirectX或OpenGL来绘制
    }
};
3. 创建装饰者基类
  • 创建一个装饰者类 Decorator,继承自 Control,持有一个 Control 对象的引用,并在 draw() 方法中调用该对象的 draw() 方法。
//抽象的装饰器类
class Decorator : public Control
{
public:
    Decorator(Control* tmpctrl) :m_control(tmpctrl) {}//构造函数
    virtual void draw()
    {
        m_control->draw();//虚函数,调用的是哪个draw取决于m_control指向的对象
    }
private:
    Control* m_control;  //需要被装饰的其他控件,这里用的是父类指针Control *
};
4. 实现具体装饰者
  • 创建多个具体装饰者类,如 BorderDecVerScrollBarDecHorScrollBarDec,它们继承自 Decorator,在 draw() 方法中扩展功能(如绘制边框或滚动条)。
//具体的“边框”装饰器类
class BorderDec :public Decorator
{
public:
    BorderDec(Control* tmpctrl) :Decorator(tmpctrl) {}//构造函数
    virtual void draw()
    {
        Decorator::draw(); //调用父类的draw方法以保持以往已经绘制出的内容
        drawBorder(); //也要绘制自己的内容
    }
private:
    void drawBorder()
    {
        cout << "绘制边框!" << endl;
    }
};
//具体的“垂直滚动条”装饰器类
class VerScrollBarDec :public Decorator
{
public:
    VerScrollBarDec(Control* tmpctrl) :Decorator(tmpctrl) {}//构造函数
    virtual void draw()
    {
        Decorator::draw(); //调用父类的draw方法以保持以往已经绘制出的内容
        drawVerScrollBar(); //也要绘制自己的内容
    }
private:
    void drawVerScrollBar()
    {
        cout << "绘制垂直滚动条!" << endl;
    }
};
//具体的“水平滚动条”装饰器类
class HorScrollBarDec :public Decorator
{
public:
    HorScrollBarDec(Control* tmpctrl) :Decorator(tmpctrl) {}//构造函数
    virtual void draw()
    {
        Decorator::draw(); //调用父类的draw方法以保持以往已经绘制出的内容
        drawHorScrollBar(); //也要绘制自己的内容
    }
private:
    void drawHorScrollBar()
    {
        cout << "绘制水平滚动条!" << endl;
    }
};
5. 构建控件和装饰者
  • main() 函数中,创建一个 ListCtrl 对象。
  • 使用装饰者类包装该对象,添加所需的功能。例如,先用 BorderDec 装饰,再用 VerScrollBarDec 装饰。
int main()
{
    //(1)创建一个又带边框,又带垂直滚动条的列表控件
    //首先绘制普通的列表控件
    Control* plistctrl = new ListCtrl();
    //plistctrl->draw(); //这里先不绘制了

    //接着“借助普通的列表控件”,可以通过边框装饰器绘制出一个“带边框的列表控件”
    Decorator* plistctrl_b = new BorderDec(plistctrl); //注意形参,是普通列表控件,这里类型用Control *而不用Decorator *也是可以的
    //plistctrl_b->draw();

    //接着“借助带边框的列表控件”,可以通垂直滚动条装饰器绘制出一个“带垂直滚动条又带边框的列表控件”
    Decorator* plistctrl_b_v = new VerScrollBarDec(plistctrl_b); //注意形参,是带边框的列表控件
    plistctrl_b_v->draw(); //这里完整最终绘制

    cout << "-------------------------------" << endl;
    //(2)创建一个只带水平滚动条的列表控件
    //首先绘制普通的列表控件
    Control* plistctrl2 = new ListCtrl();
    //plistctrl2->draw(); //这里先不绘制了

    //接着“借助普通的列表控件”,可以通水平滚动条装饰器绘制出一个“带水平滚动条的列表控件”
    Decorator* plistctrl2_h = new HorScrollBarDec(plistctrl2); //注意形参,是普通列表控件
    plistctrl2_h->draw(); //这里完成最终绘制

    //(3)释放资源
    delete plistctrl_b_v;
    delete plistctrl_b;
    delete plistctrl;

    delete plistctrl2_h;
    delete plistctrl2;

    return 0;
}

UML 图

装饰模式 UML 图1

UML 图解析
  • Control(抽象构件)

    • 这是一个接口,定义了所有具体构件和装饰器必须实现的 draw() 方法。
    • 其目的是提供一个统一的接口,使得客户端可以以一致的方式操作未被装饰的对象和经过装饰的对象,实现透明操作。
  • ListCtrl(具体构件)

    • 继承自 Control,实现了 draw() 方法,提供基本的列表控件绘制功能。
    • 作为被装饰的对象,允许后续的装饰者为其添加额外的功能。
  • Decorator(抽象装饰器类)

    • 同样继承自 Control,持有一个指向 Control 对象的指针(m_control)。
    • 实现了 draw() 方法,调用 m_controldraw() 方法,以保持原有的绘制功能,并允许在此基础上添加新的行为。
  • BorderDec、VerScrollBarDec、HorScrollBarDec(具体装饰器类)

    • 这些类继承自 Decorator,各自实现了额外的功能。
    • 通过重写 draw() 方法,调用父类的 draw() 方法,再添加自己的绘制逻辑(如绘制边框、垂直滚动条和水平滚动条)。

例二:工作流程

  • Beverage(抽象饮料类):定义了饮料的基本接口。
  • FruitBeverage(具体饮料类):实现了 Beverage 接口,返回普通水果饮料的价格。
  • Decorator(抽象装饰器类):持有一个 Beverage 对象的引用,调用其 getprice 方法。
  • SugarDec(砂糖装饰器类):在基础饮料价格上增加1元。
  • MilkDec(牛奶装饰器类):在基础饮料价格上增加2元。
  • BubbleDec(珍珠装饰器类):在基础饮料价格上增加2元。
1. 定义组件接口
  • 创建一个抽象类 Beverage,定义一个纯虚函数 getprice(),用于获取饮料价格。
// 抽象的饮料类
class Beverage {
public:
    virtual int getprice() = 0; // 获取价格
    virtual ~Beverage() {}
};
2. 实现具体组件
  • 创建一个具体类 FruitBeverage,继承自 Beverage,实现 getprice() 方法,返回水果饮料的基本价格。
// 水果饮料类
class FruitBeverage : public Beverage {
public:
    virtual int getprice() override {
        return 10; // 一杯单纯的水果饮料,售价为10元
    }
};
3. 创建装饰者基类
  • 创建一个装饰者类 Decorator,继承自 Beverage,持有一个 Beverage 对象的引用,并在 getprice() 方法中调用该对象的 getprice() 方法。
// 抽象的装饰器类
class Decorator : public Beverage {
public:
    Decorator(Beverage* tmpbvg) : m_pbvg(tmpbvg) {} // 构造函数
    virtual int getprice() override {
        return m_pbvg->getprice(); // 调用被装饰饮料的 getprice 方法
    }
    virtual ~Decorator() {
        delete m_pbvg; // 释放被装饰饮料的内存
    }
private:
    Beverage* m_pbvg; // 需要被装饰的饮料
};
4. 实现具体装饰者
  • 创建多个具体装饰者类,如 SugarDecMilkDecBubbleDec,它们继承自 Decorator,在 getprice() 方法中扩展功能(如增加价格)。
// 具体的“砂糖”装饰器类
class SugarDec : public Decorator {
public:
    SugarDec(Beverage* tmpbvg) : Decorator(tmpbvg) {} // 构造函数
    virtual int getprice() override {
        return Decorator::getprice() + 1; // 额外加1元
    }
};

// 具体的“牛奶”装饰器类
class MilkDec : public Decorator {
public:
    MilkDec(Beverage* tmpbvg) : Decorator(tmpbvg) {} // 构造函数
    virtual int getprice() override {
        return Decorator::getprice() + 2; // 额外加2元
    }
};

// 具体的“珍珠”装饰器类
class BubbleDec : public Decorator {
public:
    BubbleDec(Beverage* tmpbvg) : Decorator(tmpbvg) {} // 构造函数
    virtual int getprice() override {
        return Decorator::getprice() + 2; // 额外加2元
    }
};
5. 构建饮料和装饰者
  • main() 函数中,创建一个 FruitBeverage 对象。
  • 使用装饰者类包装该对象,添加所需的功能。例如,先用 BubbleDec 装饰,再用 SugarDec 装饰。
int main() {
    // 创建一杯单纯的水果饮料,价格10元
    Beverage* pfruit = new FruitBeverage();
    
    // 向饮料中增加珍珠,价格多加了2元
    Decorator* pfruit_addbubb = new BubbleDec(pfruit);
    
    // 再向饮料中增加砂糖,价格又多加了1元
    Decorator* pfruit_addbubb_addsugar = new SugarDec(pfruit_addbubb);
    
    // 输出最终的价格
    cout << "加了珍珠又加了砂糖的水果饮料最终价格是:" 
         << pfruit_addbubb_addsugar->getprice() << "元人民币" << endl;

    // 释放资源
    delete pfruit_addbubb_addsugar; // 删除最外层装饰器
    delete pfruit_addbubb; // 删除中间装饰器
    delete pfruit; // 删除基础饮料

    return 0;
}

UML 图

装饰模式 UML 图2

UML 图解析
  • Beverage(抽象饮料类)

    • 这是一个接口,定义了所有具体饮料和装饰器必须实现的 getprice() 方法。
    • 其目的是提供统一的接口,使客户端能够以一致的方式操作未被装饰的对象和经过装饰的对象,实现透明操作。
  • FruitBeverage(具体饮料类)

    • 继承自 Beverage,实现了 getprice() 方法,返回水果饮料的基本价格(10元)。
    • 作为被装饰的对象,允许后续的装饰者为其添加额外的功能。
  • Decorator(抽象装饰器类)

    • 同样继承自 Beverage,持有一个指向 Beverage 对象的指针(m_pbvg)。
    • 实现了 getprice() 方法,调用 m_pbvggetprice() 方法,以保持原有的价格计算功能,并允许在此基础上添加新的价格调整。
  • SugarDec、MilkDec、BubbleDec(具体装饰器类)

    • 这些类继承自 Decorator,各自实现了额外的功能。
    • 通过重写 getprice() 方法,调用父类的 getprice() 方法,再增加自己的价格(如砂糖加1元、牛奶加2元、珍珠加2元),实现对饮料价格的动态扩展。

优缺点

优点

  • 灵活性:装饰模式允许在运行时动态地增加对象的功能,而不需要修改对象的结构。这使得功能的添加和组合变得更加灵活。

  • 可扩展性:可以通过添加新的装饰者类来扩展功能,避免了类的膨胀。每个装饰者都可以独立开发和维护。

  • 组合性:可以通过组合多个装饰者来创建复杂的功能,而不需要创建大量的子类。这种组合方式使得代码更整洁,易于理解。

  • 遵循单一职责原则:装饰者和具体组件各自负责不同的功能,符合单一职责原则,使得代码更加模块化。

缺点

  • 复杂性:使用装饰模式可能会导致代码结构变得复杂,尤其是在装饰者层次较多时,可能会让理解和调试变得困难。
  • 装饰者数量:如果不加控制,可能会出现过度装饰的情况,导致对象的功能变得难以理解或者过于复杂。
  • 性能开销:每个装饰者都增加了一层间接调用,可能会导致性能开销,尤其是在装饰链较长时。

适用场景

  • 需要动态添加功能的场景:当需要在运行时为对象添加额外功能时,装饰模式是一个很好的选择。例如,图形界面中的控件可以在不修改原有控件的情况下,动态添加边框、滚动条等功能。
  • 避免类的膨胀:当有许多功能组合的可能性时,使用装饰模式可以避免创建大量的子类。比如,饮料的不同配料组合。
  • 需要组合多个功能的场景:当对象的功能需要组合时,装饰模式提供了一种灵活的方式来实现这些组合,而不需要修改现有的类。
  • 需要遵循开闭原则的场景:当系统需要对扩展开放而对修改关闭时,装饰模式可以通过添加新的装饰者来实现,而不需要修改现有的代码。

例一:示例代码

#include <iostream>
using namespace std;

//抽象的控件类
class Control
{
public:
    virtual void draw() = 0; //draw方法,用于将自身绘制到屏幕上
public:
    virtual ~Control() {} //做父类时析构函数应该为虚函数
};

//列表控件类
class ListCtrl :public Control
{
public:
    virtual void draw()
    {
        cout << "绘制普通的列表控件!" << endl;//具体可以用DirectX或OpenGL来绘制
    }
};

//---------------------------------
//抽象的装饰器类
class Decorator : public Control
{
public:
    Decorator(Control* tmpctrl) :m_control(tmpctrl) {}//构造函数
    virtual void draw()
    {
        m_control->draw();//虚函数,调用的是哪个draw取决于m_control指向的对象
    }
private:
    Control* m_control;  //需要被装饰的其他控件,这里用的是父类指针Control *
};

//具体的“边框”装饰器类
class BorderDec :public Decorator
{
public:
    BorderDec(Control* tmpctrl) :Decorator(tmpctrl) {}//构造函数
    virtual void draw()
    {
        Decorator::draw(); //调用父类的draw方法以保持以往已经绘制出的内容
        drawBorder(); //也要绘制自己的内容
    }
private:
    void drawBorder()
    {
        cout << "绘制边框!" << endl;
    }
};
//具体的“垂直滚动条”装饰器类
class VerScrollBarDec :public Decorator
{
public:
    VerScrollBarDec(Control* tmpctrl) :Decorator(tmpctrl) {}//构造函数
    virtual void draw()
    {
        Decorator::draw(); //调用父类的draw方法以保持以往已经绘制出的内容
        drawVerScrollBar(); //也要绘制自己的内容
    }
private:
    void drawVerScrollBar()
    {
        cout << "绘制垂直滚动条!" << endl;
    }
};
//具体的“水平滚动条”装饰器类
class HorScrollBarDec :public Decorator
{
public:
    HorScrollBarDec(Control* tmpctrl) :Decorator(tmpctrl) {}//构造函数
    virtual void draw()
    {
        Decorator::draw(); //调用父类的draw方法以保持以往已经绘制出的内容
        drawHorScrollBar(); //也要绘制自己的内容
    }
private:
    void drawHorScrollBar()
    {
        cout << "绘制水平滚动条!" << endl;
    }
};

int main()
{
    //(1)创建一个又带边框,又带垂直滚动条的列表控件
    //首先绘制普通的列表控件
    Control* plistctrl = new ListCtrl();
    //plistctrl->draw(); //这里先不绘制了

    //接着“借助普通的列表控件”,可以通过边框装饰器绘制出一个“带边框的列表控件”
    Decorator* plistctrl_b = new BorderDec(plistctrl); //注意形参,是普通列表控件,这里类型用Control *而不用Decorator *也是可以的
    //plistctrl_b->draw();

    //接着“借助带边框的列表控件”,可以通垂直滚动条装饰器绘制出一个“带垂直滚动条又带边框的列表控件”
    Decorator* plistctrl_b_v = new VerScrollBarDec(plistctrl_b); //注意形参,是带边框的列表控件
    plistctrl_b_v->draw(); //这里完整最终绘制

    cout << "-------------------------------" << endl;
    //(2)创建一个只带水平滚动条的列表控件
    //首先绘制普通的列表控件
    Control* plistctrl2 = new ListCtrl();
    //plistctrl2->draw(); //这里先不绘制了

    //接着“借助普通的列表控件”,可以通水平滚动条装饰器绘制出一个“带水平滚动条的列表控件”
    Decorator* plistctrl2_h = new HorScrollBarDec(plistctrl2); //注意形参,是普通列表控件
    plistctrl2_h->draw(); //这里完成最终绘制

    //(3)释放资源
    delete plistctrl_b_v;
    delete plistctrl_b;
    delete plistctrl;

    delete plistctrl2_h;
    delete plistctrl2;

    return 0;
}

例二:示例代码

#include <iostream>
using namespace std;

//抽象的饮料类
class Beverage
{
public:
    virtual int getprice() = 0; //获取价格
public:
    virtual ~Beverage() {}
};

//水果饮料类
class FruitBeverage : public Beverage
{
public:
    virtual int getprice()
    {
        return 10;  //一杯单纯的水果饮料,售价为10元
    }
};

//抽象的装饰器类
class Decorator : public Beverage
{
public:
    Decorator(Beverage* tmpbvg) :m_pbvg(tmpbvg) {}//构造函数
    virtual int getprice()
    {
        return m_pbvg->getprice();
    }
private:
    Beverage* m_pbvg;
};
//具体的“砂糖”装饰器类
class SugarDec : public Decorator
{
public:
    SugarDec(Beverage* tmpbvg) :Decorator(tmpbvg) {}//构造函数
    virtual int getprice()
    {
        return Decorator::getprice() + 1; //额外加多1元,要调用父类的getprice方法以把以往的价格增加进来
    }
};
//具体的“牛奶”装饰器类
class MilkDec : public Decorator
{
public:
    MilkDec(Beverage* tmpbvg) :Decorator(tmpbvg) {}//构造函数
    virtual int getprice()
    {
        return Decorator::getprice() + 2; //额外加多2元,要调用父类的getprice方法以把以往的价格增加进来
    }
};
//具体的“珍珠”装饰器类
class BubbleDec : public Decorator
{
public:
    BubbleDec(Beverage* tmpbvg) :Decorator(tmpbvg) {}//构造函数
    virtual int getprice()
    {
        return Decorator::getprice() + 2; //额外加多2元,要调用父类的getprice方法以把以往的价格增加进来
    }
};

int main()
{
    //-------------------
    //创建一杯单纯的水果饮料,价格10元
    Beverage* pfruit = new FruitBeverage();
    //向饮料中增加珍珠,价格多加了2元
    Decorator* pfruit_addbubb = new BubbleDec(pfruit);
    //再向饮料中增加砂糖,价格又多加了1元
    Decorator* pfruit_addbubb_addsugar = new SugarDec(pfruit_addbubb);
    //输出最终的价格
    cout << "加了珍珠又加了砂糖的水果饮料最终价格是:" << pfruit_addbubb_addsugar->getprice() << "元人民币" << endl;

    //释放资源
    delete pfruit_addbubb_addsugar;
    delete pfruit_addbubb;
    delete pfruit;

    return 0;
}
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐