首页 > 编程笔记 > C++笔记 阅读:20

C++装饰器模式及其实现(非常详细)

装饰器模式(Decorator Pattern)用于动态地在对象上添加额外的职责而不修改其原始类的结构。

女士化妆就是一个常见的装饰器模式的例子。可以将女士视为一个对象,将化妆视为一个装饰器。女士本身有一个“原型”,也就是她的基础外貌和特征,但在一些场合下需要让她看起来更年轻、更有活力。这就可以使用化妆这个装饰器来帮助这位女士变得更有魅力。

具体来讲,可以创建一个“化妆”类,这个类接受一个女士对象作为参数,并在女士对象的基础上添加新的功能。例如可以创建一个“眼影”类,这个类接受一个女士对象作为参数,并为她添加涂眼影的功能。类似地,也可以创建“口红”“腮红”等类来为女士添加不同的特色。

通过这样的设计就可以动态地为一名女士添加或修改化妆的功能了。例如可以先给一名女士涂上眼影,然后涂上口红,这样她就会看起来更有精神。也可以随时去掉这些化妆功能,例如擦掉口红或眼影。

这就是装饰器模式的基本思想,也就是在不影响对象自身的基础上动态地添加或修改功能。

在装饰器模式中定义了一个装饰器类,该类将目标类作为其成员变量,并实现与目标对象相同的接口。装饰器类可以在执行目标对象的操作之前或之后添加附加功能。

与继承不同,装饰器模式使用组合的方式,以增加对象的功能和责任。通过多个装饰器对象,可以一层一层地添加新的行为和职责,从而形成一个装饰器链。

装饰器模式可以在不修改现有代码的情况下动态地添加新的功能;通过不同的组合方式,可以实现各种不同的功能组合。通过装饰器模式可以避免复杂的继承层次结构。但是装饰器模式可能会产生过多的细粒度对象,通过多个装饰器对象,可能会创建大量的对象,从而导致系统变得复杂;可能引入冗余的代码,在装饰器链中可能会出现重复的功能代码。

传统装饰器模式

装饰器模式的结构包含抽象组件(Component)、具体组件(ConcreteComponent)、装饰器(Decorator)和具体装饰器(ConcreteDecorator),如下图所示:


图 1 传统装饰器模式UML简图

C++11元编程下的结构设计

本文中实现的装饰器模式模块利用 C++ 的模板特性和 STL 容器对对象行为进行动态扩展,能够提高代码的灵活性和可维护性。

ConcreteDecoratorA 和 ConcreteDecoratorB 是具体的装饰对象,这两个模块用于实现具体的装饰的内容,如下图所示。


图 2 C++模板类装饰器模式UML简图

decorateeItfc 是装饰对象接口,对应于 decorator 模板类的模板参数。

decoratee 模板类作为 decorator 的内嵌类用于定义接收 decorateeItfc 模板参数,以便包覆 decorateeItfc 指针模块,方便进行模块管理和后续扩展。

decorator 模板类是装饰器的实现部分,提供了增加装饰、移除装饰、遍历装饰等操作。

C++装饰器模式实现和解析

本模块通过模板类 decorator 使用模板参数 itfcType 来指定需要被装饰的接口类型,实现了对不同类型对象的装饰功能,提供了两个宏以方便和规范实现接口的定义方法,并利用一个不暴露的类定义来约束接口的继承关系。

提供了灵活的功能扩展接口。通过在 decorator 类中定义一系列操作容器的成员函数和函数对象(如 decratMe() 和 decratMeCallback)以实现对被装饰对象的一系列操作。

使用迭代器支持遍历操作,通过提供 begin() 和 end() 函数,使 decorator 类支持通过迭代器遍历被装饰对象,定义基类接口,代码如下:
//designM/decorator.hpp
namespace decorator_private__
{
    struct stDecorateeItfc__{
        virtual~stDecorateeItfc__(){}
    };
}

宏 BEGIN_DECLARE_DECORTEE_ITFC 用来表示接口声明的开始,宏展开后实际上是一个结构体的定义,类的名字则由宏参数 name 提供,同时使用 name 参数定义同名的虚析构函数来保证内存安全。类定义从 decorator_private__::stDecorateeItfc__ 继承,提供了检查继承和约束关系的能力,代码如下:
//designM/decorator.hpp
#define BEGIN_DECLARE_DECORTEE_ITFC(name)  \
struct name:public decorator_private__::stDecorateeItfc__{  \
    virtual~name(){}

宏 DECORTEE_METHOD 是一个可变参的宏,参数 RET 定义了接口返回值;NAME 定义了接口名称,其他可变的参数是接口的参数类型表,并使用 __VA_ARGS__ 在纯虚函数中进行展开。通过 DECORTEE_METHOD 宏可以很方便地规范和定义接口函数。使用时可以使用 DECORTEE_METHOD 多次定义名称或者参数不同的接口。定义完成后使用宏 END_DECLARE_DECORTEE_ITFC() 结束定义,声明接口和结束定义的代码如下:
//designM/decorator.hpp
#define DECORTEE_METHOD(RET,NAME,...)     \
    virtual RET NAME(__VA_ARGS__) = 0;
#define END_DECLARE_DECORTEE_ITFC()  };

宏 DECLARE_DECORTEE_DEFAULT 提供了一个简单的接口定义方法,在这个宏的实现中仅仅实现了一个可变参名为 operation() 的函数,虽然函数的名字只有一个,但是可以利用 C++ 的函数重载功能实现多个不同的接口函数,这种方式在功能差异比较大的情况下容易导致代码理解困难,读者应该慎重地在方便性和灵活性之间进行权衡。如果在使用时不需要复杂的接口,则可以使用 DECLARE_DECORTEE_DEFAULT 声名接口,代码如下:
//designM/decorator.hpp
#define DECLARE_DECORTEE_DEFAULT(name,...)        \
    BEGIN_DECLARE_DECORTEE_ITFC(name)          \
        DECORTEE_METHOD(void,operation,__VA_ARGS__)  \
    END_DECLARE_DECORTEE_ITFC()

在 decorator 类内部定义了一个嵌套的模板类 decoratee,用于表示被装饰的对象。这样可以保持装饰器类和被装饰对象类的解耦,并且支持对不同类型的对象进行装饰。需要特别注意的是 decoratee 类中对于装饰品的管理并没有拥有并处理对象的内存,使用时需要在自己的代码中对内存进行管理,代码如下:
//designM/decorator.hpp
class decoratee
{
protected:
    itfc_t * p_imp__;    //实际的装饰品,类似实际的化妆品
public:
    decoratee():p_imp__(nullptr) {}
    decoratee(itfc_t * imp):p_imp__(imp){}
    decoratee(decoratee&& b): p_imp__(b.p_imp__) {}

    decoratee& operator=(decoratee&& b) {
        p_imp__ = b.p_imp__;
        return *this;
    }
    //方便调用实际的对象
    itfc_t * operator->() { return p_imp__; }

    void set(itfc_t * imp) { p_imp__ = imp; }
    itfc_t * get() { return p_imp__; }

    //装饰品数据类型包装数据类型
    using drte_t = decoratee;
    //装饰品
    using data_t = std::vector< drte_t >;
    using iterator = typename data_t::iterator;

    protected:
        data_t m_drtes__;
    public:
        decorator() {}
        virtual ~decorator() {}

模块设计了两个增加装饰对象的方法,一个以对象引用的方式添加,另一个以智能指针的方式添加。这两种方法都返回装饰对象在实际存储向量中的索引号。这个索引号将用在移除装饰对象的方法 remove() 中。

在 decorator 类中,并没有对装饰品对象进行内存管理和声明周期管理,在使用时需要在自己的业务代码中进行处理。读者也可以通过修改 decoratee 类将存储对象修改为智能指针以实现自动地对内存进行管理,添加和移除装饰品的代码如下:
//designM/decorator.hpp

size_t decrat(dcrte_t&dcrtee)
{
    m_dcrtes__.push_back(dcrtee);
    return m_dcrtes__.size() - 1;
}
size_t decrat(itfcType * dcrtee)
{
    m_dcrtes__.push_back(dcrte_t(dcrtee));
    return m_dcrtes__.size() - 1;
}
void remove(size_t idx)
{
    if(idx< m_dcrtes__.size()){
        m_dcrtes__.erase(m_dcrtes__.begin() + idx);
    }
}

模板函数 decratMe() 是一个可变参的模板函数,提供了调用可变参数的能力,但是约束了接口的名称,在 decratMe() 函数中通过 for 循环调用 operation() 函数来执行装饰操作,主要的目的是为宏 DECLARE_DECORTEE_DEFAULT 提供一个执行装饰动作的接口,代码如下:
template< typename...Params >
void decratMe(Params&&...args)
{
    for(size_t i = 0;i< m_dcrtes__.size(); ++i){
        m_dcrtes__[i]->operation(std::forward<Params >(args)...);
    }
}
函数 decratMeCallback() 有两个不同的实现,它们都是以回调函数的方式调用装饰动作的接口,调用这个函数将对所有的装饰对象进行遍历处理,将每个装饰品以传参的方式提交函数对象,以便在函数对象中进行使用。使用函数的对象的主要目的是提高装饰操作时的灵活性,将可变的内容留在实际工程代码中实现,代码如下:
//designM/decorator.hpp
void decratMeCallback(std::function< void(dcrte_t&) > cb)
{
    size_t count = m_dcrtes__.size();

    for(size_t i = 0;i< count;i ++){
        cb(m_dcrtes__[i]);
    }
}

void decratMeCallback(std::function< void(itfc_t * itfc) > cb)
{
    size_t count = m_dcrtes__.size();

    for(size_t i = 0;i< count;i ++){
        cb(m_dcrtes__[i].get());
    }
}

begin() 和 end() 方法提供了以迭代器访问装饰品的接口,begin() 函数可返回装饰对象向量头部迭代器,如此利用迭代器可以自由地使用装饰对象内容,代码如下:
     iterator begin(){return m_dcrtes__.begin();}
     iterator end(){return m_dcrtes__.end();}
};

C++装饰器模式应用示例

下面给出了两个使用示例:
【实例 1】首先使用宏 DECLARE_DECORTEE_DEFAULT 声明一个名为 IDecoratee 的接口类。DECLARE_DECORTEE_DEFAULT 宏会自动地扩展一个名为 operation() 的纯虚函数。

这个函数的参数类型是 const std::string&,返回值类型是 void,代码如下:
DECLARE_DECORTEE_DEFAULT(IDecoratee,const std::string&)
接下来实现被装饰者接口的具体类 ConcreteDecoratee1 和 ConcreteDecoratee2。这两个具体装饰品类都从 IDecoratee 继承而来,并实现了 operation() 函数,以此实现具体操作。在本例中通过终端输出自己类型的名字和传入的参数内容,代码如下:
//decorator.cpp
class ConcreteDecoratee1:public IDecoratee{
public:
    void operation(const std::string&str)override{
        std::cout<< "ConcreteDecoratee 1:"<< str<< std::endl;
    }
};

class ConcreteDecoratee2:public IDecoratee{
public:
    void operation(const std::string&str)override{
       std::cout<< "ConcreteDecoratee 2:"<< str<< std::endl;
    }
};

在 main() 函数中首先定义了一个装饰器对象 dcrtr,obj1 和 obj2 分别是两个装饰品,用来装饰 dcrtr 装饰器对象。调用 decratMe() 模板函数的函数执行装饰操作,代码如下:
//decorator.cpp
int main()
{
    decorator<IDecoratee > dcrtr;
    ConcreteDecoratee1 * obj1 = new ConcreteDecoratee1();
    ConcreteDecoratee2 * obj2 = new ConcreteDecoratee2();

    //将被装饰者对象添加到装饰者
    size_t idx1 = dcrtr.decrat(obj1);
    size_t idx2 = dcrtr.decrat(obj2);

    //对所有被装饰者对象进行操作
    dcrtr.decratMe("Hello");
        delete obj1;
        delete obj2;
    return 0;
}
上面的例子的执行结果如下:

ConcreteDecoratee 1:Hello
ConcreteDecoratee 2:Hello


【实例 2】首先使用接口定义宏定义接口类 ExampleClass__,然后定义一个接口函数 print(),这个函数的返回值是 void,没有需要传入的参数内容,代码如下:
//decorator2.cpp
BEGIN_DECLARE_DECORTEE_ITFC(ExampleClass__)
    DECORTEE_METHOD(void,print)
END_DECLARE_DECORTEE_ITFC()

class ExampleClass:public ExampleClass__
{
public:
    std::string name;
    ExampleClass(){}
    ExampleClass(const std::string&n):name(n){}
    void print()
    {
        std::cout<< "Name:"<< name<< std::endl;
    }
};

在主函数 main() 中,创建了一个 decorator 对象,以此来装饰 ExampleClass 对象:
这个示例展示了装饰器模式的灵活性和扩展性,可以动态地为对象添加功能,而不必修改其源代码,主程序中的代码如下:
//decorator2.cpp
#include <iostream>
#include "designM/decorator.hpp"

int main()
{
    decorator<ExampleClass> dec;

    //创建一个 ExampleClass 对象并装饰
    ExampleClass * obj1 = new ExampleClass("Obj1");
    dec.decorat(obj1);

    //创建一个 ExampleClass 对象并装饰
    ExampleClass * obj2 = new ExampleClass("Obj2");
    dec.decorat(obj2);

    //使用 Lambda 表达式打印所有装饰的 ExampleClass 对象
    dec.decoratMeCallback([](decorator<ExampleClass>::drte_t& obj) {
        obj->print();
    });

    //删除第 1 个装饰的对象
    dec.remove(0);
    std::cout << "After remove:" << std::endl;

    //再次使用 Lambda 表达式打印所有装饰的 ExampleClass 对象
    dec.decoratMeCallback([](decorator<ExampleClass>::drte_t& obj) {
        obj->print();
    });
    delete obj1;
    delete obj2;
    return 0;
}
上面的例子的执行结果如下:

Name:Obj1
Name:Obj2
After remove:
Name:Obj2

相关文章