外部多态(External-Polymorphism)
--透明的扩展C++中已经存在的数据类型的一种对象结构模式
Chris Cleeland
chris@envision.com
Envision Solutions, St. Louis , MO 63141
Douglas C. Schmidt and Timothy H. Harrison
schmidt@cs.wustl.edu and harrison@cs.wustl.edu
Department of Computer Science
Washington University , St. Louis , Missouri , 63130
本论文发表于 1996 年 9 月 4 日 到6日在伊利诺斯州Allerton公园举办的第三次模式编程语言大会。
1、 目的
允许没有继承关系并且/或者没有虚函数的C++类能够被多态的使用。这些没有关系的类可以被使用它们的软件以统一的方式使用。
2、 动机
不同来源的C++类一起工作是困难的。应用程序经常需要这些类“体现”出某些公共的行为,但会被这些类已有的设计所限制。如果只是类接口需要适配,一个很明显解决方案是使用适配器(Adapter)或者装饰(Decorator)[1]这样的对象结构模式。有时会遇到更复杂的需求,比如既需要改变下边的接口也需要改变实现。在这种情况下,可能需要这些类的行为好像它们有公共的基类一样。
例如,试想我们正在调试一个由来自不同的库的C++类构建起来的应用程序,它能够方便的将任意对象的内部状态以人能够读懂的格式输出到文件或者显示在控制台。它甚至能够方便将所有存在的类实例放到一个集合里并且遍历这个集合让每个实例输出自己的信息。
因为集合中的元素都是同一类的,要维护一个集合就要求必须存在一个公共的基类。但是这些类早已经设计、实现并且开始使用了,并且我们也不能选择通过修改继承树并引入一个公共基类的方式—我们可能不能访问源代码。另外,像C++这样的OO语言中的类属于具体数据类型(concrete date type)[2],它需要精确的存储格局,不能包括一些隐藏的指针(例如C++的虚函数表指针)。用一个公共的多态的基类重新实现这些类是不可行的。
因此,使不相关的类体现出公共行为的解决方案必须满足以下强制约束:
1、 空间效率—方案不能影响已经存在对象的存储格局。特别是没有虚函数的类(例如,具体数据类型)不能被强制加入一个虚函数表指针。
2、 多态—所有库的对象必须可以使用统一的透明的方式访问,特别是当新类加到系统中是,我们不需要修改已经存在的代码。
考虑下边这个例子,该例子使用了ACE网络编程框架中的类[3]:
1. SOCK_Acceptor acceptor; // 全局存储
2.
3. int main (void) {
4. SOCK_Stream stream; // 临时存储
5. INET_Addr *addr =
6. new INET_Addr // 自由存储
7. ...
Sock_Stream、Sock_Acceptor、INET_Addr都是具体数据类型,它们没有一个公共的祖先,并且/或者它们都没有虚函数。如果在一次调试中,一个应用程序要在第7行检查所有存在的ACE对象的状态,我们可能得到以下的输出:
Sock_Stream::this = 0x 47c 393ab, handle_ = {-1}
SOCK_Acceptor::this = 0x 2c 49a 45b, handle_ = {-1}
INET_Addr::this = 0x 3c 48a 432,
port_ = {0}, addr_ = { 0.0.0 .0}
一个既能为这些类增加输出内部数据的能力又不需要修改它们的二进制格局的有效方法是使用外部多态模式(External Polymorphism pattern)。这个模式通过在外部构建一个平行的类继承结构来为一些具体类增加多态的行为,不需要这些具体类有继承关系。下边的OMT图演示了怎样使用外部多态模式创建一个外部的平行类继承结构:
在图中,我们定义了一个抽象基类(Dumpable)具有一个dump接口。参数化的类ConcreteDumpable<>从Dumpable继承,并且含有一个指向它的参数类型的实例的成员指针,例如:SOCK_Stream。另外,还定义了dump的函数体,它调用了dump<>模板函数(途中显示为一个不存在的模板类SignatureAdapter<>,该类以具体类作为模板参数),dump模板函数调用具体类中相应的实现方法,例如,SOCK_Stream::dump或者INET_Addr::printTo。
使用外部多态使得收集所有Dumpable实例,遍历它们并统一的调用每个实例的dump方法成为可能。特别注意原来的ACE具体数据类型不需要改变。
3、 适用性
在以下情况下使用外部多态:
1、 你的类库中包含有不能从具有虚方法的公共基类继承的具体类;并且
2、 如果你使用多态的方法处理所有的对象,你的类库或者应用程序会变得简单优雅。
在以下情况下不要使用外部多态:
1、 你的类库中已经包含了从具有虚方法的公共基类继承来的抽象数据类型;或者
2、 你的编程语言或者编程环境允许动态的将方法添加到类中
4、 结构和参与者
l Common(Dumpable)
--这个抽象类作为外部平行继承层次的基类,定义了接口,这些接口的行为会体现出多态的特点并被client使用
l ConcreteCommon<ConcreteType>(ConcreateDumpable)
--Common的这个参数化子类实现了Common定义的接口,一个典型的实现是仅仅简单的调用相应的SignatureAdapter模板函数。
l SignatureAdapter::Request<ConcreteType>(::dump<>)
--模板函数适配器,它将请求发送给对象。在某些情况下,例如:当specificRequest的形式是一致的时,这个部分就不需要了。然而,如果specificRequest在几个具体类中有不同的形式,SignatureAdapter可以用来将ConcreteCommon类与这些不同部分分开。
l ConcreteType
--ConcreteType类定义了执行要求的任务的specificRequest操作,尽管具体类没有通过继承关联起来,外部多态模式也会使你能够多态的使用它们所有的或者部分方法。
5、 协作
外部多态模式的一个典型应用是:一个外部的client类通过Common*类型的指针发送多态请求,下面是协作图:
很多使用外部多态模式的程序都维护了一个对象集合,并在上边进行遍历,统一的处理所有集合中的对象。尽管严格的说这不是模式的一部分,但它是经常被提到的一种使用情景。
6、 结论
外部多态模式具有以下优点:
l 透明的(Transparent)—本来没有被设计为在一起工作的类可以被透明的扩展为相互关联并能够被多态处理。特别是,已经存在的类的对象二进制布局不会因为增加虚函数表指针而变化。
l 灵活的(Flexible)--在支持参数化类型(例如,C++ template)的语言中实现该模式使得多态的扩展像int或者doulbe这种非可扩展数据类型成为可能。
l 外围的(Peripheral)--因为这个模式将自己放在已经存在的类的外围,因此很容易通过条件编译去掉该模式。这个特点对于那些因为调试目的而使用外部多态模式的系统来说非常有用。
该模式有以下缺点:
l 不稳定性(Instability)--在Common和ConcreteCommon中的方法必须随着Concrete类中的方法改变而改变。
l 低效(Ineffiencient)--从ConcreateCommon对象的虚方法到Concrete对象的相应方法的多次调用导致了额外的成本。然而,明智的使用内联(inline)(例如,在SignatureAdapter中和Concrete中)可以将调用成本减少到一次虚方法转发。
使用该模式需要考虑的其它问题:
l 可能的不一致情况(Possibility of inconsistency)--外部多态方法不能通过指向具体类的指针被访问,例如,在第2节的例子中,通过SOCK_Stream的指针不可能访问dump。而且,使用一个ConcreteCommon类的指针也不可能访问其它具体类的方法。
7、 实现
当使用该模式时要解决以下问题:
l specificRequest的参数。加入多态行为通常需要适合各种specificRequest的形式。这有时会很复杂,例如,有些需要参数,有些不需要。实现者必须决定是否在多态接口中暴露参数,还是不提供给客户端。
l 代码放在哪里。在第6节中提到,外部类继承结构必须与原来的类同时维护。实现者必须小心选择代码文件,把需要依赖的代码,比如原型适配器(Signature adaption)也实现在里边。
l 外部多态不是适配器(Adapter)。适配器模式的目的是把一个接口转化成一个对客户有用的接口。而外部多态则致力于为已经存在的接口提供一个新的基类。例如一个可能(serendipitous)的使用外部多态的情况:所有不同的类的specificRequest的原型都一样,这时就不需要SignatureAdapter了,在这种情况下,外部多态看起来就不像适配器了。
8、 例子代码
来看一个实现的例子,想像这样一个场景:我们需要为一个有很多类的程序建立一个灵活的调试环境。这个实现使用外部多态模式定义了一个机制,其中所有参与的对象(1)都被收集到内存中的一个集合中(2),并且可以在需要的时候输出它们的内部状态。
class Dumpable
{
public:
Dumpable (const void *);
// 纯虚函数必须被子类实现
virtual void dump (void) const = 0;
};
ObjectCollection类是客户—一个简单的集合,它拥有左右对象的句柄。是基于STL的vector[4]类实现的。
class ObjectCollection : public vector<Dumpable*>
{
public:
// 遍历集合中的所有对象并输出它们的内部状态
void dump_objects (void);
};
dump_objects方法可以像下面这样实现:
void
ObjectCollection::dump_objects (void)
{
struct DumpObject {
bool operator()(const Dumpable*& dp) {
dp->dump();
}
};
for_each(begin(), end(), DumpObject());
}
现在基本结构已经有了,我们可以定义ConcreteDumpable如下:
template <class ConcreteType>
class ConcreteDumpable : public Dumpable
{
public:
ConcreteDumpable (const ConcreteType* t);
// 具体的dump方法
virtual void dump (void) const;
private:
// 指向实际对象的指针
const ConcreteType* realThis_;
};
ConcreteDumpable类的方法实现如下:
template <class ConcreteType>
ConcreteDumpable<ConcreteType>::ConcreteDumpable(const ConcreteType* t)
: realThis_ (t)
{
}
template <class ConcreteType> void
ConcreteDumpable<ConcreteType>::dump (void) const
{
dump<ConcreteType>(realThis_);
}
现在只剩下原型适配器了。假设SOCK_Stream和SOCK_Acceptor都有一个dump方法输出到cerr,INET_Addr有一个printTo方法,该方法以一个输出流作为参数。我们需要定义两个原型适配器。第一个是一个范型原型适配器,可以和任何定义了dump方法的具体类型一起工作:
template <class ConcreteType> void
dump<ConcreteType>(const ConcreteType* t)
{
t->dump();
}
第二个是专门为INET_Addr(没有dump方法)特化的原型适配器:
void
dump<INET_Addr>(const INET_Addr* t)
{
t->printTo(cerr);
}
ObjectCollection的实例可以通过下面这样调用包含很多ConcreteDumpable<>的实例:
...
ObjectCollection oc;
// 各种对象,如SOCK_Stream等
...
oc.insert(oc.end(), aSockStream);
oc.insert(oc.end(), aSockAcceptor);
oc.insert(oc.end(), aInetAddr);
...
以后我们可以通过下边这样简单的调用获得这些对象的状态:
...
oc.dump_objects();
...
9、 已知应用
外部多态模式已经应用在下列系统中:
l ACE程序框架使用该模式注册ACE对象到一个内存中的单实例(Singleton)对象数据库。这个数据库存储了所有存在的ACE对象,可以被调试者用来得到对象的状态。因为很多ACE的类都是具体数据类型,让它们从一个包含虚方法的公共基类继承是不可能的。
l DV公司开发的用于可视化编程的程序框架DV-centro C++使用外部多态模式在无关的内部系统类外围建立继承体系。
l ObjectSpace的System<Toolkit>中的Universal Streaming System使用外部多态模式通过流来实现对象持久化。
l Morgan Stanley公司的一个内部财政项目中也发现了一个该模式的变体
l 该模式已经被应用到一些商业项目中,这些项目的代码来源不同,但是需要有公共的多态的接口。该模式的实现为ACE等各种商业库提供了统一的接口。
l 原型适配器的思想来源于OSE类库的使用。在OSE中,模板函数用来为排序链表提供比较方法,等等。
10、 相关模式
这个模式和GOF设计模式中的装饰(Decorator)和适配器(Adapter)模式很相似。装饰模式可以透明的动态扩展一个对象而不需要子类化。当客户使用一个装饰类的对象时,可以认为是在操作实际的对象,而实际是操作的装饰。适配器模式把一个接口转换成客户需要的接口,适配器使因为接口不兼容而不能一起工作的类一起工作。
在这两个模式和外部多态模式之间有几个不同,装饰模式要求它要装饰的类已经是抽象类(也就是说,它们有虚方法,会被装饰类重载)。相对的外部多态模式为具体类(没有虚方法的类)增加多态。另外,因为装饰类要从被它装饰的类继承而来,它必须定义所有继承来的方法。相对的,外部多态中的ConcreteCommon类只需要定义具体类中需要被多态处理的方法。
外部多态和GOF的适配器模式很相似,但是它们有些细微但是重要的区别:
1、 目的不同:适配器把一个接口转换成用户可以直接使用的东西。外部多态本质上没有转换接口的动机。只是增加了访问相似的功能的方法。
2、 分层vs平行(layer vs peer):外部多态模式在具体类外围创建整个类继承结构。适配器在已经存在的继承结构中增加新层。
3、 扩展vs转化:外部多态扩展已经存在的接口,使得类似的功能可以被多态的访问。适配器创建了新的接口。
4、 行为vs接口:外部多态模式更关注于自己的行为,而不是和行为关联的名字。
外部多态模式和AG通信系统内部使用并记载的多态激励者(Polymorphic Actuator)模式相似。
参考资料
[1] E. Gamma, R. Helm, R. Johnson, and J. Vlissides, Design Patterns:
Elements of Reusable Object-Oriented Software. Reading ,
MA: Addison-Wesley, 1995.
[2] Bjarne Stroustrup, The C++ Programming Language, 2nd Edition.
Addison-Wesley, 1991.
[3] D. C. Schmidt, “IPC SAP: An Object-Oriented Interface to
Interprocess Communication Services,” C++ Report, vol. 4,
November/December 1992.
[4] D. L. Musser and A. Saini, STL Tutorial and Reference
Guide: C++ Programming with the Standard Template Library.
Addison-Wesley, 1995.