外部的多态:一种用于透明地扩展c++具体数据类型的对象结构模式(作者:Chris Cleeland,Douglas C. Schmidt ,by huihoo.org CORBA课题 Thzhang 译 , Allen整理,制作) 意图允许那些没有继承关系或/和没有虚函数的类能够被多态地对待。这些没有关系的类能够被使用它们的软件以一种共同的行为处理。动机使用来自不同源的C++类是非常困难的。一个应用经常希望这些类能够"投射(project)"到一个共同的行为,但是却受到这些类存在设计的限制。如果仅仅是类的接口需要适配,一个明显解决方法是使用对象结构模式,如适配器和装饰。有时候却要有更复杂的需求,如需要改变底层的接口和实现。在这种情况下,这些类可能被需要表现得好像它们拥有一个共同的祖先(父类)。例如,考虑这样一个情况,我们正在调试一个由来自于不同c++库的类构成的应用。如果能够要求任意类实例以人可读的形式将其内部状态导出到文件或是控制台显示将是非常便捷的。如果能够将所有活动的类实例收集到集合中,使用迭代器遍历集合中的每个实例,要求它们导出自己的状态就更加方便。 因为集合是同质的(译者:即要求其中所有的对象是相同类型的),为了维护一个单一的集合,一个共同的基类必须存在。应为目前的类是已经被设计好的、实现好的并且被使用中的,通过修改继承树来引入公共的基类不是一个选择,我们可能不能访问到类的源代码。此外,类在像C++这种OO语言是具体的数据类型,这就要求有严格的存储布局,但这个布局是受到隐藏指针(如C++中的虚表指针)制约的。用一个共同的、多态的基类来重新实现这些类是不现实的。 于是,将无关的类投射到一个共同的行为需要确定以下约束该解决方案的各种因素: 1、空间效率。解决方案不能限制已经存在对象的存储布局。特别是哪些没有虚函数的类,一定不要迫使它们增加虚表指针。 2、多态。所有库对象必须以一个统一、透明的行为来访问。特别是那些新包含在系统中的类,我们不希望改变存在的代码。 考虑下面使用来自于ACE编程框架类的例子: 1. SOCK_Acceptor acceptor; // Global storage 2. 3. int main (void) { 4. SOCK_Stream stream; // Automatic storage 5. INET_Addr *addr = 6. new INET_Addr // Dynamic storage. 7. ... SOCK_Stream、SOCK_Acceptor和INET_Addr类都是具体的数据类型,因为它们没有继承自共同的祖先和/或它们不包含虚函数。如果在一个调试会话中,一个应用希望检测所有活动的ACE对象的状态,我们可能得到下面的输出: Sock_Stream::this = 0x47c393ab, handle_ = {-1} SOCK_Acceptor::this = 0x2c49a45b, handle_ = {-1} INET_Addr::this = 0x3c48a432, port_ = {0}, addr_ = {0.0.0.0} 在不改变其二进制布局情况下,通过投射来导出这些类实例状态的一种有效的方法是使用外部多态模式。这个模式构建了一种平行的、外部继承的层次,它使一个不必有继承关系的具体类集合投射到一个多态的行为上。下面的OMT图展示了如何使用外部多态模式来创建一个外部的,并行的类层次: 像图示中展示的那样,我们定义了一个抽象类dumpable,其拥有希望的dump接口方法。参数化类ConcreteDumpable<>从抽象类dumpable继承同时拥有一个指向参数类实例的指针,就是SOCK_Stream.。此外,它还为dump接口定义了一个实现(body),这个实现委托模板函数dump<>来完成最终的功能。dump<>模板函数将调用具体的实现类的对应方法,也就是SOCK_Stream::dump或INET_Addr::printTo。 使用外部多态模式,现在就可以实现收集dumpable实例,并通过迭代器遍历它们,以一致行为调用每个实例中的dump方法。注意的是原始的ACE具体数据类型没有被改变。 适应性在下列情况下使用外部多态模式:1、你的类库包含并不是从一个拥有虚方法的公共基类继承的具体数据类型。 2、你的类库或应用的行为可以被显著的简化,如果你能够以一种多态的行为看待所有的对象。 在下列情况下不要使用外部多态模式: 1、你的类库已经包含了从拥有虚方法的公共基类继承的抽象的数据类型。 2、你的编程语言和环境允许类动态的增加方法。 结构和参与者1、Common (Dumpable):这个抽象类构成外部的、并行层次的基础,它定义了接口,接口的行为将被多态的投射并被用户使用。2、ConcreteCommon 3、SignatureAdapter::request 4、ConcreteType:ConcreteType定义了specificRequest操作,用于实现希望的功能。虽然具体类是继承无关的,外部多态模式允许你多态的看待它们的全部或一些方法。 协作外部多态模式被典型地使用在当存在外部客户端通过多态的Common*接口发送请求的情况下。下面是展示这个协作过程的互动图:一些应用的外部多态模式维护一个对象集合,通过它应用可以使用迭代器统一的对待所有集合中的对象。虽然这并不是这个模式严格要求的一部分,这却是经常提及的最通常的用例。 结论使用外部多态模式带来如下好处:1、透明性:最初没有设计成工作在一起的类能够被相对透明的扩展,以至于它们能够被多态的看待。特别是存在类的对象布局没有被增加的虚指针改变。 2、灵活性:多态的扩展不具有扩展能力的数据类型,如int或double,是可能的,当这个模式被一个支持参数化类型的语言实现的时候。 3、外围性:因为这个模式在已经存在类的边界处建立,所以很容易使用条件编译来移除这个模式。对于那些使用外部多态模式用于调试目的的系统中,这个特性是特别有用。 使用外部多态模式有如下缺点: 1、不稳定性:Common和ConcreteCommon中的方法必须跟踪具体类型中方法变化。 2、低效率性:从ConcreteCommon中的虚方法到相应的具体类型对象的相应方法多次调用传递导致额外的负载。但是对于一个单一的虚函数调用,明知的使用inline可以减少这类负载。 使用这个模式需要另外考虑的一个问题: 可能的不一致性:通过指向具体类的指针不能访问外部多态方法。例如,上面的例子中不肯能通过SOCK_Stream的指针来访问dumpable接口中的dump方法。此外也不可能通过ConcreteCommon的对象指针访问具体类型的方法。 实现当实现这个模式时,将产生下列问题:1、specificRequest的参数:多态行为的投射需要适配各种specificRequest函数签名。这将是复杂的,例如当一些需要参数,一些不需要参数时。实现必须确定是否在多态接口中暴露这些参数还是将它们与用户隔离。 2、代码跑到哪里去了?正像前面所提到的,外部的类层次必须和原始的类保持并行。实现必须小心的选择源文件,在这里协作的代码将被实现,如函数签名的适配。 3、外部模式不是适配器:适配器模式的目的是转换一个接口成用户可以使用的接口。另一个方面,外部多态模式为存在的接口强行提供一个新的基接口。 例子代码Dumpable类构成层次的基础,定义了需要的多态接口,在这个例子中是用于导出信息:
ObjectCollection是一个简单的客户集合用于存放所有对象。这个类是基于STL中的victor来实现的:
dump_objects函数的实现如下:
现在基础已经完成,我们建立ConcreteDumpable:
现在只剩下签名适配器了。假设SOCK_Stream 和 SOCK_Acceptor都拥有一个dump方法使用cerr来输出信息。另一方面INET_Addr拥有一个printTo方法,该方法使用输出流作为唯一的参数。我们将定义两个签名适配器,一个是泛化的签名适配器,可以适配任何具有dump方法的具体类型: template dump { t->dump(); } 但是,第二个是特化的签名适配器,用于客户化INET_Addr:
相关模式这个模式与GOF提出的适配器模式和装饰模式非常相似。装饰模式动态的扩展一个对象而不是使用子类化的方式。当用户使用装饰对象时,它将认为它操作的是实际的对象,但是事实上它操作的是装饰对象。适配器模式是转化一个接口使其变成客户需要的接口。适配器模式使那些具有不兼容接口的类能够工作在一起。在GOF模式和外部多态模式之间有几点不同。装饰模式总是假定其装饰的类已经是抽象的。相反,外部多态模式为具体的类增加多态。此外,因为装饰器继承自它所装饰的类,它必须重新定义所有继承的方法。相反,外部多态模式中的ConcreteCommon类仅仅定义具体类中想要被多态处理的方法。 外部多态模式与GOF的适配器模式相似,但是有几个微妙但重要的差别: 1、 意图不同:适配器转化接口使其可以被客户直接使用。外部多态没有转换接口的本质动机,而是提供一个用于访问相似的功能的新的底层。 2、 上下层次对水平层次:外部多态模式在存在的具体类外层创建一个完整的类层次,适配器在存在的类层次内创建新的层次。 3、 扩展对转换:外部多态模式扩展存在的接口,使相似的功能能够被多态的访问。适配器则是创建新的接口。 4、 行为对接口:外部多态模式重要是关注自己的行为而不是和特定行为相关的名字。 |