类型一致性和闭合行为
1 类class和类型type
从抽象的角度来理解,最好将类视为类型的实现。也就是说类型包括了类的目标以及类的状态空间和行为。实际上,一个类型可以有多个类,每个类都包括自己独立的内部设计。一个典型的例子是STL链表类型,在GNU实现中的list类与windows实现的list类,二者拥有相同的外部特性,但在内部实现上却有着不同。
注意子类型与子类的区别,在下一节将给出子类型的定义。从语法上讲,我们可以将任意一个类定义为另一类的子类,但从语义上讲,它们没有任何意义,因为这些子类和其相应的父类之间没有任何关系。因此,即使s是t的子类(派生类),并不意味着s是t的子类型。
2 类型一致原则
类型一致性原则来源于抽象数据类型原理,此原理是面向对象的基础。类型一致性原则对于创建类库的类层次结构至关重要,在某些书中也叫为liskov替换原则,此原则的表达为:如果s为t的真子类型,则s必须与t一致,即类型s的对象可以出现在类型t的对象所需要的任何环境中,并且当此对象的任何获取操作执行时,仍能保证其正确性。
在完善的面向对象的设计中,每个类的类型必须与其基类相一致,即类或派生类的继承层次结构必须遵循类型一致原则。要这个做的原因在于,为了毫不费力的利用多态性,我们必须能传递子类对象以代替基类对象。
2.1 抗变性与协变性原则
为了保证子类的类型与基类保持一致,首先要保证子类的不变式至少和基类的不变式一样强。
其次还需要满足下列三个操作限制条件:
a 每个基类的操作必须与其子类中一个操作相对应,它们具有相同的名字和函数原型。
b 抗变原则:每个子类操作的前置条件弱于或等于基类操作的前置条件
c 协变原则:每个子类操作的后置条件强于或等于基类操作的后置条件
2.2 抗变性和协变性的实例
雇员类的一个派生类是经理类,但如何才能保证经理类是雇员的真子类呢?
假设雇员的不变式是gradLevel>0,而经理的不变式为gradLevel>20,这就保证了经理类的不变式比雇员类的不变式强,从而满足了要求。
其次,两个类都需要计算红利,基类和派生类在计算时只是输入参数范围不同。如果基类的输入参数为[0,5],输出参数为[0,10],则派生类经理的计算操作的前置条件要更弱,即输入参数若比(0,5)更小是不合法的。而后置条件应该更强,应该在(0,10)之间。
3 闭合行为原则
在类型一致的情况下,仅在只读情况下即仅执行获取操作时,才能得到完善的设计。如果处理的情况中执行了修改操作,我们还需要遵守闭合行为原则。此原则要求,子类从基类继承的行为必须符合子类的不变式。
在基于类型/子类型层次结构的继承层次结构中,派生类C的任何操作,包括从基类继承的操作,均应该满足C的类不变式。
下面举个例子来说明对这个原则的理解。基类为多边形,派生类为四边形。多边形的操作中有一个是move,即将图形整个移动到另一个位置。这个操作对四边形而言,移动之后还是四边形,因此这个操作是闭合的。而另一个操作是add_vertex,即为多边形增加一个顶点。如果四边形执行这个操作,则其不再是四边形了。因此add_vertex操作并不是闭合的。
从上面的例子中我们看到,基类行为中的子类闭合并不是自动产生的,我们必须将其设计进来。作为派生类的设计者,必须深思熟虑,并明确覆盖其基类的操作,否则将违背派生类的不变式。