Abstract Class(抽象类)模式

—类行为型模式

 

Bobby Woolf 著,透明

 

意图

 

       为一个类体系(hierarchy)定义接口,并将具体实现交给子类。抽象类(Abstract Class)在保留类的多态性的同时,让子类可以重新定义接口的实现。

 

别名

 

       利斯科夫置换原则(Liskov Substitution Principle, [LW93])、契约设计(Design by Contract, [Meyer91])、基类(Base Class, [Auer95])、模板类(Template Class, [Woolf97])。

 

动机

 

       设想我们要实现一个简单的算术功能。每个应用程序都需要使用整数和浮点数这样的简单数字量,并且需要在这些简单数字量上进行加、减、乘、除这样的简单算术。

       处理这样简单数学问题的一个明显的方法是将它交给CPU去完成。现在的CPU都有对整数和浮点数做简单算术的内建指令。这是做这些计算最有效的途径。

       但还有一个问题摆在我们面前:并不是所有的数字量(number[1])都能用CPU的整数和浮点数来表示。对于CPU来说,整数的范围是有限的,浮点数的精度也是有限的,并且在二进制和十进制浮点数之间转换时还会有精度的损失。

       一个健壮的面向对象的数字量体系( framework )应该尽可能地利用CPU的效率。但是,为了使这个体系更加健壮,它必须在需要的时候克服CPU的限制。它应该能够为整数提供一个虚拟的“无限”的范围,包括那些极大和极小的数值。它应该为小数——至少为特定的一部分小数——提供完整的精度,它在进行简单浮点数算术运算时应该不损失精度。它甚至可以进行复数运算,只要先对之进行某些简化。

       为了达到这些目标,这个健壮的数字量体系将使用不同的类:作为CPU数的IntergerFloat、为超过CPU范围的整数准备的LargePositiveIntegerLargeNegativeInteger、为完整精度的小数准备的FixedPoint、为没有舍入(round off)的除法准备的Fraction,等等。这样,这个体系可以进行CPU能直接进行的所有计算,并且还通过使用其他的类表示了CPU不能直接表示的数字量。下面的图对这个体系中使用的类做了一个展示:

 


       对于这些数字量类来说还有一个问题:系统的其他部分不需要也不应该知道它们。系统的其他部分只知道有一些数字量对象,这些对象知道怎样进行算术运算。当系统某个地方的代码中有象“x+y”这样的一个语句时,它并不关心“x是一个Float”或者“y是一个Fraction”这些实现细节。这些代码只知道:xy是数字量,这些数字量知道怎样进行加法运算。这暗示着:如果有一些数字量对象知道怎样做加法运算,则所有的数字量对象都应该知道这一点。

       因此这个数字量体系需要的并不仅仅是不同的数字量类。它还需要清楚的指出哪些类是这个体系中的一部分。它应该要求这个体系中所有的类都能完成最小数量的某些功能,比如加法。另外,这个体系需要用多态的方式提供所有的数字量操作,这样它才能对系统的其他部分隐藏自己使用多种子类的复杂性。

       为了实现这些功能,这个体系使用了一个普适(generalized)的超类(superclass)——Number。一个Number的对象可以表现任何数字量,无论是整数、浮点数还是其他。它定义了最少的、所有的数字量都必须提供的功能,比如加法。它不对数字量的结构做定义,也不具体实现任何功能。这些细节都被推迟到IntegerFloat等子类中实现。我们用Number作为超类实现Integer,并通过相似的途径得到下面的类图:


 

       现在,所有的数字量类都是Number的子类,它们都已经被定义为可以提供基本的算术运算。使用一对数字量对象的客户(client)知道所有的数字量都可以进行基本算术运算,而不用管这些对象是Number的哪个子类。

       我们这里的Number类就是Abstract Class模式的一个范例。在《设计模式》一书中这样描述抽象类:“抽象类(Abstract Class)的主要目的是为它的子类定义公共接口。”[GHJV95,第11] Number类定义的类型可以用几种不同的方式实现,但所有的实现都将拥有同样的接口,所以客户可以交替使用它们。这样,客户代码将得到简化,它只需要描述它要做“什么”而不是去指定“怎样”做。客户代码甚至可以使用那些未知的、未来的实现,只要这些实现遵循这个公共接口。

       Abstract Class模式的关键是一个超类,它定义了一个类型及为这个类型提供不同实现版本的子类。抽象类可以是一个纯接口,这样它的子类必须实现所有的细节;但更实用的方法可能是让抽象类实现对所有子类都适用那一部分功能。这样子类只需要继承已实现的部分并完成未实现的部分。

       这个超类被称为“抽象的”是因为它的实现是不完全的,客户程序不能创建它的实例。抽象类的子类——客户可以创建这些子类的实例——被称为“具体的”(concrete class)。[WWW90,第27]

 

关键点

 

       一个包含Abstract Class模式的应用体系(framework)有如下的特点:

l         一个超类,它定义了一个类型。

l         一个或多个子类,它(们)实现超类定义的类型。

l         子类间的多态性(polymorphism),因为它们共享超类定义的接口。

这个体系还可能对本模式做一些变化:

l         超类可能提供一部分但不是全部的实现。

l         超类可能提供完整的实现,这将是一个最小化的默认实现。

l         超类可能在定义接口的同时也定义变量(state)。

l         子类可能扩展超类定义的接口,在其中加入新的功能。但是,这些扩展的接口不能与其他子类形成多态。

 

适用性

 

在以下情况使用Abstract Class模式

l         一个体系需要几个子类,这些子类拥有相同的接口,或者它们的接口相交迭而形成一个公共的核心接口。

l         公共接口应该在一个地方定义,这样所有的类都知道它们必须遵循这个接口,客户也知道应该使用这个接口。

l         一个体系需要有可扩展性,你可以在未来向其中添加新的子类而不必改变已有的超类或者客户代码。

 


结构

 


参与者

 

l         AbstractClass (Number, Integer) —— 抽象类
-
为所有具体类ConcrenteClass定义公共接口。
-
不定义变量,也不做实现,除非它对所有具体类(concrete class)——包括未来的具体类——都将是通用的。
-
本身可能是另一个AbstractClass的子类。

l         ConcreteClass (FixedPoint, Float, Fraction, LargeNegativeInteger, LargePositiveInteger, SmallInteger) —— 具体类
-
某个AbstractClass的直接子类或间接子类。
-
对继承自AbstractClass的接口进行实现。
-
在实现接口的过程中,声明需要的变量。

l         Client —— 客户
-
通过AbstractClass的接口与ConcreteClass的实例协作。

 

协作

 

l         客户使用抽象类接口与对象交互,而这个对象可能是任何一个具体类。

l         具体类的实现依赖于抽象类所提供通用的默认实现。

 

效果

 

Abstract Class模式的优点如下:

l         类多态性。具体类之间是多态的,因为它们都支持抽象类定义的公共接口。这也就是说,客户可以使用任何一个具体类而不必管它究竟是哪个具体类。这样的公共接口使扩展变得容易,因为客户程序将可以使用尚未实现的具体类,只要这些类遵循公共接口。

l         算法复用。如果抽象类包含了一些实现细节,那么它通常会以模板方法(Template Method[GHJV95,第214])的形式出现。这样的实现(在一些案例中甚至是变量)必须对所有的具体类——现在的和将来的——都是通用的。这样只需要在抽象类中实现一次,所有的具体类都可以复用它们。

使用Abstract Class模式可能会遇到下面的问题:

l         抽象和具体。客户通常认为他们可以创建任何类的实例,但对于抽象类,他们不能。客户不应该尝试创建抽象类的实例,而只能创建具体类的实例。我们把本模式中的超类叫做“抽象类”是因为它的实现并不完全,所以客户是不能创建它的实例的;而子类被叫做“具体类”是因为它们的实现是完全的,所以客户可以创建它们的实例。

l         体系单一。本模式强迫所有的具体类聚集到一个单一的类体系中,它们有共同的超类——AbstractClass。有时候,一个类从类型上看应该属于某个体系,但为了继承一些功能它实际上属于另一个体系。在这种情形下,最好是根据类型将这个类放在合适的体系中,然后让它委托另一个类的实例来实现所需的功能。
另一个可能导致这个问题的情况是:完全不同的类需要实现同样的操作。你可能让它们拥有公共的超类,并在这个超类中实现所需的操作,这样的做法当然是有效的。[2]但是这样会把这个超类变成所有的类——即使是那些不需要这个操作的类——的抽象类。很显然,要定义这个操作并复用它,继承并不是最好的方法。需要这个操作的类可以将之委托给有这个操作的其他对象,这会是更好的解决方法。

l         过分精确的接口。有时候一个具体类并不打算实现抽象类规定(specify)的所有操作。比如说,Smalltalk中的Collection抽象类规定了add:和remove:操作,但是Array子类不会实现它们。除非所有的具体类都能够实现一个操作,否则抽象类就不应该规定它。如果一个操作已经被规定了,具体类就无论如何必须实现它。如果具体类不能做出相应的操作,它应该给出一个错误信息。

 

实现

 

在实现Abstract Class模式时需要考虑以下几点:

1对类进行分解。通常我们用一个类来实现一个对象,这个类中定义接口并做出实现。这使得这个类所代表的抽象很难被复用。Abstract Class模式建议:一个对象应该用两个类来实现,一个抽象类定义接口,一个具体类实现这些接口。

2不要使用成员变量。通常抽象类不会声明任何成员变量。如果它这样做了,所有的具体类都将被迫继承这些变量。如果某个具体类的实现不需要这些变量,这就是效率上的损失。但是,如果所有的具体类都需要某个变量,而且未来的具体类也可能需要它,那么可以在抽象类中声明它。

3通过模板方法实现。抽象类通常用模板方法[GHJV95,第214]来实现。抽象类只定义接口,而将实现留给具体类。但是,如果一个操作可以有适用于所有具体类的默认实现,也可以在抽象类中实现它。这样的实现通常是一个模板方法或者一个基本的成员函数。

4不要在抽象类中加入私有信息(message)。抽象类为整个体系定义接口。接口中只应该有公有信息,具体类将实现它们。抽象类不需要定义私有信息,通常它也不会这么做。但是抽象类中也可能有私有信息,这通常是在实现模板方法时需要调用的成员函数。

 

代码示例

 

       Smalltalk中的Magnitude体系是Abstract Class模式的一个精彩的例子。它包括了我们在“动机”一节中讨论的Number体系,因为数字量也是数量[3]


       Magnitude类有六个主要的操作:等于(=)、不等于(~=)、小于(<)、大于(>)、小于等于(<=)、大于等于(>=)。Magnitude的例子包括Number(数字量)、Timestamp(时间)和Character(字符),它们都是Magnitude的子类并且懂得Magnitude的操作,如下图所示:

       六个方法中的四个是用模板方法实现的:不等于、大于、小于等于、大于等于。它们是根据两个成员函数而实现的:等于和小于。这两个成员函数被推迟到子类中实现。

Magnitude>>= aMagnitude

^self subclassResponsibility

Magnitude>>~= aMagnitude

^(self = aMagnitude) not

Magnitude>>< aMagnitude

^self subclassResponsibility

Magnitude>>> aMagnitude

^(self <= aMagnitude) not

Magnitude>><= aMagnitude

^(self = aMagnitude) or: [self < aMagnitude]

Magnitude>>>= aMagnitude

^(self < aMagnitude) not

       因此,Magnitude的子类只需要实现这两个成员函数,然后它就可以得到其它的四个方法了。举例来说,Character类假设字符是以ASCII码形式出现的,所以它按照ASCII码的顺序将字符排序。为了实现这个目的,它将使用一个方法返回一个字符的ASCII值,这个方法的名字可能是asciiValue

Character>>= aCharacter

^(self asciiValue) = (aCharacter asciiValue)

Character>>< aCharacter

^(self asciiValue) < (aCharacter asciiValue)

       这样,抽象类Magnitude为不同的具体类大大简化了实现工作。在上面的例子中,具体类只需要实现两个方法,就可以自由使用另外了四个方法了。

       抽象类还简化了客户代码必须理解的接口。客户必须知道它在对两个同一子类型的对象进行比较:两个Character或者两个Number,等等。但是,客户不必知道它们到底是哪个子类型,因为所有子类型的行为都是一样的,所有子类型都有相同的六个比较方法。

       为什么这一实现很有使用价值?一个例子是SortedCollection的工作方式。SortedCollectionCollection的子类,它可以对其中的元素进行排序。默认情况下,它认为其中的元素都是Magnitude(并且有相同的子类型),然后SortedCollection使用<=操作完成元素的排序。因此,SortedCollection不必关心这些元素究竟是CharacterNumber还是Timestamp,因为它们都是Magnitude,它们都将以正确的方式处理<=操作。

 

已知应用

 

       Abstract Class模式是如此基础,以至于几乎在任何一个多级类体系中你都可以发现它的身影。在这些体系中,超类通常是抽象的,最终类(leaf class)必须是具体的。整个Smalltalk 类体系的根类——Object——是这一语言最根本的抽象类。在JAVA中则是java.lang.Object扮演了这一角色。如果一个类体系是作为体系的根类被人了解的(就象NumberCollectionStreamWindow等等),这个根类几乎一定是一个抽象类。

       几乎每一个成文的设计模式——比如《设计模式》[GHJV95]一书中讲到的那些——都使用了一个或多个抽象类。如果需要创建一个对象实例,这些模式通常会建议使用抽象类。举例来说,Composite模式[GHJV95,第107]用抽象类ComponentLeaf类和Composite类定义接口。为了在RealSubject类上使用Porxy模式[GHJV95,第137],开发者应该使用抽象类SubjectRealSubjectProxy定义共享接口。当一个模式说某个参与者为几个子类“定义一个接口”[State”,GHJV95,第203]或者“声明(declare)一个接口”[Strategy”,GHJV95,第209[4]]时,它就是在描述一个抽象类。

       Auer对怎样开发可复用的、可扩展的类体系进行了讨论[Auer95],他也建议用一个基类来定义接口、让子类来实现它。

 

相关模式

 

       绝大多数设计级的模式都使用了抽象类。《设计模式》一书所介绍的23个模式中,有20个建议使用抽象类(SingletonFacadeMemento除外)。[GHJV95]

       一个抽象类经常会用模板方法[GHJV95,第214]来实现。

       Auer给出了一个简单的类体系开发过程,其中的接口就是用抽象类定义的。[Auer95]

 

参考书目

 

[Auer95] Ken Auer. “Reusability Through Self-Encapsulation.” Pattern Languages of Program Design. Edited by James Coplien and Douglas Schmidt. Addison-Wesley, 1995.

[GHJV95] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1995. 中文版:《设计模式:可复用面向对象软件的基础》,李英军等译,机械工业出版社20009月。[5]

[LW93] Barbara Liskov and Jeannette Wing. “A New Definition of the Subtype Relation.” ECOOP ’93, Lecture Notes on Computer Science 707. Berlin, Heidelberg: Springer-Verlag, 1993, pp. 118-141.

[Meyer91] Bertrand Meyer. “Design by Contract.” Advances in Object-Oriented Software Engineering. Edited by Dino Mandrioli and Bertrand Meyer. Prentice-Hall, 1991, pp. 1-50.

[WWW90] Rebecca Wirfs-Brock, Brian Wilkerson, and Lauren Wiener. Designing Object-Oriented Software. Prentice Hall, 1990.

[Woolf97] Bobby Woolf. “Polymorphic Hierarchy.” The Smalltalk Report. January, 1997. 6(4).

 

致谢

       我要感谢Dana AnthonySteve Berzeuk,他们帮助我对这篇文章做了改进。


[1] 译者注:number这个词在这里不太好译,叫“数据”、“数字”、“数”……似乎都不好,最后决定叫“数字量”。希望各位指正。

[2] 译者注:本句可能译得有问题,请各位指点。原文如下:The default implementation for this operation is usually defined in the first superclass they all have in common. This in effect makes that superclass an AbstractClass for those subclasses.

[3] 译者注:此处将“magnitude”译为“数量”,请注意与“数字量(number)”相区别。

[4] 译者注:在李英军先生的译本中此处为“定义所有支持的算法的公共接口”,并没有出现“声明”字样。

[5] 译者注:本文中提到这本书时,页码都以中文版为准。