8.3 建模步骤C-2 识别类的关系
首先重复本章开头所提到的:
虽然本书先讲解“识别类和属性”,再讲解“识别类的关系”,但在实际工作中,先“识别类和属性”再“识别类的关系”这个思考顺序只是一个微小的思考周期内的顺序。建模一张类图,需要很多个思考周期。也就是说,识别类和属性→识别类的关系→识别类和属性→识别类的关系→……是交错进行的。
我们阅读用例规约或其他素材,一边思考一边建模,不管识别出类、属性还是关系,画上去就是,并不需要假装看不见类的关系,非得先把类和属性都识别完了,再来识别类的关系。
8.3.1 类的关系
类的关系有三种:泛化(Generalization)、关联(Association)和依赖(Dependency),UML表示法如图8-80。
图8-80 类的关系
泛化和关联是类的静态关系。这是系统要想办法记住的关系,或者说,这两个关系属于系统要维护的“数据”。即使系统当前没有运行需要用到这些关系的用例,这些关系依然存在,随时等待着被“使用”。
泛化表示集合关系。两个类形成泛化,意味着超类的对象集合包含子类的对象集合。B、C泛化到A(或者说,B、C继承自A),意味着A的对象集合包含B和C的对象集合,如图8-81。
图8-81 泛化表示集合关系
★也可以换一种说法:子类的特征集合包含超类的特征集合。
由于是集合关系,泛化关系是没有1对多、多对多等多重性的。
任何集合都是本身的子集,但此处的“包含”不考虑这种情况,也就是说,泛化的“包含”指真子集的“包含”。泛化关系只能发生在不同的类之间,不能同一个类形成自反泛化,否则就会导致批量刷废话——每个类都无条件地刷一个自反泛化。
间接的“自反泛化”也是不允许的,A的子类或子类的子类不能成为A的超类。也就是说,泛化关系是传递和非对称的:对象集合A、B、C,如果A⊃B,B⊃C,那么可以得出A⊃C,而且C⊅B,C⊅A,B⊅A。
如图8-82的情况,都是不允许的:
图8-82 不允许的泛化关系
关联表示个体关系。关联可以发生在不同类之间,也可以发生在同一个类之间,意味着类的对象个体之间有关系。
如图8-83的左侧,A和B、C关联,意味着某个A的个体可能会和某些B和C的个体有关系;
如图8-83的右侧,A自己和自己关联,意味着某个A的个体可能会和另外的A个体有关系。
图8-83 关联表示个体关系
因为关联是类的对象个体之间的关系,所以会有1对多、多对多等多重性,甚至在某些情况下存在同一个类自己和自己的关联——自反关联。
是集合关系还是个体关系,这是泛化和关联的本质区别,从自然语言的表达来推断有时是不可靠的。
例如,自然语言"人有男有女"说的是泛化关系。"人有男有女"的意思不是一个人的个体里有若干男人个体和若干女人个体,而是说人的对象集合包含了男人的对象集合和女人的对象集合。
自然语言"人有手有脚"说的是关联关系。"人有手有脚"的意思不是人的对象集合包含了手、脚的对象集合,而是说一个人的个体组装了若干手和脚的个体。
读者可以自行体会一下“人有车有房”和“人有高富帅有屌丝”的区别。
对于比较熟悉的领域,例如刚才的男女、手脚,拍脑袋就可以知道是泛化还是关联,那拍脑袋就可以了。如果进入陌生的领域,回溯到集合和个体的本质区别是必要的。
泛化和关联的进一步内容,后文还会单独分节讲述。
再来看依赖关系:
如果类B变化,类A也需要变化,那么可以认为类A依赖于类B。
从这个定义来看,泛化和关联也是依赖关系。泛化是子类依赖于超类,关联的依赖看关联的方向。不过,泛化和关联有另外的表示法,所以一般说的依赖指除了泛化和关联之外的其他依赖,例如调用、实例化等。
类之间的依赖关系用带箭头的虚线表示,线上可以标注依赖的类型(可选),如图8-84:
图8-84 类之间的依赖
★显然,只有在信息系统的分析或设计模型中才会出现调用、实例化等依赖关系,纯粹描述领域知识的领域模型是不需要的。
这些依赖关系并非时刻都存在,而是在信息系统执行某个用例的某个步骤时才会产生,而且持续的时间非常短——从分析工作流的假设来说,就是趋近于0。因此,要描述依赖关系,仅在类图上描述“A依赖于B”是不够的,还要具体到某个场景。
例如,要描述A会调用B、C的操作,如图8-85在类图上画依赖的虚线箭头,不是不可以,但还不够。
图8-85 在类图上表达“A依赖于B、C”还不够
应该在某个用例的某张序列图(或通信图)上描述,在什么场景,进行到哪个步骤时,A需要调用B的什么操作,在什么场景,进行到哪个步骤时,A需要调用C的什么操作,如图8-86。
图8-86 “A会调用B、C”应在序列图上表达
有了序列图,如图8-85的类图上的依赖虚线就没有必要画出来了,类图上画泛化和关联关系即可。
经常看到这样的类图:上面布满了依赖的虚线,却没有泛化和关联,如图8-87。如果这样的类图描述的是不同领域的类之间的协作,那还可以接受,如果类图上都是核心域的概念,那就要警惕了,可能建模人员根本没有去寻找泛化和关联关系。
图8-87 只有依赖关系的类图
也许有的读者会想,图8-87这不挺规整的嘛!问题就在于,依赖关系为什么是这个样子,很可能是没有依据的。建模人员拍脑袋定了这样的依赖关系,然后就假装自己已经“建模”了。
如图8-88,有ABCD四个类,可以随意编排它们之间的依赖,不管哪一种,都可以写出代码来,编译器也不会报错。但哪一种更合理,要尽量从静态关系来找依据。
图8-88 没有依据,依赖可以随意编排
时不时会有开发人员向我介绍他所做系统的“架构”,“您看,A调用B,B调用C……”,然后就没了。为什么要这样调用,也讲不出什么理由,反正我就是这样做了,而且做出来了,好像也能用,就行了呗,管它洪水滔天!
8.3.2 识别泛化关系
8.3.2.1 识别泛化的思路
(1)直接形成
类图中的两个类可能会直接形成泛化关系,如图8-89所示。严格的做法是针对每两个类,思考“A是B的一种吗?”,再反过来思考“B是A的一种吗?”不过这样做工作量很大。类图中有n个类,就需要思考2C(n,2)=n(n-1)次。n=11时,就是110次了!实际工作中,往往是先扫描一遍,大脑迅速过滤出可能值得这样思考的类,针对这些类思考即可。
图8-89 直接形成-两个类之间直接形成泛化关系
实际上,类图上已有的两个类有泛化关系但未识别的情况并不多,因为之前从用例规约识别类和属性时很有可能已经发现了。
(2)自下而上(从特殊到一般)
更多的情况是发现类图上已有的两个或多个类有共同特征,于是抽象出共同的超类,如图8-90所示。
图8-90 自下而上-两个类之上有共同的超类
关联也可以看作类的属性,关联的角色名相当于类的属性名称。如果多个类关联到同一个类而且角色名相同,也可以考虑泛化出共同的超类,如图8-91。注意,角色名要相同,否则即使类型相同也不是同一属性。
图8-91 共同的关联也可以提炼超类
(3)自上而下(从一般到特殊)
如图8-92所示,这个识别思路就是8.2.5.6 属性是否对所有对象都有意义里的思路,此处就不再赘述。
图8-92 自上而下-一个类分裂出子类