8.3.3 识别关联关系
8.3.3.1 关联的进一步细分
是否进一步细分各种关联,各种面向对象方法学观点不同。有的认为关联就是关联,不用再细分,有的则认为需要进一步细分。
例如,James J. Odell就把聚合分为6种并详细讨论,如图8-117。
图8-117 摘自Journal Of Object-Oriented Programming Vol 5, No 8. , James J. Odell , 1994
UML规范采取的是中间路线,把关联分为三种:普通关联、聚合(Aggregation)和组合(Composition)。
用图形表示,普通关联是一根直线,聚合有一端是空心菱形,组合有一端是实心菱形,如图8-118。
图8-118 三种关联的图示
在UML元模型中,把它们视为属于三个不同的AggregationKind,如图8-119。
图8-119
从元模型上看,“聚合”应该叫作“分享型聚合”,“组合”应该叫作“组合型聚合”,但本书还是使用“聚合”、“组合”,原因阅读后文自知。
聚合和组合都表示“整体-部分”的关系,在类图中,菱形一端表示整体,另一端表示部分。
相对于聚合,组合还有两条额外的约束:
(1)在同一时刻,部分对象只属于一个整体对象;
(2)整体对象被销毁,部分对象也要销毁;
虽然UML定义了聚合的概念,但实践中要不要使用聚合,经常会引起争论。在聚合关联中,部分对象同一时刻可以被多个整体对象共享,使得“整体-部分”的概念变得模糊,和普通关联难以区分。
James Rumbaugh等人在《UML参考手册(第2版)》中认为聚合是建模的“安慰剂”。
图8-120 摘自Unified Modeling Language Reference Manual, 2nd Edition, James Rumbaugh, Ivar Jacobson, Grady Booch, 2004
Craig Larman认为不需要使用聚合,在合适的情况下使用组合即可。
图8-121 摘自Applying UML and Patterns: An Introduction to Object-Oriented Analysis and Design and Iterative Development, Third Edition, Craig Larman, 2004
本书的做法和Larman一致,不使用聚合,有必要表达“整体-部分”关联时,仅使用组合。
含义模糊的聚合如图8-122。“微信群有微信账户”,而且微信群解散,微信账户还在,所以把“微信群”和“微信账户”之间的关联设为聚合,多重性为多对多。
图8-122 含糊的聚合举例
图8-122可以改为更清晰的图8-123。
图8-123 使用组合
图8-123将“微信群员”和“微信账户”分离,“微信群员”仅属于一个“微信群”。如果“微信群”对象消失,“微信群员”对象及相关属性值也就消失了,但“微信账户”还在。
注意,即使是图8-123,也要有足够证据才能建模成组合。如果只是玩文字游戏,图8-123也可以变成图8-124,从另一个角度来“组合”似乎也未尝不可。
图8-124 另一个角度的组合
以上说的“足够证据”,指的是是否有利于责任分配。
8.3.3.2 组合的作用
把关联定义为组合关联,意味着部分对象成为整体对象的部件,外部的对象不能发消息给部分对象,只能发给整体对象,再由整体对象分解和分配给组成它的部分对象,如图8-125所示。
图8-125 组合关联影响责任分配
类图上有很多类,类之间的密切程度会有所不同。如果根据目前责任分配的情况,判断某些类之间协作的频率远超过它们和外部其他类协作的频率,而且预判将来也可能是这样,那么通过建立组合关联来强制把它们封装成一个整体来分配责任,是合算的。
和划分部门类比
建立组合关联和公司的部门划分有类似之处。
公司不划分部门,老总一个个员工派任务也能达到目标,只是效率不高,而且不管出现什么变化都要打开老总的“代码”来修改。
划分部门之后,老总就省心多了,只需要给各部门分配大任务,部门把任务分解,再分配给部门内的各小组,各小组再把任务分解,分配给小组内的小小组……。这样,各种逻辑就会分散到各个部门、小组、小小组。
当然,这是有代价的。划分部门之后,上级就不要越过下级去找更下级,下级也不能想找谁就找谁,都要讲基本法。
如果部门内各下级之间的协作频率远高于和其他部门协作的频率,说明这样的代价是值得付出的,部门划分以及责任的分解和分配是合理的。反之则说明不合理。
不要因为偷懒或炫耀而定义组合
可以想象,如果公司老总在没有充分调研员工能力以及公司业务的情况下,着急过一把官瘾,胡乱划分部门,提拔干部,会大大损害所有人的利益,很容易激起反抗。
然而,如果软件开发人员着急过“架构师”的瘾,胡乱定义组合(聚合)关联,并不会激起各个代码片段的反抗。计算机程序目前还没有产生自我意识,没有Neo(电影“The Matrix”,《黑客帝国》),特别乖,爱怎么整都可以。
以前可能是为了偷懒而胡乱定义组合(聚合)关联。
如图8-126,如果建模为普通关联,还得给关联想个合适的名字。算了,懒得想,貌似说“订单有顾客”也说得通嘛,“有”那不就是组合(聚合)吗?干脆加个菱形吧,这样还省事,而且相对于一根直线,菱形让人有高大上的感觉!
图8-126 为了偷懒滥用组合(聚合)
最近一些年,由于DDD话语对“聚合”过度吹嘘,某些软件开发人员把“划分聚合”看成“有架构师能力”的表现,于是在没有足够证据的情况下,兴奋地把“聚合”到处用——哈哈,我会切割系统了,我架构师了!
这些人的思维经常是颠倒的:先拍脑袋定“聚合”,然后就按DDD话语的建议来使用,包括外部对象的访问、创建、访问数据等,然后再用实现的代码(show me the code嘛)来“证明”之前划分的“聚合”是正确的,形成一个“完美”的循环。
用公司类比,相当于公司老总拍脑袋把张三、李四、王五等人划分成一个部门,并任命张三为部门领导,然后通过张三发号施令,再用这个“事实”来“证明”张三作为部门领导是正确的。当然,这样类比不完全贴切,后文还会提到。
定义组合(聚合)务必谨慎。在有足够的核心域逻辑作为证据之前,应该先建模为普通关联。经过序列图、状态机图等进一步建模核心域逻辑之后,如果有进一步证据支持定义组合(聚合)关联有利,可以定义组合(聚合)关联用于指导后续其他用例的责任分配。如果没有足够证据,不定义组合(聚合)关联也无所谓。
图8-127 学员发给我评点的一张带有大量菱形的类图(类的信息已隐去)
8.3.3.3 和《设计模式》用语的区别
说到这里,我们要整理一下类之间的关系,用类图表示如图8-128。
图8-128 “类之间的关系”各概念之间的关系
从图8-128可以看到,泛化、关联和依赖在一个抽象级别,普通关联、聚合和组合在一个抽象级别。此处我们把“聚合”定义为“组合之外的其它聚合”,同样,“普通关联”定义为“聚合和组合之外其他关联”。
否则,各种关联概念之间的关系应该像图8-129:
图8-129 各种关联概念之间的关系:另一个版本
按照图8-128,我们在表达的时候要注意,说“泛化和关联”可以,但说“泛化和聚合”、“泛化和组合”或“继承和组合”是不合适的。
而GoF所写的”Design Patterns: Elements of Reusable Object-Oriented Software”(《设计模式》),第1章中有一句被广为流传的话:
Favor object composition over class inheritance.
优先使用对象组合而不是类继承。
这句话常让人误解组合和继承是一个级别的,其实,根据GoF《设计模式》的用词,这句话中的“组合”应该近似于UML中的“关联”。
如图8-130,在GoF《设计模式》中,给出这句话之后,作者接下来讨论了aggregation(聚合)和acquaintance(认识)的区别,并且说acquaintance有时也被称为association(关联)或using(使用)。然而,在后面的内容中,作者把这几个词全部抛弃,一律使用composition。
图8-130 摘自Design Patterns: Elements of Reusable Object-Oriented Software, Erich Gamma , et al. , 1995
根据GoF书中内容猜测,其中用词和UML以及本书的用词的对应关系可能如图8-131。左右对应为:①继承=泛化;②组合≈关联;③认识≈普通关联;④聚合≈聚合+组合。
图8-131 《设计模式》话语和UML话语中的对应
以上仅属推测,而且书中的叙述也是有矛盾的,例如这一句:
Consider the distinction between object aggregation and acquaintance and how differently they manifest themselves at compile- and run-times.
考虑对象聚合和认识之间的区别,以及它们在编译时和运行时如何不同地展现自己。
这句话好像是在说“聚合”和“认识”在编译时和运行时有所不同,这和图8-131中的对应③和④矛盾。
另外,图8-130的片段中,把association(关联)和using(使用)说成同一个意思,这个也是让人困惑的。using听起来更像是UML话语中的“依赖”。
8.3.3.4 DDD****话语中的“聚合”和“聚合根”是伪创新
(待续……)