zl程序教程

您现在的位置是:首页 >  后端

当前栏目

软件方法(下)第8章分析之分析类图—知识篇Part13-警惕拼凑泛化

方法软件 分析 知识 警惕 类图 泛化
2023-06-13 09:14:21 时间

DDD领域驱动设计批评文集>>

《软件方法》强化自测题集>>

8.3.2.3 Liskov替换原则

只是从名称上来判断,并不能成为泛化关系的最终证据。1988年,Liskov在“Data abstraction and hierarchy”文章中提出了一个判断的标准,后来被称为Liskov替换原则(LSP):

如果对于每个类型S的对象O1,都有类型T的对象O2,对于所有以T的形式定义的程序P,当O1被O2替换时,P的行为不变,那么S是T的子类型。

很多书和文章中提到Liskov替换原则时,会以矩形和正方形(有时会换成椭圆和圆)的问题为例。

假设把正方形看作矩形的子类,如图8-107。

图8-107 把正方形当作矩形的子类

设置某矩形的A边长为4,再设置B边长为5,按照设想,此时求面积应该得到4×5=20。如果用正方形代替矩形,要么为了保持正方形的约束,最终得到的面积是5×5=25,要么两边自由变化,正方形就不再是正方形了。

根据Liskov替换原则可以判断出图8-107不合适,但Liskov替换原则没有解释其中的原因。

Bertrand Meyer在“Object-Oriented Software Construction”一书用契约的观点解释:子类操作的前置条件应该不强于超类,后置条件应该不弱于超类。

例如,构造一个矩形对象需要提供两个边长参数,对这两个参数并无要求,而构造正方形对象却要求这两个参数必须相等,即,子类操作的前置条件强于超类,不合适。

我们仅从属性的角度来看看。如果独立描述矩形和正方形所需的属性,可以得到图8-108。

图8-108 建模矩形和正方形的属性

从图8-108可以得知,正方形的属性比矩形还少一个,把正方形作为矩形的子类是不合适的。

反过来,矩形倒是更像正方形的子类,复用边长,再加一个边长。Bertrand Meyer在“Object-Oriented Software Construction”中就提到,某类库的早期版本,就是让矩形继承正方形。

图8-109 摘自“Object-Oriented Software Construction( Second Edition )”, Bertrand Meyer, 1997

要建立正确的关系,可以减弱超类的定义,由子类定义有几个边长属性,如图8-110。

图8-110 超类不定义有几个边长属性

图8-110中,正方形是矩形的子类,但不是自由矩形的子类。自由矩形、正方形和黄金分割矩形(边长比为黄金分割比0.618····:1)等是互相不重叠的矩形子集(子类)。

也可以把A边长和B边长属性保留在矩形中,正方形、黄金分割矩形如果适用特殊的规则,可以作为矩形的状态。正方形、黄金分割矩形等没有增加新的属性,只是要求A边长和B边长的值符合某个约束,也就是说,正方形、黄金分割矩形是属性值组合中的一个子集的表征,这个就是状态。如图8-111。

图8-111 用状态来表达不同矩形

8.3.2.4 警惕拼凑泛化

您可能注意到,以上我们尽量通过属性(包括关联)来解释泛化关系。

虽然泛化带来的好处如Liskov所说,是落在行为上——“P的行为不变”,但如果抛开属性直扑行为,很可能会带来“假泛化”、“假面向对象”。

如图8-112,因为X和Y都有操作op1,所以泛化出A,把op1提上去成为抽象操作。这个没有问题。问题出在前面,怎么知道op1作为X和Y的操作是合适的?最终的依据还是X和Y的属性(包括关联)。

图8-112 操作怎么来的,需要有依据

有一种偷懒遮羞布,就是胡乱安排操作,然后拼凑出泛化关系,根本不顾安排的操作是否合理。

一些“面向对象”实践经常得到各种名称最后有or或er的类(汉语则为“器”),就有这种嫌疑。开发人员可能一开始按照面向过程的思路噼里啪啦把代码写出来,然后出于赶时髦需要“面向对象”,他就把过程名称加上or或er作为类名称,然后把原来的过程作为or或er类的操作。

当存在多个类似过程时,还可以加上一些泛化关系(或接口-实现)来做点缀,如图8-113,这样看起来就更有“面向对象”的味道了。

图8-113 带有泛化关系的or或er类

可能在某些开发人员眼里,图8-113很有格调,可以用来吹嘘的高大上词汇有:OCP(开放-关闭原则)、DIP(依赖倒置原则)、模板方法模式……等。这些由泛化关系衍生出来,被网红圈子广泛吹嘘的原则和模式作用十分有限,指望了解被包装出来的“SOLID原则”之类就能应对软件复杂性,那真是太天真了。

*很多人可能是从Robert C. Martin的书《敏捷软件开发-原则、方法与实践》中了解到OCP、DIP、LSP等。这些原则并非Robert C. Martin首先提出,而且和敏捷过程没有必然关系,但Robert C. Martin起这样的书名,经常会导致开发人员误解这些是敏捷人士提出来的。*

or或er类往往没有属性,只有操作,大量的逻辑仍然隐藏在子类操作的实现中。当然,开发人员也可能觉得这是好事,“这说明我有算法啊!”——“算法”,又是一个可以吹嘘的高大上词汇。

和其他的偷懒遮羞布类似,这种做法一一对应,思考工作量小,还有各种高大上词汇护法,于是开发人员洋洋得意,感觉自己已经很厉害了,连称“受用”,纷纷去拥抱这种偷懒遮羞布。

or或er类有时会使用“策略模式”作为伪装,如图8-114,哇,我可以灵活组装各种策略!顺便再吹一通“组合优于继承”之类,其实还是换汤不换药,逻辑仍然隐藏在子类操作的实现中。

图8-114 “策略模式”换汤不换药

如果一个类的命名中有类似这些内容:er、or、器、策略、Strategy、Policy、规则、Rule、算法、Algorithm……然后这个类或其子类的操作中有长长的“算法”,那么应该思考一下,长长的“算法”中到底定义了哪些变量?哪个部分的代码最复杂?

背后往往就是候选的实体类以及需要封装的操作,尽量分离出实体类,把各种逻辑尽量封装在实体类的操作中。这样的思考更辛苦,但也更有价值。

[推荐升级]23套UML+EA和StarUML的建模示范视频-全程字幕(2022.6.1更新)

6月23-26晚剔除“伪创新”的领域驱动设计-网络公开课

《软件方法》书中自测题-题目全文+分卷自测(1-8章)16套111题

《软件方法》强化自测题集110题

CTO也糊涂的常用术语:功能模块、业务架构、用户需求……[20210217更新]

如何选择UMLChina服务