8.3 建模步骤C-2 识别类的关系
8.3.3 泛化的一些重点讨论
8.3.3.2 Liskov替换原则和矩形-正方形问题
1988年,Liskov在“Data abstraction and hierarchy”文章中提出了一个判断子类型的标准,后来被称为Liskov替换原则(LSP):
如果对于每个类型S的对象o1,都有类型T的对象o2,对于所有以T的形式定义的程序P,当用o1替换o2时,P的行为不变,那么S是T的子类型。
图8-105 Liskov文章截图
很多书和文章中提到Liskov替换原则时,会以矩形和正方形(有时会换成椭圆和圆)的问题为例。
假设把正方形看作矩形的子类,如图8-106。
图8-106 正方形作为矩形的子类
设置某矩形的A边长为4,再设置B边长为5,按照设想,此时求面积应该得到4×5=20。如果用正方形代替矩形,经过上面两次设置后,最终得到的面积是5×5=25。根据Liskov替换原则判断,图8-106不合适。
关于这个问题,网络上搜索到的文章大多是从实现技巧的角度来解释和解决。
而从面向对象思想和领域逻辑的角度来思考和讨论这个问题,上世纪90年代已经有不少文献,讨论得较详细的有:
Bertrand Meyer的书“Object-Oriented Software Construction (2nd Edition)”(1998)
Kazimir Majorinc的论文“Elipse-Circle Dilemma and Inverse Inheritance”(1998)
Walter L. Hiirsch的论文“Should Superclasses be Abstract?”(ECOOP '94)
UML三友之一James Rumbaugh的书“Object-Oriented Modeling and Design”(1991)、“OMT Insights”(1996)
本书从领域知识和集合的角度来谈一谈这个问题。
从领域知识上看,矩形的定义是:有一个角是直角的平行四边形。由此衍生的性质有:对边平行且相等、对角线互相平分且相等、面积=长×宽……。正方形也确实符合矩形的定义并具有矩形的性质,所以,正方形是矩形的子类(子集),是正确的。
但是,矩形还有其他没有画出来的子类(子集),而我们不知不觉地把没画出来的矩形子集的性质当成了所有矩形的性质,导致了冲突。
如图8-107,A1是A的子类,因为A1有公认的名字(例如正方形),所以被显式画出来。但A集合除了A1子集之外,还有其他子集,可能没有想好名字,没画出来。
图8-107 产生错觉的原因
假设此时我们想到一个对象x,x是A集合的一个元素,但不是A1集合的元素。也就是说,x是图8-107中?(最大的?即A-A1)的元素,同时也是A的元素。
如果图上没有显式画出?,只有A1和A,既然x不属于A1,那就只剩下A了。于是,我们在意识中记住了“x是A的元素”,忘记了和A1同一级别的表述应该是“x是?的元素”,然后就会产生?和A相等的错觉。
正如上一小节所说,在建模泛化关系时,没有必要为了完整硬要加一个“其他*”什么之类的,但我们心里必须有这个意识。
合适的处理方法是,把不属于超类的性质从超类移除出去。
可以如图8-108。正方形是矩形的子类,除此之外,还有其他子类,如自由矩形、正方形和黄金分割矩形(边长比为黄金分割比0.618····:1)等。超类不实现“设置A边(a)”、“设置B边(b)”的操作,也不应先入为主地认为操作和属性会一一对应,例如“设置A边(a)”操作只会影响属性“A边长”的值。
图8-108 改进后的矩形泛化类图
更彻底的如图8-109,把属性的定义也放到子类。
图8-109 属性的定义放到子类
还有一个经常容易造成困惑的地方。
一个“自由矩形”对象,一开始的边长是(3,4),设置A边,让边长变成(4,4),它是不是就应该变成一个“正方形”对象?后面还能再自由设置边长吗?
它依然是“自由矩形”,只不过其两个边长在某个时刻碰巧相等,它并不是一个边长为4的“正方形”对象。这两个对象的属性值虽然相同,但遵循的行为规则是不同的。
类不止定义了属性,还定义了和这些属性相关的行为规则。
一把手牌3张K,您觉得这个牌怎么样?
图8-110 扑克牌手牌
如果是“斗地主牌”,算是好牌,如果是“21点牌”,就已经爆了。
还有一种做法,把“等边”、“黄金分割”等都看成状态,如图8-111。
图8-111 用状态来表达不同矩形
如果说一旦变成了正方形就永远只能正方形,可以如图8-112。
图8-112 一旦正方形就只能正方形