如果你研究过visitor pattern,很可能会涉及到一个Double Dispatch的概念,这里我们就叫它双重分发吧,双重分发是一种思想,而访问者模式则是一种具体实现。本文会尽量以一种通俗的语言来描述这些概念(本文以C++为例)。首先我们梳理一下比较熟悉的两个基本概念:静态类型(static type)与动态类型(dynamic type)。
静态类型是在编译期需要确定并进行检查的,检查失败则编译通不过。
动态类型则是在运行时确定的,在C++语言中,用一个基类的指针或引用,指向一个派生类的对象,派生类中重写(override)了基类的某个虚函数,则在运行时,使用这个指针或引用变量调用虚函数时,会调用到派生类中的实现函数,虽然这个指针或引用是基类类型的,在C++中也称为多态,这是C++在语言层面上进行支持的。英文中称这种根据对象的运行时类型来分配一个具体的函数给调用者的行为为dispatch,C++支持的多态也称Single Dispatch。
好戏开始了。
想一下,C++中的多态帮我们解决了什么问题:我们抽象出一个基类(星球),在其中定义了一些方法(搞个撞飞船吧),然后派生出一些派生类(彗星、月球、地球)。因为飞船可能会撞月球,也可能会撞地球,当我们要编程描述这个事实时,定义一个基类的指针或引用就可以保存各种可能的不同的派生类对象,这很方便。
现在,如果我们要描述不同类型的飞船撞不同类型的星球这一事实,多对多交互的关系呢?也就是在如下代码中,SpaceShip也是一个基类,我们希望在运行时,根据传递过来的实参对象是阿波罗飞船还是外星人的飞船来执行不同的操作。
class SpaceShip {};
class ApolloSpacecraft : public SpaceShip {};
class Asteroid {
public:
virtual void CollideWith(SpaceShip*) {
std::cout << "Asteroid hit a SpaceShip\n";
}
virtual void CollideWith(ApolloSpacecraft*) {
std::cout << "Asteroid hit an ApolloSpacecraft\n";
}
};
class ExplodingAsteroid : public Asteroid {
public:
void CollideWith(SpaceShip*) override {
std::cout << "ExplodingAsteroid hit a SpaceShip\n";
}
void CollideWith(ApolloSpacecraft*) override {
std::cout << "ExplodingAsteroid hit an ApolloSpacecraft\n";
}
};
如果我们进行如下定义:
Asteroid* theAsteroid = new Asteroid;
SpaceShip* theSpaceShip = new SpaceShip;
ApolloSpacecraft* theApolloSpacecraft = new ApolloSpacecraft;
Asteroid* theAsteroidReference = new theExplodingAsteroid;
theAsteroidReference.CollideWith(theSpaceShip);
theAsteroidReference.CollideWith(theApolloSpacecraft);
将会输出:"
ExplodingAsteroid hit a SpaceShip"和"
ExplodingAsteroid hit an ApolloSpacecraft"。符合预期,基类指针theAsteroidReference发挥了多态的作用。
现在,如果我们进行如下定义:
SpaceShip* theSpaceShipReference = new theApolloSpacecraft;
theAsteroidReference.CollideWith(theSpaceShipReference);
theAsteroidReference.CollideWith(theSpaceShipReference);
我们希望实参对象指针theSpaceShipReference也能发挥多态的作用。很遗憾,输出是:“ExplodingAsteroid hit a SpaceShip" 和 “ExplodingAsteroid hit a SpaceShip"。
CollideWith不能根据实参指针所指的实际对象来决定用哪一个函数。根本原因在于,类中两个不同版本的CollideWith函数参数类型不同,属于函数重载(overload),选择哪个函数是在编译期静态类型检查时确定的,现在传的是Asteroid类型的指针,那就决定了执行时调用参数是Asteroid类型指针的这个版本的CollideWith函数。不能像虚函数那样可以在运行时根据指针指向的实际类型的对象来调用相应的函数。
我们希望调用者有多态的功能,也希望函数参数也有多态的功能,这就是double dispatch的思想。事实上,像C#中已经用dynamic关键字可以很方便地实现double dispatch了,C++语言目前还不支持,不过相关标准在考虑中。C++当然也可以实现这一功能,不过要麻烦的多--visitor patten。
如果我们在SpaceShip及ApolloSpacecraft中加入下面的代码,能达到我们的目的。这段代码后面还会讲到,就将它代号为X。
virtual void accept(Asteroid& inAsteroid) {
inAsteroid.CollideWith(*this);
}
完整代码如下:
#include <iostream>
#include <sstream>
using namespace std;
class SpaceShip;
class ApolloSpacecraft;
class Asteroid {
public:
virtual void CollideWith(SpaceShip&) {
std::cout << "Asteroid hit a SpaceShip\n";
}
virtual void CollideWith(ApolloSpacecraft&) {
std::cout << "Asteroid hit an ApolloSpacecraft\n";
}
};
class ExplodingAsteroid : public Asteroid {
public:
void CollideWith(SpaceShip&) override {
std::cout << "ExplodingAsteroid hit a SpaceShip\n";
}
void CollideWith(ApolloSpacecraft&) override {
std::cout << "ExplodingAsteroid hit an ApolloSpacecraft\n";
}
};
class SpaceShip {
public:
virtual void accept(Asteroid& inAsteroid) {
inAsteroid.CollideWith(*this);
}
};
class ApolloSpacecraft : public SpaceShip {
public:
virtual void accept(Asteroid& inAsteroid) override {
inAsteroid.CollideWith(*this);
}
};
int main() {
// your code goes here
SpaceShip theSpaceShip;
ApolloSpacecraft theApolloSpacecraft;
Asteroid theAsteroid;
ExplodingAsteroid theExplodingAsteroid;
SpaceShip& theSpaceShipReference1 = theSpaceShip;
SpaceShip& theSpaceShipReference2 = theApolloSpacecraft;
Asteroid& theAsteroidReference = theExplodingAsteroid;
theSpaceShipReference1.accept(theAsteroid);
//theSpaceShipReference1.accept(theAsteroidReference);
theSpaceShipReference2.accept(theAsteroid);
//theSpaceShipReference2.accept(theAsteroidReference);
return 0;
}
输出为:
Asteroid hit a SpaceShip
Asteroid hit an ApolloSpacecraft
现在好了,有了上面X代码,谁愿意接受(accept)我小星星,我就可以投入(CollideWith)谁的怀抱。大家也可以把上面代码中的注释去掉,体会不同的排列组合的输出。
事实上,这里的飞船、阿波罗飞船,可以看着是一系列数据对象,要操作这些对象,我们定义了小星星(Asteroid)这个动作类。数据对象在编写完成后,只要加上一个accept函数,把要操作它的对象引进来,留给别人一个机会。那以后这个人(小星星Asteroid)是送花,还是请吃饭,还是撞它(CollideWith),或者需求不断变化加其它动作函数,只需要改这个动作类就行了,数据类不变。这符合开放-封闭原则。
参考:https://en.wikipedia.org/wiki/Visitor_pattern#C++_example