场景
考虑一个电脑游戏, 里面的每种生物都有一个名字和两个属性: 攻击力(attack)
和防御力(defense)
:
1 2 3 4 5 6 7
| struct Creature { string name; int attack, defense; ... }
|
在游戏中, 角色可能会拿到一些装备(比如, 一把魔法剑), 或者终结能力的增强. 不管哪种情况, 他的攻击力/防御力都会发生变化. 我们用CreatureModifier
来修改它的属性.
更进一步说, 可能会有多个Modifier
作用到角色上, 这种情况在游戏中并不罕见. 因此, 我们需要按照他们获取装备的次序来将这些Modifier
依次作用在角色上.
首先来看一个实现:
Pointer Chain
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class CreatureModifier { CreatureModifier* next{nullptr}; protected: explicit CreatureModifier(Creature& creature) " creature(creature) {} void add(CreatureModifier* cm){ if( next){ next->add(cm); } else{ next = cm; } }
virtual void handle() { if( next ){ next->handle(); } } };
|
这里, 实际上是用一个指针链表来将各个修改器连接起来.
实现很简单, 其中值得说的是handle()
. 这是一个虚函数, 因此派生类必须自己实现它. 基类的实现看似仅仅是调用下一个的handle()
方法. 但是当我们实现具体的修改器时, 有意思的事情就发生了.
实现一个具体的修改器:
1 2 3 4 5 6 7 8 9 10 11 12
| class DoubleAttackModifier : public CreatureModifier { public: explicit DoubleAttackModifier( Creature& creature) : CreatureModifier(creature) {}
void handle() override { creature.attack *= 2; CreatureModifier::handle(); } }
|
唯一需要保证的是, 每个派生类不要忘记调用它基类的handle()
方法.
在实现另一个修改器: 如果他的攻击力小于等于2, 那么防御力加1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class IncreaseDefenseModifier : public CreatureModifier { public: explicit IncreaseDefenseModifier(Creature& creature) : CreatureModifier(creature) {} void handle() override { if( creature.attack <= 2){ creeature.defense += 1; } CreatureModifier::handle(); } }
|
下面我们使用它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Creature goblin{"Goblin", 1, 1}; CreatureModifier root{goblin}; DoubleAttackModifier r1{goblin}; DoubleAttackModifier r1_2{goblin}; IncreateSefenseModifier r2{goblin};
root.add(&r1); root.add(&r1_2); root.add(&r2);
root.handle();
cout << goblin << endl;
|
现在, 再考虑一个有趣的场景: 假如有一种咒语, 可以让被施法的对象得不到任何装备的加成, 要如何实现?
1 2 3 4 5 6 7 8 9 10
| class NoBonusesModifier : public CreatureModifier { public: explicit NoBonusesModifier(Creature& creature) : CreatureModifier(creature) {} void handle() override { } }
|
Broker Chain
前面的例子太简单人为. 考虑一个更复杂的情况. 我们不希望永久性修改对象的原始属性. 一种实现方法是通过一个中心化的控制组件来实现. 这个组件保存所有的修改器, 用于查询某个特定的生物对象的攻击力和防御力.
这个组件称之为event broker
. 它被连接到每个参与者中, 因此它属于Mediator Pattern
. 同时, 它通过event来响应查询, 又具有Observer Pattern
的特点.
首先, 构造Game
类, 它代表玩的游戏.
1 2 3 4
| struct Game { signal<void(Query&)> queries; };
|
上面代码中我们使用了Boost.Signals2
库.
然后是考虑如何实现查询: 在获取最终结果前, 需要应用所有的修改器, 我们将查询封装到一个单独的对象中(这被称为命令模式)
这里有一点混淆的地方. 有一种称为**命令-查询分离(Command Query Seperation CQS)**的观点主张将命令(Command, 它改变了对象的状态但是不返回值)和查询(Query, 它不改变对象状态, 但是返回值) 分开. 但是GoF4中没有查询的概念, 这里也不加区分.
1 2 3 4 5 6 7 8 9
| class Creature { Game& game; int attack, defense; public: string name; Creature(Game& game, ...) : game{game}, ... {...} };
|
接下来是获取attack
和defense
的值.
1 2 3 4 5 6
| int Creature::get_attack() const { Query q{name, Query::Argument::attack, attack}; game.queries(q); return q.result; }
|
我们创建一个Qquery
对象, 并将其发送给所有订阅Game::queries
的对象. 每个订阅了的组件都能获得机会来修改基线的attck
值.
接下来实现修改器. 首先定义基类, 这时不再需要handle()
方法了
1 2 3 4 5 6 7 8
| class CreatureModifier { Game& game; Creature& creature; public: CreatureModifier(Game& game, Creature& creature) : game(game), creature(creature) {} };
|
然后定义它的具体实现修改的派生类.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class DoubleAttackModifier : public CreatureModifier { connection conn; public: DoubleAttackModifier(Game& game, Creature& creature) : CreatureModifier(game, creature) { conn = game.queries.connect([&](Query& q) { if(q.creature.name==creature.name && q.argument==Query::Artgument::attck ) { q.result *= 2; } }); }
~DoubleAttackModifier() { conn.disconnect(); } };
|
所有的工作都在构造函数和析构函数中做的: 在构造函数中, 试用Game
的引用来捕获Game::queries
信号并连接它, 指定了一个lambda来将生物的攻击力加倍.
我们还要在对象析构的时候断开信号的连接. 这样, 我们临时使用修改器, 当修改器超过它的作用范围时就让它失效. 例如下面的使用代码:
1 2 3 4 5 6 7 8 9 10 11
| Game game; Creature goblin{game, "Strong Goblin", 2, 2}; cout << goblin << endl;
{ DoubleAttackModifier dam{ game, goblin}; cout << goblin << endl; }
cout << goblin << endl;
|
Summary