目录
1.概述
装饰器模式也是我们日常编程用的比较多的一种设计模式,它允许向一个现有的对象添加新的功能,同时又不改变其结构,实际上就是现有的类的一个包装。这种模式可以视为结构性模式,应为它提供了一种用于组合类的机制,,使得这些类可以在不改变其代码的情况下进行扩展。
在装饰器模式中,有一个被装饰的组件(通常是抽象类或接口),它定义了需要实现的方法。然后,有一个装饰器类,它实现了同样的接口,并且包装了被装饰的组件。装饰器类可以在被装饰的组件方式调用之前或之后添加额外的功能。
2.结构
装饰器模式的UML类图如下:
主要角色:
1)抽象组件(Component):定义了原始对象和装饰器对象的公共接口或抽象类,可以是具体组件类的父类或接口。
2)具体组件(Concrete Component):是被装饰的原始对象,它定义了需要添加新功能的对象。
3)抽象装饰器(Decorator):这是一个抽象基类或接口,它也实现了组件接口,并包含一个成员变量用来引用具体组件对象。装饰器可以添加额外的功能,通常通过在装饰器中委托给具体组件实现。
4)具体装饰器(Concrete Decorator):实现了抽象装饰器的接口,负责向抽象组件添加新的功能。具体装饰器通常会在调用原始对象的方法之前或之后执行自己的操作。
装饰器模式通过嵌套包装多个装饰器对象,可以实现多层次的功能增强。每个具体装饰器类都可以选择性地增加新的功能,同时保持对象接口的一致性。
在C++中,装饰器模式通常涉及到类的继承和组合,以及对虚函数的使用,以确保适当的方法可以在组合中被调用。这使得C++的装饰器模式非常灵活,可用于构建可维护和可扩展的对象结构。
3.实现
3.1.示例1:简单实现
Decorator.h
cpp
#include <iostream>
#include <memory>
// 抽象组件
class Component {
public:
virtual void doWork() = 0;
};
// 具体组件
class ConcreteComponent : public Component {
public:
void doWork() override {
// 执行具体操作
std::cout << "ConcreteComponent doWork..." << std::endl;
}
};
// 抽象装饰器
class Decorator : public Component {
protected:
Component* m_pComponent;
public:
Decorator(Component* pComponent) : m_pComponent(pComponent) {}
void doWork() override {
m_pComponent->doWork();
}
};
// 具体装饰器
class ConcreteDecorator1 : public Decorator {
public:
ConcreteDecorator1(Component* pComponent) : Decorator(pComponent) {}
void doWork() override {
std::cout << "ConcreteDecorator1 doWork before..." << std::endl;
__super::doWork();
std::cout << "ConcreteDecorator1 doWork after..." << std::endl;
}
};
// 具体装饰器
class ConcreteDecorator2 : public Decorator {
public:
ConcreteDecorator2(Component* pComponent) : Decorator(pComponent) {}
void doWork() override {
std::cout << "ConcreteDecorator2 doWork before..." << std::endl;
__super::doWork();
std::cout << "ConcreteDecorator2 doWork after..." << std::endl;
}
};
main.cpp
cpp
int main()
{
std::unique_ptr<Component> pConcreteComponent(new ConcreteComponent());
std::unique_ptr<Component> pConcreteDecorator1(new
ConcreteDecorator1(pConcreteComponent.get()));
std::unique_ptr<Component> pConcreteDecorator2(new
ConcreteDecorator2(pConcreteDecorator1.get()));
pConcreteComponent->doWork();
std::cout << std::endl;
pConcreteDecorator1->doWork();
std::cout << std::endl;
pConcreteDecorator2->doWork();
}
输出:
cpp
ConcreteComponent doWork...
ConcreteDecorator1 doWork before...
ConcreteComponent doWork...
ConcreteDecorator1 doWork after...
ConcreteDecorator2 doWork before...
ConcreteDecorator1 doWork before...
ConcreteComponent doWork...
ConcreteDecorator1 doWork after...
ConcreteDecorator2 doWork after...
pConcreteComponent和pConcreteDecorator1还比较简单,pConcreteDecorator2装饰器就比较复杂了,就好像是在pConcreteDecorator1的基础上套了一个壳,从这点也可以看出装饰器模式结构可以非常的灵活。
3.2.示例2:函数装饰器
虽然装饰器模式通常用于类,但是同样可以应用于函数。例如,假设代码中有一个特定的函数给你带来了麻烦:你希望记录该函数被调用的所有时间,并在Excel中分析统计信息。当然,这可以通过在调用函数前后放置一些代码来实现,即
cpp
cout<< "Entering function XYz\n":
// do the work
cout <<"Exiting function XYZ\n";
这很好,但从关注点分离的角度来看并不好:我们确实希望将日志功能存储在某个地方,以便复用并在必要时强化它。
有不同的方法可以实现这一点。一种方法是简单地将整个工作单元作为一个函数提供给某些日志组件:
cpp
struct Logger{
function<void()> func;
string name;
public:
Logger(const function<void()>& func, const string& name):func{func},name{name}{}
void operator()()const {
cout<< "Entering "<< name << "\n";
func();
cout<< "Exiting "<< name << "\n";
}
};
使用这种方法,我们可以编写如下代码来使用它:
cpp
Logger([](){ cout << "Hello\n";},"HelloFunction")();// Entering HelloFunction
// Hello
// Exiting HelloFunction
始终有一种选择,可以将函数作为模板参数而不是std::function传人。这导致与前面的结果略有不同:
cpp
template <typename Func>
struct Logger2{
Func func;
string name;
public:
Logger2(const Func& func,const string& name)
:func{func},name{name}{}
void operator()()const{
cout << "Entering "<< name << endl;
func();
cout << "Exiting "<< name << endl;
}
};
此实现的用途完全不变。我们可以创建一个工具函数来实际创建这样的记录器:
cpp
template <typename Func>
auto make_logger2(Func func,const string& name)
{
return logger2<Func>{ func, name };
}
然后像下面这样使用它:
cpp
auto call = make_logger2([](){cout <<"Hello!" <<endl; },"HelloFunction");
call();// output same as before
你可能会问 "这有什么意义?" 嗯......我们现在有能力在需要装饰某个函数时创建一个装饰器(其中包含装饰的函数)并调用它。
现在,我们遇到了一个新的挑战:如果要记录调用函数add()的日志,该怎么办呢?此处将add()函数定义如下:
cpp
double add(double a, double b)
{
cout << a << "+"<< b<<"="<<(a + b)<< endl;
return a + b;
}
需要 add()函数的返回值吗?如果需要的话,它将从 1ogger中返回(因为1ogger装饰了add()函数)。这并不容易!但也绝不是不可能。我们再次改进一下记录器:
cpp
template <typename R, typename... Args>
struct Logger3<R(Args...)>
{
Logger3(function<R(Args...)>func, const string& name)
: func{func},name{name}{}
R operator()(Args ...args)
{
cout << "Entering"<< name << endl;
R result = func(args...);
cout <<"Exiting "<< name << endl;
return result;
}
function<R(Args ...)> func;
string name;
};
在上述代码中,模板参数R表示返回值的类型,Args表示丽数的参数类型。与之前相同的是,装饰器在必要时调用函数;唯一的区别是opeIator() 返回了一个R,因此采用方法不会丢失返回值。
我们可以构造另一个工具函数 make_:
cpp
template <typename R, typename... Args>
auto make_1ogger3(R(*func)(Args...), const string& name)
{
return Logger3<R(Args...)>(
function<R(Args...)>(func)
name);
}
请注意,这里没有使用std::function,而是将第一个参数定义为普通函数指针,我们现在可以使用此函数实例化日志调用并使用它:
cpp
auto logged add = make logger3(add, "Add");
auto result=logged add(2,3);
当然,make_logger3可以被依赖注入取代。这种方法的好处是能够:
1)通过提供空对象而不是实际的记录器,动态地打开和关闭日志记录 。
2)禁用记录器正在记录的代码的实际调用(同样,通过替换其他记录器)。
总之,对于开发人员而言,这是一个有用的工具函数。
4.总结
使用装饰器模式的好处:
1)可以在运行时动态的添加或删除功能。
2)可以递归地组合多个装饰器,从而创建复杂的对象。
3)可以使用不同的装饰器组合来创建不同的行为。
4)可以在不修改原有代码的情况下增加新功能。
总体来说,装饰器模式是一种强大且灵活的设计模式,它可以用于创建可扩展、可定制和可组合的对象。