我们将从这两个经典模式的定义开始本章的学习。从理论上看,这些模式以及它们之间的区别都很明显。然而,C++ 的特性引入了一些介于两者之间的设计解决方案,使得界限变得模糊。尽管随着复杂性的增加,这种清晰性可能会降低,但理解这些简单情况的清晰定义仍然很有帮助。那么,让我们先从清晰的概念开始。
装饰器模式也是一种结构型模式;它允许向一个对象添加行为。经典的装饰器模式扩展了类所执行的现有操作的行为。用新行为“装饰”该类,并创建一个新类型(被装饰类型)的对象。装饰器实现了原始类的接口,并将其自身接口的请求转发给该原始类,但在这些请求被转发之前和之后,装饰器还会执行其他操作 —— 这些操作就是“装饰”。此类装饰器有时也称为“类包装器”。
我们将从一个尽可能贴近经典定义的 C++ 装饰器模式示例开始。例子中,设计一个环境为中世纪的奇幻游戏(真实生活的基础上加入了龙、精灵等元素)。当然,没有战斗的中世纪还叫什么中世纪?在游戏中,玩家可以选择适合自己阵营的单位,并在需要时进行战斗。以下是基本的 Unit 类 —— 至少是与战斗相关的部分:
// Example 01
class Unit {
public:
Unit(double strength, double armor) :
strength_(strength), armor_(armor) {}
virtual bool hit(Unit& target) {
return attack() > target.defense();
}
virtual double attack() = 0;
virtual double defense() = 0;
protected:
double strength_;
double armor_;
};
该单位拥有力量(决定其攻击力)和护甲(提供防御力)。攻击力和防御力的实际数值由派生类 —— 即具体的单位 —— 但战斗机制本身就在这里:如果攻击力强于防御力,该单位就能成功击中目标(当然,这在游戏设计上是非常简化的,但我们希望示例尽可能简洁)。
那么,游戏中的实际单位有哪些呢?人类军队的支柱是英勇的骑士。这个单位拥有坚固的护甲和锋利的宝剑,使其在攻击和防御上都有加成:
// Example 01
class Knight : public Unit {
public:
using Unit::Unit;
double attack() { return strength_ + sword_bonus_; }
double defense() { return armor_ + plate_bonus_; }
protected:
static constexpr double sword_bonus_ = 2;
static constexpr double plate_bonus_ = 3;
};
与骑士作战的是粗野的食人魔。食人魔挥舞着简单的木棒,穿着破旧的皮革,这些都不是什么优秀的战争装备,因此在战斗中会受到一些惩罚:
// Example 01
class Ogre : public Unit {
public:
using Unit::Unit;
double attack() { return strength_ + club_penalty_; }
double defense() { return armor_ + leather_penalty_; }
protected:
static constexpr double club_penalty_ = -1;
static constexpr double leather_penalty_ = -1;
};
然而,食人魔天生就异常强壮:
Knight k(10, 5);
Ogre o(12, 2);
k.hit(o); // Yes!
这里,骑士凭借其攻击加成和敌方薄弱的护甲,将成功击中食人魔。
但游戏远未结束。随着单位们不断战斗,幸存下来的单位会获得经验,并最终成为老兵。一个老兵单位本质上仍是同类型的单位,但它获得了攻击和防御加成,以反映其战斗经验。我们不想改变类的接口,但希望修改 attack() 和 defense() 函数的行为。这正是装饰器模式的用武之地,以下是 VeteranUnit 装饰器的经典实现:
// Example 01
class VeteranUnit : public Unit {
public:
VeteranUnit(Unit& unit,
double strength_bonus,
double armor_bonus) :
Unit(strength_bonus, armor_bonus), unit_(unit) {}
double attack() { return unit_.attack() + strength_; }
double defense() { return unit_.defense() + armor_; }
private:
Unit& unit_;
};
这个类直接继承自 Unit 类,在类型层次结构中,与 Knight 或 Ogre 等具体单位类处于同一层级。我们仍然保留了装饰并成为老兵的原始单位 —— VeteranUnit 装饰器包含对该原始单位的引用。其使用方式是:装饰一个单位,然后一直使用这个装饰后的单位,但并不会删除原始单位:
// Example 01
Knight k(10, 5);
Ogre o(12, 2);
VeteranUnit vk(k, 7, 2);
VeteranUnit vo(o, 1, 9);
vk.hit(vo); // Another hit!
原来的两个对手都达到了第一个老兵等级,胜利再次属于骑士。但经验是最好的老师,食人魔又获得了一个等级,并且获得了带有巨大防御加成的附魔符文护甲:
VeteranUnit vvo(vo, 1, 9);
vk.hit(vvo); // Miss!
此设计中,可以对一个已经被装饰过的对象再次进行装饰!随着单位等级的提升,加成效果会叠加。这一次,这位经验丰富的战士的防御力对骑士来说过于强大了。
这是教科书式的经典装饰器模式,在 C++ 中可以工作,但存在一些局限性。第一个局限性相当明显:尽管在装饰后使用装饰的单位,但原始单位必须一直存在,并且这些对象的生命周期必须仔细管理。对于这类实际问题,存在一些实用的解决方案。但本书的重点在于将设计模式与泛型编程相结合,以及这种结合所带来的新设计可能性,所以我们的探索方向将有所不同。
第二个问题更具有 C++ 的特性,最好通过一个例子来说明。游戏设计者为骑士单位添加了一项特殊能力 —— 可以向前冲锋攻击敌人,从而获得一个短期的攻击加成。这个加成仅对下一次攻击有效,但在激烈的战斗中,可能扭转战局:
// Example 02
class Knight : public Unit {
public:
Knight(double strength, double armor) :
Unit(strength, armor), charge_bonus_(0) {}
double attack() {
double res = strength_ + sword_bonus_ + charge_bonus_;
charge_bonus_ = 0;
return res;
}
double defense() { return armor_ + plate_bonus_; }
void charge() { charge_bonus_ = 1; }
protected:
double charge_bonus_;
static constexpr double sword_bonus_ = 2;
static constexpr double plate_bonus_ = 3;
};
冲锋加成通过调用 charge() 成员函数来激活,持续一次攻击后即重置。当玩家发动冲锋时,游戏会执行类似如下的代码:
Knight k(10, 5);
Ogre o(12, 2);
k.charge();
k.hit(o);
当然,我们会期望老兵骑士也能向前冲锋。但这里遇到了一个问题 —— 代码无法编译:
VeteranUnit vk(k, 7, 2);
vk.charge(); // Does not compile!
问题的根源在于,charge() 是 Knight 类接口的一部分,而 VeteranUnit 装饰器是从 Unit 类派生的。我们可以将 charge() 函数移到基类 Unit 中,但这是一种糟糕的设计 —— Ogre 也继承自 Unit,而食人魔不能冲锋,因此不应拥有这样的接口(这违反了公有继承的“is-a”原则)。
这个问题源于我们实现装饰器对象的方式:Knight 和 VeteranUnit 都从同一个基类 Unit 派生,但彼此之间互不了解。虽然存在一些丑陋的变通方法,但这确实是 C++ 的一个根本性限制 —— 不擅长处理跨分支的类型转换(即转换到同一继承树中另一分支的类型)。
然而,编程语言在一方面设限,另一方面也提供了更好的工具来解决此类问题。接下来,将介绍这些更强大的工具。
用 C++ 实现经典装饰器时遇到了两个问题 —— 首先,装饰的对象并不拥有原始对象,两者都必须保留(如果之后需要移除装饰,这可能不是问题而是一种特性,这也是装饰器模式如此实现的原因)。另一个问题是,一个装饰的骑士实际上根本不是一个骑士,而只是一个 Unit。如果装饰器本身是从装饰类派生,就可以解决第二个问题。所以 VeteranUnit 类没有固定的基类 —— 基类应该是装饰类。这种描述完全符合奇异递归模板模式(CRTP)。要应用 CRTP,需要将装饰器变成一个模板,并从模板参数继承:
// Example 03
template <typename U>
class VeteranUnit : public U {
public:
VeteranUnit(U&& unit,
double strength_bonus,
double armor_bonus) :
U(unit), strength_bonus_(strength_bonus),
armor_bonus_(armor_bonus) {}
double attack() { return U::attack() + strength_bonus_; }
double defense() { return U::defense() + armor_bonus_; }
private:
double strength_bonus_;
double armor_bonus_;
};
现在,要将一个单位提升为老兵,必须将其转换为装饰版本的单位类:
// Example 03
Knight k(10, 5);
Ogre o(12, 2);
k.hit(o); // 命中!
VeteranUnit<Knight> vk(std::move(k), 7, 2);
VeteranUnit<Ogre> vo(std::move(o), 1, 9);
vk.hit(vo); // 命中!
VeteranUnit<VeteranUnit<Ogre>> vvo(std::move(vo), 1, 9);
vk.hit(vvo); // 未命中...
vk.charge(); // 现在可以编译了,vk 也是一个 Knight
vk.hit(vvo); // 命中!并附带冲锋加成!
这与上一节末尾的场景相同,但现在使用了模板装饰器。首先,VeteranUnit 是一个从具体单位(如 Knight 或 Ogre)派生的类,可以访问基类的接口:例如,一个老兵骑士 VeteranUnit<Knight> 也是一个 Knight,并从 Knight 继承了 charge() 成员函数。其次,装饰的单位明确地拥有了原始单位 —— 要创建一个老兵单位,必须将原始单位移动到其中(老兵单位的基类通过移动构造函数从原始单位构造)。原始对象将处于未指定的“已移动”状态,对该对象唯一安全的操作就是调用其析构函数。至少对于这些简单实现的单位类来说,移动操作实际上只是复制,因此原始对象仍然可用,但不应依赖于此 —— 对“已移动”状态做出假设是潜在的 bug。
我们对 VeteranUnit 构造函数的声明强制,并要求了这种所有权转移。如果尝试在不从原始单位移动的情况下构造一个老兵单位,代码将无法编译:
VeteranUnit<Knight> vk(k, 7, 2); // 无法编译
通过只提供一个接受右值引用(即 Unit&&)的构造函数,要求调用者同意所有权的转移。
为了演示目的,我们都是在栈上创建所有单位对象作为局部变量。但在非简单的程序中,这并不可行 —— 需要这些对象在创建它们的函数执行完毕后仍然长期存在。可以将装饰器对象与内存所有权机制集成起来,确保在创建了装饰版本后,删除原始的“已移动”单位对象。
假设在整个程序中,所有权由 unique_ptr 来管理(在时刻每个对象都有一个明确的所有者)。以下是实现方式。首先,可以为需要使用的指针声明别名:
using Unit_ptr = std::unique_ptr<Unit>;
using Knight_ptr = std::unique_ptr<Knight>;
虽然单位都可以由 Unit_ptr 指针拥有,但无法通过它调用特定于单位的成员函数(如 charge()),因此可能还需要指向具体类的指针。接下来,需要在这类指针之间移动对象。从指向派生类的指针,移动到指向基类的指针就很容易:
Knight_ptr k(new Knight(10, 5));
Unit_ptr u(std::move(k)); // 现在k为空
反向移动则稍微困难一些;std::move 无法隐式工作,和不能在没有显式转换的情况下从 Unit* 转换到 Knight* 一样。所以需要一个移动转换:
// Example 04
template <typename To, typename From>
std::unique_ptr<To> move_cast(std::unique_ptr<From>& p) {
return std::unique_ptr<To>(static_cast<To*>(p.release()));
}
这里,使用 static_cast 转换到派生类,如果假设的关系(即基类对象确实是预期的派生对象)成立,则转换有效,否则结果是未定义的。可以使用 dynamic_cast 在运行时测试这个假设。以下是一个进行测试的版本,但仅在启用断言时进行(我们可以用抛出异常代替断言):
// Example 04
template <typename To, typename From>
std::unique_ptr<To> move_cast(std::unique_ptr<From>& p) {
#ifndef NDEBUG
auto p1 =
std::unique_ptr<To>(dynamic_cast<To*>(p.release()));
assert(p1);
return p1;
#else
return std::unique_ptr<To>(static_cast<To*>(p.release()));
#endif
}
如果所有对象都将由 unique_ptr 的实例来拥有,VeteranUnit 装饰器必须在其构造函数中接受一个指针,并将对象从该指针中移动出来:
// Example 04
template <typename U> class VeteranUnit : public U {
public:
template <typename P>
VeteranUnit(P&& p,
double strength_bonus,
double armor_bonus) :
U(std::move(*move_cast<U>(p))),
strength_bonus_(strength_bonus),
armor_bonus_(armor_bonus) {}
double attack() { return U::attack() + strength_bonus_; }
double defense() { return U::defense() + armor_bonus_; }
private:
double strength_bonus_;
double armor_bonus_;
};
这里棘手的部分在于 VeteranUnit<U> 的基类 U 的初始化 —— 必须将对象从指向基类的 unique_ptr 移动到派生类的移动构造函数中(无法简单地将对象从一个 unique_ptr 直接移动到另一个,需要将其包装进派生类中),还必须确保不泄露内存。原始的 unique_ptr 释放,因此其析构函数将不再起作用,但 move_cast 返回了一个新的 unique_ptr,现在由它来拥有同一个对象。这个新 unique_ptr 是一个临时变量,在新对象初始化结束时将删除,但在那之前,我们会用它所拥有的对象来构造一个新的派生对象,即 VeteranUnit(就当前情况而言,对象本身的移动初始化相比复制并没有节省时间,但如果将来更重量级的单位对象提供了优化的移动构造函数,这样做就是一种良好的实践)。
以下是这个新的装饰器在使用 unique_ptr 管理资源(在我们的例子中是单位)的程序中的使用方式:
// Example 04
Knight_ptr k(new Knight(10, 5));
// 使用 Knight_ptr 以便可以调用 charge() 方法
Unit_ptr o(new Ogre(12, 2));
// 如果需要,这里也可以使用 Ogre_ptr
Knight_ptr vk(new VeteranUnit<Knight>(k, 7, 2));
Unit_ptr vo(new VeteranUnit<Ogre>(o, 1, 9));
Unit_ptr vvo(new VeteranUnit<VeteranUnit<Ogre>>(vo, 1, 9));
vk->hit(*vvo); // 未命中
vk->charge(); // 可以调用,因为 vk 是 Knight_ptr 类型
vk->hit(*vvo); // 命中
我们没有重新定义 hit() 函数 —— 它仍然通过引用来接受一个单位对象。这没问题,因为该函数并不拥有该对象的所有权,只是对其进行操作。没有必要向它传递一个拥有所有权的指针 —— 那样会暗示所有权的转移。
严格来说,这个例子与上一个例子之间几乎没有区别 —— 无论哪种方式,都不应再访问已移动的对象。但从实际角度看,存在显著差异 —— 已移动的指针不再拥有该对象。其值为 nullptr,因此在单位提升后,试图操作原始单位的行为都会很快暴露出来(程序将解引用空指针并崩溃)。
这里,可以对一个已经装饰类再次进行装饰,因为装饰器的效果是叠加的。同样,也可以将两个不同的装饰器应用到同一个类上,每个装饰器为类添加一种特定的新行为。在游戏引擎中,可以打印每次攻击的结果,无论是否命中。但如果结果不符合预期,就不知道原因。为了调试,打印攻击和防御的数值可能会很有用。我们并不希望始终对所有单位都这样做,但对于我们感兴趣的代码部分,可以使用一个调试装饰器,为单位添加打印计算中间结果的新行为。
DebugDecorator 使用了与之前装饰器相同的设计思路 —— 它是一个类模板,生成一个装饰对象派生的类。它的 attack() 和 defense() 虚函数将调用转发给基类,并打印结果:
// Example 05
template <typename U> class DebugDecorator : public U {
public:
using U::U;
template <typename P> DebugDecorator(P&& p) :
U(std::move(*move_cast<U>(p))) {}
double attack() {
double res = U::attack();
cout << "Attack: " << res << endl;
return res;
}
double defense() {
double res = U::defense();
cout << "Defense: " << res << endl;
return res;
}
};
这个例子中,省略了动态内存分配,而是依靠移动对象本身来实现所有权的转移。完全可以让装饰器既支持堆栈式使用,也支持 unique_ptr:
// Example 06
template <typename U> class VeteranUnit : public U {
...
};
template <typename U> class DebugDecorator : public U {
using U::U;
public:
template <typename P>
DebugDecorator(std::unique_ptr<P>& p) :
U(std::move(*move_cast<U>(p))) {}
double attack() override {
double res = U::attack();
cout << "Attack: " << res << endl;
return res;
}
double defense() override {
double res = U::defense();
cout << "Defense: " << res << endl;
return res;
}
using ptr = std::unique_ptr<DebugDecorator>;
template <typename... Args>
static ptr construct(Args&&... args) {
return ptr{new DebugDecorator(std::forward<Args>(args)...)};
}
};
实现装饰器时,应小心避免以意想不到的方式改变基类的行为。例如,考虑 DebugDecorator 的以下可能实现:
template <typename U> class DebugDecorator : public U {
double attack() {
cout << "Attack: " << U::attack() << endl;
return U::attack();
}
};
这里有一个微妙的错误 —— 装饰对象除了预期的新行为(即打印输出)之外,还隐藏了对原始行为的修改 —— 在基类上调用了两次 attack()。这不仅可能导致打印的值不正确(如果两次调用 attack() 返回不同的值),而且像骑士的冲锋这样的一次性攻击加成也会取消。
DebugDecorator 为所装饰的每个成员函数添加了非常相似的行为。C++ 拥有一套丰富的工具,专门用于提高代码重用性和减少重复。让我们看看是否能做得更好一些,设计出一个更具可重用性、更通用的装饰器。
有些装饰器对其所修改的类非常特定,其行为目标非常明确。而另一些装饰器则至少在原则上非常通用。一个记录函数调用并打印返回值的调试装饰器,如果能够正确实现,理论上可以用于函数。
在 C++14 或更高版本中,使用可变参数模板、参数包和完美转发,这样的实现相当直接:
// Example 07
template <typename Callable> class DebugDecorator {
public:
template <typename F>
DebugDecorator(F&& f, const char* s) :
c_(std::forward<F>(f)), s_(s) {}
template <typename ... Args>
auto operator()(Args&& ... args) const {
cout << "Invoking " << s_ << endl;
auto res = c_(std::forward<Args>(args) ...);
cout << "Result: " << res << endl;
return res;
}
private:
Callable c_;
const std::string s_;
};
这个装饰器可以包裹可调用对象或函数(能用 () 语法调用的东西),无论其参数数量多少。会打印自定义字符串以及调用的结果,但手动写出可调用对象的类型就很棘手 —— 更好的方式是让编译器通过模板参数推导来完成:
// Example 07
template <typename Callable>
auto decorate_debug(Callable&& c, const char* s) {
return DebugDecorator<Callable>(
std::forward<Callable>(c), s);
}
这个模板函数会推导出 Callable 的类型,并用调试包装器对其进行装饰。现在可以将其应用于函数或对象。以下是一个装饰的函数示例:
// Example 07
int g(int i, int j) { return i - j; } // 某个函数
auto g1 = decorate_debug(g, "g()"); // 装饰过的函数
g1(5, 2); // 打印 "Invoking g()" 和 "Result: 3"
也可以装饰一个可调用对象:
// Example 07
struct S {
double operator()() const {
return double(rand() + 1)/double(rand() + 1);
}
};
S s; // 可调用对象
auto s1 =
decorate_debug(s, "rand/rand"); // 装饰可调用对象
s1(); s1(); // 输出结果,两次
装饰器并不拥有可调用对象的所有权(如果需要,可以将其编写为拥有所有权的形式)。
甚至可以装饰一个 Lambda 表达式,它本质上只是一个隐式类型的可调用对象。本例中的 Lambda 定义了一个带有两个整数参数的可调用对象:
// Example 07
auto f2 = decorate_debug(
[](int i, int j) { return i + j; }, "i+j");
f2(5, 3); // 输出 "Invoking i+j" 和 "Result: 8"
示例中,我们决定在装饰器类的构造函数和辅助函数中,都使用完美转发来传递可调用对象。通常情况下,可调用对象是按值传递的,并且假设复制成本很低。在所有情况下,重要的是装饰器必须在其数据成员中存储可调用对象的一个副本。如果改为通过引用捕获,就会存在一个错误等待发生:
template <typename Callable> class DebugDecorator {
public:
DebugDecorator(const Callable& c, const char* s) :
c_(c), s_(s) {}
...
private:
const Callable& c_;
const std::string s_;
};
装饰一个函数可能工作正常,但装饰一个 Lambda 表达式会失败(尽管这种失败可能不会立即显现出来)。const Callable& c_ 成员将绑定到一个临时的 Lambda 对象上:
auto f2 = decorate_debug(
[](int i, int j) { return i + j; }, "i+j");
这个临时对象的生命周期在该语句末尾的分号处结束,之后对 f2 的使用都将访问一个悬空引用(悬垂指针)(地址 sanitizer 工具可以帮助检测此类错误)。
装饰器存在一些局限性。首先,当尝试装饰一个不返回值的函数时,就无法正常工作,例如以下这个 Lambda 表达式,会增加其参数的值但不返回内容:
auto incr = decorate_debug([](int& x) { ++x; }, "++x");
int i;
incr(i); // 无法编译
问题出在 DebugDecorator 内部 auto res = ... 这一行生成的 void res 表达式上。这是合理的,无法声明类型为 void 的变量。
这个问题可以通过 C++17 中的 if constexpr 来解决:
// Example 08
template <typename Callable> class DebugDecorator {
public:
...
template <typename... Args>
auto operator()(Args&&... args) const {
cout << "Invoking " << s_ << endl;
using r_t = decltype(c_(std::forward<Args>(args)...));
if constexpr (!std::is_same_v<res_t, void>) {
auto res = c_(std::forward<Args>(args)...);
cout << "Result: " << res << endl;
return res;
} else {
c_(std::forward<Args>(args)...);
}
}
private:
Callable c_;
const std::string s_;
};
C++17 之前,最常见的替代 if constexpr 的方法是使用函数重载(第一个参数根据 if constexpr 对应的分支为 std::true_type 或 std::false_type):
// Example 08a
template <typename Callable> class DebugDecorator {
public:
...
template <typename... Args>
auto operator()(Args&&... args) const {
cout << "Invoking " << s_ << endl;
using r_t = decltype(c_(std::forward<Args>(args)...));
return this->call_impl(std::is_same<res_t, void>{},
std::forward<Args>(args)...);
}
private:
Callable c_;
const std::string s_;
template <typename... Args>
auto call_impl(std::false_type, Args&&... args) const {
auto res = c_(std::forward<Args>(args)...);
cout << "Result: " << res << endl;
return res;
}
template <typename... Args>
void call_impl(std::true_type, Args&&... args) const {
c_(std::forward<Args>(args)...);
}
};
第二个局限性在于,装饰器的 auto 返回类型推导并非完全精确 —— 如果一个函数返回 double&,那么装饰的函数将只返回 double。最后,可能会包装成员函数的调用,但需要稍微不同的语法。
现在,C++ 中的模板机制非常强大,我们有办法让这个通用装饰器变得更加通用,但这些方法也会使其变得更加复杂。像这样的代码更适合放在库中(例如标准库),但在大多数实际应用中,为一个调试装饰器投入如此大的精力并不值得。另一个局限性是,装饰器越通用,能做的事情就越少。我们能采取的、对调用函数或成员函数都有意义的操作非常有限(甚至要在装饰器中生成一个好的调试消息,可能都需要使用编译器扩展,参见示例09)。可以添加一些调试打印输出,并打印结果,只要该结果定义了流输出操作符。还可以锁定互斥锁,以在多线程程序中保护非线程安全的函数调用。也许还有少数其他通用操作。但总的来说,不要为了追求代码的通用性而陷入为通用而通用的陷阱。
无论拥有的是相对通用,还是非常具体的装饰器,经常需要向一个对象添加多种行为。现在,让我们更系统地回顾一下应用多个装饰器的问题。
我们希望在此处实现的装饰器特性有一个专门的名称:可组合性。如果多种行为可以独立地应用到同一个对象上,这些行为就可组合:例子中,如果有两个装饰器 A 和 B,那么 A(B(object)) 应该同时具备两种行为。与可组合性相对的是显式创建组合行为:如果没有可组合性,为了同时拥有两种行为,就需要编写一个新的装饰器 AB。由于当装饰器数量稍多时,为每一种组合都编写新代码是不现实的,所以可组合性是一个非常重要的特性。
幸运的是,使用之前的装饰器方法,实现可组合性并不困难。之前在游戏设计中使用的 CRTP 装饰器天然就具备可组合性:
template <typename U> class VeteranUnit : public U { ... };
template <typename U> class DebugDecorator : public U { ... };
Unit_ptr o(new DebugDecorator<Ogre>(12, 2));
Unit_ptr vo(new DebugDecorator<VeteranUnit<Ogre>>(o, 1, 9));
每个装饰器都继承自它所装饰的对象,除了新增的行为外,保留了原始对象的接口。装饰器的顺序很重要,新行为是在装饰调用之前或之后添加的。DebugDecorator 作用于它所装饰的对象并为其提供调试功能,因此 VeteranUnit<DebugDecorator<Ogre>> 这样的对象将对对象的基础部分(Ogre)进行调试,这同样有用。
(某种程度上)通用装饰器也可以进行组合,已经有了一个可以与许多不同可调用对象一起工作的调试装饰器,并且提到了可能需要用互斥锁来保护这些调用的需求。现在,可以以类似的方式(并具有类似的局限性)实现这样一个锁定装饰器,类似于多态调试装饰器:
// Example 10
template <typename Callable> class LockDecorator {
public:
template <typename F>
LockDecorator(F&& f, std::mutex& m) :
c_(std::forward<F>(f)), m_(m) {}
template <typename ... Args>
auto operator()(Args&& ... args) const {
std::lock_guard<std::mutex> l(m_);
return c_(std::forward<Args>(args) ...);
}
private:
Callable c_;
std::mutex& m_;
};
template <typename Callable>
auto decorate_lock(Callable&& c, std::mutex& m) {
return LockDecorator<Callable>(std::forward<Callable>(c), m);
}
再次,将使用 decorate_lock() 辅助函数,将繁琐的推导可调用对象正确类型的工作交给编译器。现在,可以使用互斥锁来保护一个非线程安全的函数调用:
std::mutex m;
auto safe_f = decorate_lock([](int x) {
return unsafe_f(x); }, m
)
如果想用互斥锁保护一个函数调用,并在调用时输出调试信息,无需编写一个新的“带锁的调试装饰器”,而是可以按顺序应用这两个装饰器:
auto safe_f = decorate_debug(
decorate_lock(
[](int x) { return unsafe_f(x); },
m
),
"f(x)");
这个例子展示了可组合性的好处 —— 不必为每一种行为组合都编写特殊的装饰器(试想一下,如果这些装饰器不可组合,而你有五种不同的基础装饰器,这时需要编写多少个组合装饰器!)。
这种可组合性在装饰器中很容易实现,保留了原始对象的接口,至少是我们关心的部分 —— 行为发生了改变,但接口保持不变。当一个装饰器用作另一个装饰器的原始对象时,保留的接口再次保留,如此往复。
接口的保留是装饰器模式的一个基本特征,但这也是其局限性的表现。
锁定装饰器并不像乍看之下那么有用(所以不要一心想让代码线程安全,就到处给调用加上锁)。无论实现得多好,也并非所有接口都能变得线程安全。这时,就必须在修改行为的同时改变接口。这正是适配器模式的任务。