如何实现软件设计中的高凝聚?


本文是下篇,上篇见这里
耦合只是结构化设计运动所定义的两个最具突破性的概念之一。
另一个可能更重要:它是关于内聚力(凝聚)的概念"。

耦合是指跨越不同模块的边界的关系,而凝聚是指模块边界内部元素之间的关系。

同样,模块的边界和它们的接口取决于你想定义为一个模块的内容,这取决于你想实现什么。
如果你在微服务层面工作,你可以把微服务视为你的模块;
如果你看一个类的内部,它的方法可以是你的模块。
其他一切都来自于此:模块的边界和接口是什么,模块之间的关系是什么,模块内部的元素是什么。
 
凝聚设计的目标
我在上面说过,软件架构中98.978972%的问题是关于耦合的。然而,结构化设计运动认为,一开始关注凝聚是比较容易的。

毕竟,我们首先考虑并创建模块。它们之间的联系是在开发过程中出现的,或者是在之后。因此,凝聚应该是在任何耦合有时间指向它的鼻子之前就被关注的。

当一个模块的元素应该归属在一起时,当它们形成一个功能整体时,它就被认为具有很强的内聚力、凝聚。
换句话说:一个模块的元素应该瞄准同一个目标;它们应该试图解决同一个领域的问题。(banq注:不只是同一个领域,而且是同一个上下文中的问题)

一个强内聚的模块有什么好处?

  1. 如果你需要改变一些逻辑,当一个模块的元素有很强的共性时,推理起来会更容易。
  2. 内聚的元素经常一起改变。不需要考虑改变多个模块和它们的接口,当你需要的一切都在一个模块中。
  3. 当你有很强的内聚力时,你通常会减少其他模块之间的联系,因为你需要的一切都在模块本身里面。简而言之,提高内聚力可以减少耦合。

像耦合一样,内聚力是一个谱系。对于一个特定的功能,很难创造出100%的内聚力的模块,因为它们可能需要其他模块的某些部分。那么,有哪些不同程度的内聚力是可能的,哪些是我们应该争取的?
 
凝聚力的不同层次
根据结构化设计运动,有6个不同层次的凝聚力。这里从最差到最好列出了它们。也就是说,结构化设计清楚地表明,这些级别中的每一个都是有用的,这取决于情况。

从本章的讨论中,你不应该得出结论说所有的逻辑模块都是坏的,也不应该得出结论说编辑和验证总是应该分布在整个系统中~也不应该试图得出任何其他黑白分明的经验法则。
 
1. 巧合的凝聚力
当一个模块中的元素没有任何有意义的关系时,就会出现巧合的内聚。例如,名为 "utils "或 "misc "的模块往往具有这种凝聚力。它们包含了所有的东西和任何东西。

改变具有巧合内聚性的模块是很困难的:它们的元素是相互独立的,所以它们很有可能被其他模块使用(因此也被耦合)。这就是为什么这种类型的内聚力是最弱的。

此外,我们很难对巧合内聚的模块进行推理,因为我们不能轻易地在我们的心理模型中把它们联系起来,也就是生活在我们大脑中的系统的代表。

然而,巧合性并不总是坏事。例如,如果你有不可能改变的机械功能,这些功能与你工作的业务领域无关,而且到处都有一些使用,那么拥有一个具有(非常)不同元素的模块可能是好的。

例如,一个模块有一个平衡二叉树的函数,另一个函数对数组进行排序,最后一个函数计算自然数的平方根。这些函数不可能改变,但它们有一些共同点:它们都是机械的,也就是说,它们不依赖于现实世界,例如你所工作的业务领域。

因为这个共同点,我们也可以把我们的模块看作是逻辑耦合的。
 
2. 逻辑凝聚
当一个模块的元素有一些弱关系时,我们可以把它的内聚力限定为逻辑性的。比如说。

  • 具有类似接口的元素。
  • 用相同的输入和/或输出工作的元素。
  • 都在使用一个数据库的元素。

这些元素的类别往往是模糊的,或者是太大了,以至于没有真正的意义。这些类别可以是技术性的(比如 "每个使用数据库的元素"),但不仅仅是。我们也可以在一个模块中封装一个广泛而无意义的领域问题。

总之,不同元素之间的共性常常让人觉得很肤浅。
 
3.  时间凝聚
这个被认为比逻辑内聚性好一点,因为时间内聚性模块的元素被限定在一个重要的维度上:时间。

事实上,这种模块的元素是在同一时间范围内执行的。例如,好的老模块在其名称中含有某种时间指示,如 "init"、"first"、"next"、"when"、"startup"、"terminal "或 "cleanup"。
 
4. 通信凝聚
交互通信调用凝聚的模块有不同的元素在同一数据上操作。因此,这是我们在本文中看到的第一类内聚力,这些元素很可能是关于同一个领域的问题;它们使用的是手头的问题数据。

这种内聚力在电子商务中很常见:例如,"股票 "模块可以有多个元素操作与产品有关的相同数据。然后,该模块与电子商务中的一个精确的领域问题相匹配,即如何在我们的代码中表示 "股票 "的概念。

也就是说,你也可以认为我们的 "股票 "模块只有逻辑上的凝聚力,如果它太大,无法正确推理。同样,这取决于你的代码库。
  
5. 顺序凝聚
顺序内聚与通信内聚类似。
不同的是:这种模块的元素采取其他元素的输出,并将其作为自己的输入。它通常遵循数据的线性转换,就像好的旧管道。

用支持函数式编程范式的语言实现顺序内聚比较容易。你可以获得许多有利于创建数据的顺序转换的结构,例如著名的 "map "或 "reduce "函数。
 
6. 功能凝聚
最后,圣杯:功能凝聚,或试图将所有与单一功能相关的东西放在一起。 (banq注:单一职责
这些模块的元素试图实现相同的目标,试图解决相同的问题。

正如我们之前所提到的,当我们在解决了问题,脱离了混乱的现实世界之后,功能的凝聚力更容易实现。以一个提高自然数的幂的函数为例:它有一个目标,它不会改变,它脱离了现实世界,生活在抽象的、有序的数学世界中。完美的模块!

作为一个旁观者,这就是为什么有些人,试图推销他们神奇的方法,让他们拥有最完美、最干净、最闪亮的代码,会很快把一些从数学中得出的例子搬上桌面。因为它们通常在功能上是内聚的,在不失去这种内聚性的情况下,将任何概念应用到它们身上都比较容易,并得出结论说这个概念因此在任何情况下都是有用的。就个人而言,当我看到人们试图通过建立一些好的旧数学模型来证明他们的观点时,我总是很怀疑。这些例子很常见,这让我经常怀疑。

已知的和经过战斗检验的算法,例如二进制搜索,也是高度凝聚的。总之,任何相当机械的东西;但我上面不是说过,机械的东西是巧合的,或者至少是逻辑上的耦合?

如果你把一个数的平方根的计算和一个把一个数提高到一个选定的幂的函数放在同一个模块中,我们可以认为其内聚性是逻辑性的(这都是关于数学的)。然而,如果你认为这两个函数是不同的模块,因为这是你想看的好的抽象层次,这些模块中的每一个都是功能上的内聚。同样,这取决于你选择的 "模块 "的定义,取决于你想实现的目标。

我记得Rich Hickey在他的一次演讲中说,创建编程语言比为实际业务开发应用程序要容易。当你为一个公司的业务领域编码时,要做到功能上的凝聚力并不那么容易。不同功能之间的界限在现实世界中并不那么清晰或稳定,不同的概念会相互 "泄露"。

例如,一个模块 "Shipment "可能需要一些关于模块 "Product "和模块 "Carrier "的知识。因此,很难将代表这些知识的代码干净地分开。某种程度的耦合可能是必要的。重新思考这些模块的内聚力也是一种解决方案。
 
不同架构的凝聚力
让我们考虑另一种高水平的内聚力分类,有两种不同的类型。

  • 技术凝聚
  • 领域凝聚

这些分类不是来自结构化领域运动,但我个人认为它们很有用。此外,它们也是许多架构风格的基础。

例如,技术凝聚力是你通常的MVC架构。不同的层是技术性的,与任何领域都没有关系。

  • 模型层是关于我们如何表示和存储信息。
  • 视图层是关于如何显示信息的。
  • 控制器管理其他两层之间的不同连接,以及潜在的第三方API。

你也可以用不同的方式来组织你的项目:不要把这些层作为模块,你可以有反映你正在工作的业务领域的模块。

根据我的经验,尝试创建与领域相关的模块效果更好,因为它们反映了我们要做的事情:通过实现一些功能为公司解决问题。因此,我们正在努力使我们的代码库与该领域的现实情况同构。

技术上的凝聚力往往是很人为的,它以一种不能捕捉领域知识的方式来分组。对我来说,它的目标是在每个层次上都有逻辑上的内聚模块。因此,当领域发生变化时,它需要同时在许多模块中发生变化,这正是我们应该努力避免的。
 
 
方法论
以下是我在构建一个应用程序时试图遵循的准则。

  1. 建立有凝聚力的模块是首要任务。我的目标是功能上的、顺序上的或沟通上的凝聚力。凝聚力应该是关于问题领域的,而不是关于技术问题。
  2. 如果我不能像我想要的那样凝聚,我会问自己为什么。如果找不到好的理由,我就努力争取更高的凝聚力。
  3. 如果可以的话,我在构建不同的模块时,或者在构建之后,都会查看它们之间的联系。我问自己:是否有很好的理由将这些模块耦合起来?我怎样才能减少耦合度?

我们的目标并不是要立即提出最好的设计。像作家一样,程序员需要多次起草领域问题的代码,然后才能抓到一个可接受的解决方案。也就是说,一个能够使代码易于调试、维护、并有足够的可扩展性的方案。

我们也可以尝试获得一些数据来做出更明智的决定,考虑到软件熵和我们解决方案的整体复杂性,当我们退后一步,试着看看我们正在走向哪里。

以下是我们在编码时可以问自己的更多问题。

  • 如果我们不得不改变这个模块,会发生什么?我们是否需要同时改变其他模块?如果是的话,我们是否应该重构这些模块,使它们更有凝聚力(因此,更少耦合)?
  • 我们应该减少这个模块的范围吗?我们可以轻松地修改它,还是需要时间,因为它太大了,我们可怜的大脑无法推理?我们是否应该考虑创建两个(或更多)模块来代替?

 

结构与混乱
一个有内聚力的模块,其所有部分都是为了解决一个共同的问题。
如果你之前几乎没有考虑过耦合和内聚的问题,那么把注意力集中在想法上而不是不同分类的术语上是一个好办法。也就是说,拥有一套清晰的、具有精确定义的词汇有助于与你的同事们进行有效的沟通。如果他们不习惯所有这些想法,你也可以把这些知识传授给他们。

我们在这篇文章中看到了什么?

  • 我们的大脑在对复杂、抽象的系统进行推理时受到限制,导致我们尝试将代码库模块化为独立的大块。
  • 一个模块是一组元素,它应该尽可能地具有凝聚力,有一个边界将模块的 "内部 "与模块的 "外部 "划定。
  • 不同模块的边界之间的连接应该通过它们的接口来完成,这是一种跨边界通信的受控方式。
  • 这些连接的耦合强度取决于模块接口的数量和复杂性,传递数据的数量和性质,以及属于不同模块的部分是否经常一起变化。
  • 耦合的类型有很多,而且新的耦合还在不断被 "发现"。
  • 创建有凝聚力的模块是避免强耦合的最好方法。换句话说:松散耦合的模块往往是相当内聚的。
  • 不精确的,或高水平的内聚力应该被避免。相反,我们的目标应该是拥有专门解决一个定义明确的问题领域的模块。
  • 用机械式的模块来实现功能上的内聚比较容易。例如,关于已解决的问题的模块,如二叉树,地图函数,或数字平方根的计算,往往是强内聚的。

软件开发中的许多其他 "原则 "都直接来自于内聚和耦合这两个概念。
了解这些首要原则,并根据你所处的环境,用你的大脑在你的代码库中平衡这些原则,将是你创建可靠的软件系统的最好盟友。
 
banq注:高凝聚和松耦合的判断非常依赖上下文,甚至需要资深商业或业务领域知识,带来了一个人认知负担,每个人的精力有限,只能对一个模块内各个角落知识全面深入掌握,除了上帝,没有人有精力和智力和认知能够跨越多个模块系统,按照康威定律的团队组织结构决定技术架构,通过观察人员的能力和他们的开会频率来判断:
为什么人员在团队之间流动这么频繁?为什么团队之间开会如此频繁?是因为这些团队内部缺乏凝聚力吗?缺乏核心凝聚吗?