DDD领域驱动设计批评文集>>
《软件方法》强化自测题集>>
《软件方法》各章合集>>
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 学员发给我评点的一张带有大量菱形的类图(类的信息已隐去)