软件架构III:模块化

不同的平台为代码提供了不同的重用机制,但是所有平台都支持以某种方式将相关代码分组到模块中。

在选择的开发平台中了解模块及其多种形式对于架构师至关重要。 我们有许多架构分析的工具(例如指标、适应度函数和可视化)都依赖于这些模块化概念。 模块化是一种组织原则。

定义

词典将模块定义为“可用于构建更复杂结构的一组标准化零件或独立单元”。 我们使用模块来描述相关代码的逻辑分组,这些逻辑分组可以是面向对象语言中的一组类,也可以是结构化或功能性语言中的函数。

编程语言现在具有各种各样的包装机制,这使得开发人员很难在它们之间进行选择。 例如,在许多现代语言中,开发人员可以在函数/方法,类或包/命名空间中定义行为,每种行为都有不同的可见性和作用域规则。 其他语言通过添加诸如元对象协议之类的编程结构进一步解决复杂问题,从而为开发人员提供了更多的扩展机制。

架构师必须意识到开发人员如何打包事物,因为它在架构中具有重要意义。

测量模块化

模块化的测量包括:内聚、耦合和连通的测量。

内聚

内聚是指模块的各个部分应包含在同一模块中的程度。

计算机科学家定义了一系列内聚度量,从最佳到最坏列出如下:

  • 功能内聚
    模块的每个部分都相互关联,并且该模块包含功能必需的所有内容。
  • 顺序衔接
    两个模块进行交互,其中一个模块的输出数据,成为另一个模块的输入。
  • 沟通内聚
    两个模块构成了一条通信链,其中每个模块都基于信息进行操作和/或有助于某些输出。例如,将记录添加到数据库并基于该信息生成电子邮件。
  • 程序内聚
    两个模块必须以特定顺序执行代码。
  • 时间内聚
    模块基于时序依赖性而相关。例如,许多系统列出了一些看起来不相关的事物,必须在系统启动时对其进行初始化。这些不同的任务在时间上具有凝聚力。
  • 逻辑内聚
    模块内的数据在逻辑上相关,但在功能上无关。例如,考虑一个模块,该模块可以转换文本,序列化对象或流中的信息。操作是相关的,但功能却大不相同。
  • 巧合内聚
    模块中的元素除了在同一源文件中之外,不相关。这是凝聚力的最负面形式。

Chidamber和Kemerer面向对象指标套件是由同名作者开发的,用于衡量面向对象软件系统的特定方面。 该套件包括许多常见的代码度量标准,例如圈复杂度(请参阅"圈复杂度")和"耦合"中讨论的几个重要的耦合度量。

Chidamber和Kemerer内聚方法缺乏(LCOM)度量标准测量模块(通常是组件)的结构内聚。 初始版本出现在下面的公式中。

对于不访问特定共享字段的任何方法,P都会增加1,对于确实访问共享特定共享字段的方法,Q会减少1。 作者理解那些不了解这种表述的人。 更糟糕的是,随着时间的流逝,它变得越来越复杂。 1996年引入的第二个变体(因此名称为LCOM96B)出现在如下公式。

因为下面的书面说明更加清楚,所以我们不会在公式中纠缠变量和运算符。 基本上,LCOM度量公开类之间的偶发耦合。 这是LCOM的一个更好的定义:LCOM不通过共享字段共享的方法的总和考虑具有私有字段a和b的类。 许多方法仅访问a,而许多其他方法仅访问b。 未通过共享字段(a和b)共享的方法集之和很高; 因此,该课程的LCOM评分较高,表明该方法在缺乏内聚性方面得分很高。 考虑图中所示的三个类。

image.png

在图中,字段显示为单个字母,方法显示为块。 在X类中,LCOM得分较低,表明良好的内聚结构。 但是,Y类缺乏内聚。 Y类中的每个字段/方法对都可以出现在其自己的类中,而不会影响行为。 Z类显示了混合的内聚,开发人员可以在其中将最后的字段/方法组合重构为自己的类。

LCOM指标对于正在分析代码库以从一种架构样式转换为另一种架构样式的架构师很有用。 迁移架构是共享实用程序类时的常见头痛之一。 使用LCOM度量标准可以帮助架构师找到偶然耦合的类,并且从一开始就不应该是单个类。

许多软件指标存在严重缺陷,LCOM也无法幸免。 该指标可以找到的只是结构上的内聚不足; 它无法从逻辑上确定特定部分是否组合在一起。 这反映了我们的软件架构第二定律:更喜欢问为什么而不是如何做。

耦合

幸运的是,我们有部分基于图论的更好的工具来分析代码库中的耦合:由于方法的调用和返回形成调用图,因此基于数学的分析成为可能。

实际上,几乎所有平台工具都可以使架构师分析代码的耦合特性,以帮助重组、迁移或理解代码库。

抽象性、不稳定性和与主序列的距离

抽象性是抽象工件(抽象类,接口等)与具体工件(实现)的比率。 它代表了抽象性与实现性的度量。

架构师通过计算抽象工件的总和与具体工件的总和之比来计算抽象度。

另一个导出的度量标准,不稳定性,定义为传出耦合与传出和传入耦合总和之比,如公式所示。

不稳定性度量标准确定代码库的易变性。 由于高度耦合,表现出高度不稳定性的代码库在更改时更容易破坏。

与主序列的距离

架构师为架构提供的几个整体指标之一是与主序列的距离,即基于不稳定性和抽象性的衍生指标,如公式 D = |A + I - 1| 所示。

在方程中,A=抽象性,I=不稳定性。

注意抽象性和不稳定性都是比率,这意味着它们的结果总是介于0和1之间。因此,在绘制关系图时,我们可以看到如下图形。

image.png

距离度量在抽象性和不稳定性之间设想了一个理想的关系;接近这条理想线的类展示了这两个相互竞争的关注点的健康混合。例如,绘制一个特定的类允许开发人员计算到主序列度量的距离,如图所示。

image.png

在图3-3中,开发人员将候选类绘制成图形,然后测量与理想线的距离。越靠近底线,类就越平衡。太过靠近右上角的类进入了架构师所称的无用区域:过于抽象的代码变得难以使用。相反地,落在左下角的代码进入了痛苦的区域:实现太多而抽象不够的代码变得脆弱且难以维护,如图所示。

image.png

许多平台上都存在提供这些措施的工具,这些工具可帮助架构师在分析代码库时由于不熟悉、迁移或技术债务评估而有所帮助。

请注意,前面提到的爱德华·尤登(Edward Yourdon)和拉里·康斯坦丁(Larry Constantine)的书(结构设计:计算机程序和系统设计学科的基础)早于面向对象语言的流行,而侧重于结构化的编程构造,例如函数(非方法)。 它还定义了其他类型的耦合,我们在这里不介绍这些耦合,因为它们已被共生性取代。

共生性

1996年,Meilir Page-Jones出版了《每个程序员应该了解的面向对象设计(Dorset House)》,完善了传入和传出的度量标准,并用他命名为共生性的概念将其转换为面向对象的语言。 他对术语的定义如下:

如果一个组件的变更需要修改另一个组件以保持系统的整体正确性,则两个组件是共生的。 ——梅里尔·佩奇·琼斯

存在两种类型的共生:静态和动态。

静态共生

静态共生是指源代码级的耦合(与执行时耦合相对,在“动态共生”中进行了介绍);它是对结构化设计定义的传入和传出耦合的改进。换句话说,架构师将以下类型的静态共生视作事物传入或传出的程度:

名称共生(CoN)
多个组件必须在实体名称上达成共识。

方法名称代表了代码库耦合的最常见方式,也是最可取的方式,尤其是考虑到使系统范围内的名称更改变得微不足道的现代重构工具。

类型共生(CoT)
多个组件必须在实体类型上达成共识。

这种类型的共生是指许多静态类型语言中的通用功能,用于将变量和参数限制为特定类型。但是,此功能并不是纯粹的语言功能,某些动态类型化的语言提供选择性键入。

意义共生(CoM)或公约共生(CoC)
多个组件必须在特定值的含义上达成共识。

在代码库中这种类型的连续性最常见的情况是硬编码数字而不是常量。例如,在某些语言中,通常考虑在某个地方定义int TRUE = 1。 int FALSE =0。想像一下如果有人翻转这些值会出现问题。

位置共生(CoP)
多个组件必须在值的顺序上达成一致。

这是方法和函数调用的参数值存在的问题,即使在具有静态类型的语言中也是如此。例如,如果开发人员创建了一个方法void updateSeat(String name, String seatLocation)并使用值updateSeat("14D", "Ford, N") 对其进行调用,则即使类型正确,语义也不正确。

算法共生(CoA)
多个组件必须在特定算法上达成共识。

当开发人员定义必须在服务器和客户端上运行并产生相同结果以认证用户的安全哈希算法时,就会发生这种类型的连续性。显然,这代表了一种较高的耦合形式-如果任何一种算法更改了任何细节,握手将不再起作用。

动态共生

执行共生(CoE)
多个组件的执行顺序很重要。

考虑以下代码:

email = new Email();
email.setRecipient("[email protected]");
email.setSender("[email protected]");
email.send();
email.setSubject("whoops");

由于必须按顺序设置某些属性,因此无法正常工作。

时机共生(CoT)
执行多个组件的时间很重要。

这种类型的连续性的常见情况是由两个线程同时执行导致的竞争状态,从而影响联合操作的结果。

价值共生(CoV)
当多个值相互关联并且必须一起更改时发生。

考虑开发人员将矩形定义为代表角的四个点的情况。为了保持数据结构的完整性,开发人员无法在不考虑对其他点的影响的情况下随机更改其中一个点。

更常见和有问题的情况涉及事务,尤其是在分布式系统中。当架构师设计具有单独数据库的系统,但又需要在所有数据库中更新单个值时,所有值必须一起更改或根本不更改。

身份共生(CoI)
在多个组件必须引用同一实体时发生。

这种类型的连接的常见示例涉及两个独立的组件,它们必须共享和更新一个通用的数据结构,例如分布式队列。

架构师很难确定动态连通性,因为我们缺乏能够像分析调用图一样有效地分析运行时调用的工具。

共生属性

共生是面向架构师和开发人员的分析工具,并且共生的某些属性可帮助开发人员明智地使用它。 以下是每个这些连续性属性的描述:

强度

建筑师通过开发人员能够轻松重构这种耦合来确定连通性的强度。 如图3-5所示,显然更需要不同类型的大便。 架构师和开发人员可以通过重构更好的连续性类型来改善其代码库的耦合特性。

架构师应该选择静态连接而不是动态连接,因为开发人员可以通过简单的源代码分析来确定静态连接,而现代工具使静态连接变得微不足道。 例如,考虑意义的连续性的情况,开发人员可以通过创建命名常量而不是魔术值来重构名称的连续性,从而改善这种情况。

image.png

地区性

连续性的位置可衡量模块在代码库中彼此之间的距离。与在不同模块(在独立模块或代码库中)分离的代码相比,近邻代码(在同一模块中)通常具有越来越多的形式。换句话说,如果相距很近,则表示耦合不良的连通形式会更好。例如,如果同一组件中的两个类具有意义的连续性,则与两个组件具有相同形式的连续性相比,它对代码库的破坏较小。

开发人员必须同时考虑实力和地区性。在同一模块中发现的更强大的共生形式表示的代码嗅觉要比散布在同一个容器中的共生少。

连贯性的程度与影响的大小有关—它影响几个还是多个类?较小的连续性损坏代码基础较少。换句话说,如果您只有几个模块,那么动态的连续性并不可怕。但是,代码库趋于增长,相应地,一个小问题也变得更大。

Page-Jones提供了使用连续性改善系统模块性的三个准则:

  1. 通过将系统分解为封装元素来最大程度地减少总体占用
  2. 最小化跨越封装边界的所有剩余内容
  3. 最大化封装边界内的连续性

富有传奇色彩的软件架构创新者Jim Weirich重新流行了共生的概念,并提供了两条很好的建议:
等级规则:将强势的共生转化为弱势的共生。
局部性规则:随着软件元素之间的距离增加,请使用较弱的共生。

统一耦合和共生度量

到目前为止,我们已经讨论了耦合和共生,来自不同时代和针对不同目标的措施。但是,从架构师的角度来看,这两种观点是重叠的。 Page-Jones识别为静态连续性的内容表示传入或传出耦合的程度。结构化编程只关心输入或输出,而共生关心的是事物如何耦合在一起。

为了帮助可视化概念上的重叠,请考虑下图。结构化的编程耦合概念显示在左侧,而共生特征显示在右侧。共生被结构化编程称为数据耦合(方法调用),它为应该如何表现这种耦合提供了建议。结构化编程并没有真正解决动态融合所涉及的领域;我们很快将这一概念封装在“架构量子和粒度”中。

image.png

20世纪90年代康纳什的问题

架构师在应用这些有用的度量来分析和设计系统时存在一些问题。首先,这些度量着眼于低层代码的细节,关注的是代码质量和健壮性,而非必要的架构结构。架构师更关心模块是如何耦合的,而不是耦合的程度。例如,架构师关心同步与异步通信,而不太关心如何实现。

共生的第二个问题在于,它并没有真正解决一个基本决策,即许多现代架构师必须在诸如微服务之类的分布式体系结构中进行同步或异步通信?回到软件体系结构的第一定律,一切都是权衡的。在讨论了架构特征的范围之后,我们将介绍一些新的思考现代共生性的方法。

从模块到组件

我们始终使用术语模块作为相关代码捆绑的通用名称。然而,大多数平台都支持某种形式的组件,这是软件架构师的关键构建块之一。逻辑或物理分离的概念和相应的分析从计算机科学的早期就已经存在。然而,随着所有关于组件和分离的写作和思考,开发人员和架构师仍然在努力实现好的结果。

你可能感兴趣的:(软件架构III:模块化)