场景

考虑一个电脑游戏, 里面的每种生物都有一个名字和两个属性: 攻击力(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;
// name: Goblin attack: 4 defense: 1

现在, 再考虑一个有趣的场景: 假如有一种咒语, 可以让被施法的对象得不到任何装备的加成, 要如何实现?

1
2
3
4
5
6
7
8
9
10
class NoBonusesModifier : public CreatureModifier
{
public:
explicit NoBonusesModifier(Creature& creature)
: CreatureModifier(creature) {}

void handle() override {
// do nothing
}
}

Broker Chain

前面的例子太简单人为. 考虑一个更复杂的情况. 我们不希望永久性修改对象的原始属性. 一种实现方法是通过一个中心化的控制组件来实现. 这个组件保存所有的修改器, 用于查询某个特定的生物对象的攻击力和防御力.

这个组件称之为event broker. 它被连接到每个参与者中, 因此它属于Mediator Pattern. 同时, 它通过event来响应查询, 又具有Observer Pattern的特点.

首先, 构造Game类, 它代表玩的游戏.

1
2
3
4
struct Game // mediator
{
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}, ... {...}
// 其他成员定义...
};

接下来是获取attackdefense的值.

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; // 此时输出的攻击力为2.

// 定义一个有范围的Modifier
{
DoubleAttackModifier dam{ game, goblin};
cout << goblin << endl; // 此时的输出中, 攻击力为4
}
// 在超出dam的作用域后, goblin的攻击力又退回到2了:
cout << goblin << endl;

Summary