提纲
背景
我不认识你,就让你来处理
增加一个识别函数,避免无限循环
真正的增量式开发
非对称性处理
上升到设计模式层次
结论
增加一个识别函数,避免无限循环
真正的增量式开发
非对称性处理
上升到设计模式层次
结论
背景
Scott Meyers
在
More Effective C++
的第
31
个条款中,提到了双分派(
Double Dispatch
)的问题,简述如下:
假设你想写一个游戏软件,场景发生在外层空间,涉及宇宙飞船、太空站、小行星等天体,这些天体在空中飞行时会发生碰撞,任何两种不同的天体碰撞的结果都不一样,比如:
如果宇宙飞船和太空站以低速碰撞,宇宙飞船会泊进太空站内。否则宇宙飞船和太空站收到的损害与其碰撞速度成正比。
至于其他的碰撞结果,本文不一一列出,这不是本文的重点。这里的重点是“任何两种不同的天体碰撞的结果都不一样”。
假设你设计的继承体系如下图所示:
class GameObject {…};
class SpaceShip: public GameObject {…};
class SpaceStation: public GameObject {…};
class Asteroid: public GameObject {…};
而处理碰撞的函数可能是这样的:
void processCollision(GameObject& object1, GameObject& object2)
{
…
}
于是问题出来了。如果碰撞的结果只依赖于
object1
的动态类型,我们可以把
processCollision
作为
GameObject
的虚函数,然后调用
object1. processCollision(object2)
。如果碰撞的结果只依赖于
object2
的动态类型,则处理方法类似,只不过现在调用
object2. processCollision(object1)
。但是事实上,碰撞的结果视
object1
和
object2
两个参数,所以无法通过上述的单分派(
Single Dispatch
)来达成目标。这就是所谓的双分派(
Double Dispatch
)问题。
(如果您没有读过
Scott Meyers
的
More Effective C++
,建议您先读一遍,如果您曾经读过但现在已经忘得差不多了,也请您重温一遍,因为在下面的叙述中,我完全利用了原文中的例子,我不会给出所有的示例代码,只给出我做了修改的部分。)
接下来作者给出了很多种解决方案,但没有一种能够解决“当新型对象加入时,需要修改已有代码”的问题。其中,“使用非成员函数的碰撞处理函数”一法,虽然看上去当新型对象加入时,无需修改已有的类的申明与定义,但是仍然需要修改那些非成员的碰撞处理函数(至少必须添加新的碰撞处理函数),不能算是完全不用修改已有代码;何况把碰撞处理函数写成非成员函数,本身已经不符合封装的原则。本文尝试提供一个新的解决方案,当增加新型的对象时,完全不需要修改已有的代码。
首先说明一下,为什么我们要追求“当增加新型的对象时完全不修改已有的代码”这样一个目标。必须承认,如果旧有的系统是你自己设计的,或者你拥有原有系统的源码,那么增加新型的对象时,直接修改原来的代码,也许可读性更好,更易于理解。但是,实际情形中,原来的系统通常都是别人以二进制形式提供的,我们通常无法修改源代码,这时要想加进自己的东西,就不得不在不修改已有代码的基础上,无缝地加入自己的代码。
接下来我们来分析一下这种可能是否存在。当增加一个新的天体类型时,新的天体类型与已有的天体类型碰撞的规则不一样(这意味着碰撞处理函数不一样)。从一个方面看,这些碰撞处理函数牵涉到已有的天体类型,似乎一定要在已有的类中增加与新型天体碰撞的处理。但是,从另一个方面上看,这些新的碰撞处理,只有当新的天体类型存在时,它们才有意义,没有新的天体类型,这些碰撞处理就不会存在,换句话说,他们与新的天体类型是紧密相依的,所以没有理由一定要把这些碰撞处理写在已有的类中。所以,从感觉上看,当增加新类型的对象时,完全不需要修改已有的代码是有可能而且是应该的。
为了简化问题,我们假设碰撞是对称的(
Scott Meyers
也做过这样的假设),也就是说
A
撞击
B
的规则与
B
撞击
A
的规则是一样的。这就为我们在处理碰撞函数时提供了一个契机:当一个对象面对不认识的对象时(这个对象可能就是将来新增加的类型),就调用这个不认识的对象的碰撞处理函数。我把它归纳成:
我不认识你,就让你来处理
我不认识你,就让你来处理,这看起来有点像推卸责任,但在这里却有着非常奇妙的作用。是的,既然碰撞是对称的,我跟你的碰撞处理就等于你跟我的碰撞处理,现在,我不认识你,我不知道我碰撞你之后,会是什么结果,于是我就让你来处理你与我的碰撞,而虚函数的特性恰恰可以保证做到这一点。
下面以
SpaceShip
为例给出示例代码。为了叙述方便,这里使用虚函数
+
基于运行期类型识别的
if-then-else
链,尽管我知道
Meyers
对这种方法并不赞成,但这不是本文的重点,改用其他的方法,一样可以采用本文叙述的方案。此时,
GameObject
中已经申明了一个虚函数
collide
如下:
virtual void collide(GameObject& otherObject) = 0;
SpaceShip
的
collide
函数实现如下:
void SpaceShip::collide(GameObject& otherObject)
{
const type_info& objectType = typeid(otherObject);
if(objectType == typeid(SpaceShip)){
SpaceShip& ss = static_cast<SpaceShip&>(otherObject);
//process a SpaceShip-SpaceShip collision;
}
else if(objectType == typeid(SpaceStation)){
SpaceStation& ss = static_cast<SpaceStation&>(otherObject);
//process a SpaceShip-SpaceStation collision;
}
else if(objectType == typeid(Asteroid)){
Asteroid& ss = static_cast<Asteroid&>(otherObject);
//process a SpaceShip-Asteroid collision;
}
else{
otherObject.collide(*this); //
这里是关键
}
}
注意上面最后一行代码。原文中这里是抛出一个异常,这里改成了调用
otherObject
的碰撞处理函数,虽然只是一行之差,结果却大不相同。
每一个类的写法都是这样,它只处理与它认识的天体类型的碰撞,不认识的交给对方处理(细心的读者可能已经发现,这里有一个隐患,我将在后面说明)。当增加一个新的天体类型时,新类型的写法也是这样,但它必须处理与所有已经存在的天体类型的碰撞,这是不难做到的,新增的类型总是认识已有的类型。
采用这种写法后,总的碰撞处理函数是非常简单的,它看起来是这样: