13 Component cohesion

哪些类应归属于哪些组件?这是一项关键决策,需要优良的软件工程原则作为指导。遗憾的是,多年来,这一决策的制定方式一直较为随意,几乎完全依赖具体场景来定。
在本章中,我们将探讨组件内聚的三大原则:REP:复用 / 发布等价原则(The Reuse/Release Equivalence Principle),CCP:共同封闭原则(The Common Closure Principle),CRP:共同复用原则(The Common Reuse Principle)

The Reuse Release Equivalence Principle

过去十年间,涌现出了各式各样的模块管理工具,例如 Maven、Leiningen 和 RVM。这些工具的重要性日益凸显,因为在这十年里,人们已经创建了数量庞大的可复用组件与组件库。我们如今正身处软件复用的时代—— 这实现了面向对象模型最古老的承诺之一。
复用 / 发布等价原则(REP)乍一看似乎显而易见,至少事后回顾时是这样。任何想要复用软件组件的开发者,除非这些组件能通过发布流程进行追踪,并被赋予版本号,否则他们不会、也无法真正复用这些组件。
这不仅仅是因为,没有版本号就无法确保所有被复用的组件之间彼此兼容。更重要的是,这也反映了一个事实:软件开发者需要知道新版本何时发布,以及新版本会带来哪些变更。
开发者收到新版本通知后,根据其中的变更内容选择继续使用旧版本,这种情况并不少见。因此,发布流程必须提供恰当的通知与发布文档,让使用者能够在何时、是否集成新版本的问题上做出明智决策。
从软件设计与架构的角度来看,这条原则意味着:被组织进同一个组件的类与模块,必须构成一个内聚的整体。组件不能只是一堆随意拼凑的类和模块;相反,这些模块应当共享某个统一的主题或目标。
这一点按理说本应是显而易见的。不过,关于这个问题还有一个不那么直观的理解角度:归入同一个组件的类和模块,应当能够一起发布。它们共享同一个版本号、同一份发布追踪记录,并被包含在同一份发布文档中 —— 无论对组件作者还是使用者来说,这都应当是合乎情理的。
这是一条略显模糊的建议:说某件事应该 “合乎情理”,不过是故作权威地空口一说。这条建议之所以模糊,是因为我们很难精确描述究竟是什么 “粘合剂” 将这些类和模块绑定为一个组件。尽管建议不够明确,但这条原则本身却十分重要 —— 因为对它的违背很容易被察觉:就是 “看起来不对劲”。如果你违反了 REP,你的使用者会立刻发现,并且不会对你的架构能力留下好印象。
这条原则的模糊性,完全可以由后两条原则的强大约束力来弥补。事实上,共同封闭原则(CCP)与共同复用原则(CRP)从反向角度强有力地界定了这条原则。

The Common Closure Principle

将因相同原因、在同一时间发生变更的类归入同一组件。将因不同原因、在不同时间发生变更的类分入不同组件。
这是单一职责原则(SRP)在组件层面的重述。正如 SRP 主张一个类不应存在多个引起变更的原因,共同封闭原则(CCP)也主张:一个组件不应存在多个引起变更的原因。
对大多数应用而言,可维护性比可复用性更重要。如果应用中的代码必须修改,你会希望所有改动都集中在一个组件内,而非分散到多个组件中。如果变更被限制在单个组件里,我们就只需要重新部署这一个被修改的组件。其他不依赖该组件的模块,无需重新验证或重新部署。
CCP 引导我们把很可能因相同原因而变更的所有类集中到一处。如果两个类在物理或逻辑上关联紧密、总是一起变更,那么它们就应该属于同一个组件。这可以最大限度减少软件发布、重新验证和重新部署带来的工作量。
该原则与开放封闭原则(OCP)密切相关。事实上,CCP 所关注的 “封闭性”,正是 OCP 中所指的封闭性。OCP 认为,类应当对修改封闭,对扩展开放。由于 100% 的封闭性无法实现,封闭必须是策略性的。我们在设计类时,会使其对预期或已遇到的最常见类型的变更保持封闭。
CCP 进一步强化了这一思想:将对同类变更保持封闭的类归入同一组件。这样一来,当需求变更出现时,该变更极大概率只会影响最少数量的组件。
与 SRP 的相似之处
如前所述,CCP 是 SRP 的组件级版本。SRP 告诉我们:如果方法因不同原因而变更,应将它们分到不同类中。CCP 告诉我们:如果类因不同原因而变更,应将它们分到不同组件中。
两条原则可以用一句口诀概括:将同时、同因变更的部分聚合在一起;将不同时、不同因变更的部分分离开。

The Common Reuse Principle

共同复用原则(CRP)同样用于指导我们判断哪些类和模块应当归入同一个组件。该原则指出:倾向于被一起复用的类与模块,应当属于同一个组件。
类很少会被孤立地复用。更常见的情况是,可复用的类会与其他同属一个可复用抽象的类相互协作。CRP 规定,这些类应当归属于同一个组件。在这样的组件中,我们通常会看到彼此之间存在大量依赖关系的类。
一个简单的例子是容器类与其对应的迭代器。这两类类紧密耦合、总是一起被复用,因此它们应当放在同一个组件中。
但 CRP 不只告诉我们应该把哪些类放在同一个组件里,它还告诉我们不应该把哪些类放在一起。当一个组件使用另一个组件时,两者之间就建立了依赖关系。即便使用方组件只用到了被依赖组件中的一个类,这种依赖关系也不会因此减弱。使用方组件依然依赖于整个被依赖组件。
正是由于这种依赖,每当被依赖组件发生变更时,使用方组件很可能也需要做相应修改。即使使用方组件本身无需修改,它也往往需要重新编译、重新验证和重新部署 —— 哪怕它完全不关心被依赖组件里的那些变更。
因此,当我们依赖一个组件时,应当确保我们确实需要用到该组件中的每一个类。换句话说,我们要保证放进同一个组件的类是不可分割的 —— 不可能只依赖其中一部分,而不依赖另一部分。否则,我们就会被迫重新部署多余的组件,造成大量精力浪费。
所以,CRP 更多是在规定哪些类不应该放在一起,而不只是哪些应该放在一起。它明确指出:彼此没有紧密绑定关系的类,不应该放在同一个组件中。
与接口隔离原则(ISP)的关系
CRP 是 ISP 的通用版本。ISP 建议:不要依赖那些包含你用不到的方法的类。CRP 建议:不要依赖那些包含你用不到的类的组件。
所有这些原则都可以浓缩为一句精炼的话:不要依赖你不需要的东西。

The Tension Diagram For Component Cohesion

image

Conclusion

过去,我们对内聚性的理解远比复用 / 发布等价原则(REP)、共同封闭原则(CCP)和共同复用原则(CRP)所阐述的要简单。我们曾认为,内聚性不过是一个模块只做且仅做一件事的特性。
然而,这三条组件内聚原则,描绘出的是种类更为复杂的内聚关系。在决定把哪些类划归到同一个组件时,我们必须权衡可复用性与可开发性之间相互对立的张力。根据应用需求去平衡这些力量,并非易事。
更重要的是,这种平衡几乎总是动态变化的。也就是说,今天适用的划分方式,到了明年可能就不再合适。结果就是,随着项目重心从可开发性逐步转向可复用性,组件的组成结构往往会随之不断调整、持续演进。

posted @ 2026-03-20 10:01  cyusouyiku  阅读(7)  评论(0)    收藏  举报