软件架构师 第一部分 基础篇 第二章 模块化

首先,我们想弄清在围绕模块化的架构的讨论中使用和经常使用的一些通用术语,并提供在本书中使用的定义。

[关于软件架构]的词语中有95%用于赞扬模块化的好处,而关于如何实现模块化的话很少。

格兰福德·迈尔斯(Glenford J.Myers)(1978)

不同的平台为代码提供了不同的重用机制,但是所有平台都支持以某种方式将相关代码分组到模块中。虽然这个概念在软件体系结构中是通用的,但事实证明它的定义很难。随意在互联网搜索会产生数十个定义,没有一致性(也有一些矛盾)。从Myers的引文中可以看出,这不是一个新问题。但是,由于不存在公认的定义,因此为了整本书的一致性,我们必须跳入争端并提供我们自己的定义。

在选择的开发平台中了解模块化及其多种形式对于架构师至关重要。我们必须分析架构的许多工具(例如指标,适度函数和可视化)都依赖于这些模块化概念。模块化是一种组织原则。如果架构师在设计系统时没有注意各个部分如何连接在一起,那么他们最终会创建一堆很难以维护的系统。基于物理学的参考,软件工程对复杂的系统进行建模时,这些系统倾向于熵(或无序)。为了将能量在物理系统中得以保持秩序。软件系统也是如此:架构师必须不断消耗精力以确保良好的结构稳健性,而不是让它随性发展。

保持良好的模块化体现了我们对隐式架构特征的定义:几乎没有项目有要求架构师确保良好的模块化区别和沟通的要求,而可持续的代码库则要求有序和一致性。

定义

通常将模块定义为“每个可用于构建更复杂结构的一组标准化零件或独立单元。” 我们使用模块化来描述相关代码的逻辑分组,该逻辑分组可以是面向对象语言中的一组类,或者结构化或功能性语言中的函数。大多数语言都提供了模块化的机制(Java中的package,.NET 中的namespace)。开发人员通常使用模块作为将相关代码分组在一起的一种方式。例如,com.mycompany.customer 在Java包表示与客户有关的内容。

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

架构师必须认识到开发人员如何打包工件,因为它在架构中具有重要意义。例如,如果几个封装紧密地耦合在一起,那么将其中一个组件重用于相关模块将变得更加困难。

类中模块化重用

早于面向对象语言的开发人员可能会对为什么存在如此众多的不同分离方案感到困惑。大部分系统兼容性不是与代码的向后兼容性有关,而是与开发人员如何考虑事物有关。1968年3月,埃德斯·迪克斯特拉(Edsger Dijkstra)ACM通讯中发表了一封公开信,题为“Go To Statement Considered

Harmful”。他否定了GOTO语句在编程语言中的普遍用法,当时该用法允许在代码内进行非线性跳跃,从而使推理和调试变得困难。

本文帮助引入了以Pascal和C为例的结构化编程语言时代,这鼓励人们对事物如何组合进行更深入的思考。开发人员很快意识到,大多数语言都没有很好的方法将逻辑上相似的东西组合在一起。因此,模块化语言的短暂时代诞生了,例如Modula(Pascal的创建者Niklaus Wirth的下一种语言)和Ada。这些语言具有模块的编程构造,就像我们今天对包或名称空间(但没有类)的考虑一样。

模块化编程时代是短暂的。面向对象的语言之所以流行,是因为它们提供了封装和重用代码的新方法。尽管如此,语言设计师还是意识到了模块的效用,将它们保留为包,名称空间等形式。语言中存在许多奇怪的兼容性功能,以支持这些不同的范例。例如,Java支持模块化(通过使用静态初始化程序的程序包和程序包级初始化),面向对象和功能范式,每种编程风格都有自己的作用域规则和怪癖。

对于架构的讨论,我们使用模块化作为通用术语来表示相关的代码分组:类,函数或任何其他分组。这并不意味着物理上的分离,而只是逻辑上的分离。差异有时也很重要。例如,从便捷的角度来看,将大量类集中在一起在单片应用程序中可能是有意义的。但是,当需要重组架构时,松散分区所促进的耦合将成为将整体分离的障碍。因此,将模块化作为与特定平台强制或隐含的物理分隔分开的概念进行讨论是有用的。

值得注意的是,命名空间的一般概念与.NET平台中的技术实现是分开的。开发人员通常需要精确,完全合格的软件资产名称,以将不同的软件资产(组件,类等)彼此分开。人们每天使用的最明显的例子是互联网:绑定到IP地址的唯一的全局标识符。大多数语言都有某种模块化机制,可以兼作命名空间来组织事物:变量,函数或方法。有时,模块结构会在物理上反映出来。例如,Java要求其程序包结构必须反映物理类文件的目录结构。

没有命名冲突的语言:JAVA 1.0

Java的原始设计者在处理各种编程平台中的名称冲突和处理冲突方面具有丰富的经验。Java的原始设计使用了很多巧妙的技巧,以避免在两个具有相同名称的类之间产生歧义的可能性。例如,如果您的问题域包括目录顺序和安装顺序:两者都命名为顺序,但是具有不同的含义(和类)。Java的解决方案是创建package名称空间机制,并要求物理目录结构仅与程序包名称匹配。由于文件系统不允许相同的命名文件位于同一目录中,因此它们利用操作系统的固有功能来避免产生歧义的可能性。因此,classpathJava中的原始版本仅包含目录,从而避免了名称冲突的可能性。

但是,正如语言设计人员所发现的那样,强迫每个项目都具有完整的目录结构非常麻烦,尤其是随着项目规模的扩大。此外,构建可重复使用的工件非常困难:必须将框架和库“分解”到目录结构中。在Java的第二个主要版本(1.2,称为Java 2)中,设计人员添加了该jar机制,允许存档文件充当类路径上的目录结构。在接下来的十年中,Java开发人员一直努力将classpath目录和JAR文件结合在一起,以实现正确的选择。 当然,最初的意图已被破坏:现在,两个JAR文件可以在类路径上创建冲突的名称,从而引发了许多调试类加载器的故事。

度量模块

考虑到模块化对架构师的重要性,他们需要工具来理解它。 幸运的是,研究人员创建了各种与语言无关的指标,以帮助架构师理解模块化。我们专注于三个关键概念:内聚性耦合性共生性

内聚

内聚性是指一个模块的各个部分应该包含在同一个模块中的程度。换言之,它是衡量各部分之间的关联程度。理想情况下,内聚模块是所有部件都应该打包在一起的模块,因为将它们拆分成更小的部分需要通过模块之间的调用将这些部件耦合在一起以获得有用的结果。

尝试分割内聚模块只会导致耦合性增加和可读性下降。

拉里·康斯坦丁

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

功能内聚

模块的每个部分都相互关联,并且该模块包含功能必需的所有内容。

顺序衔接

两个模块进行交互,其中一个输出数据,而另一个成为输入。

通信内聚

两个模块构成了一条通信链,其中每个模块都基于信息进行操作和/或有助于某些输出。例如,将记录添加到数据库并基于该信息生成电子邮件。

程序内聚

两个模块必须以特定顺序执行代码。

时间内聚

模块基于时序依赖性而相关。例如,许多系统列出了一些看起来不相关的事物,必须在系统启动时对其进行初始化。这些不同的任务在时间上具有凝聚力。

逻辑内聚

模块内的数据在逻辑上相关,但在功能上无关。例如,考虑一个模块,该模块可以转换文本,序列化对象或流中的信息。操作是相关的,但功能却大不相同。实际上,每个Java项目都以StringUtils包的形式存在这种类型的内聚性的常见示例:一组可在其上操作String但彼此不相关的静态方法。

巧合内聚

模块中的元素除了在同一源文件中之外,不相关。这是凝聚力的最负面形式。

尽管列出了七个变体,但是内聚性是不如耦合性精确的度量。通常,特定模块的内聚程度由特定架构师决定。例如,考虑以下模块定义:

Customer Maintenance

add customer

update customer

get customer

notify customer

get customer orders

cancel customer orders

最后两个条目应该驻留在此模块中还是开发人员应该创建两个单独的模块,例如:

Customer Maintenance

add customer

update customer

get customer

notify customer

Order Maintenance

get customer orders 

cancel customer orders

哪一个划分是正确的结构?与往常一样,这取决于:

这是两个唯一的Order Maintenance吗?如果是这样话,将这些操作重归于Customer Maintenance是很有意义的。

Customer Maintenance是否会增长得更大,从而鼓励开发人员寻找机会来提取行为?

是否Order Maintenance需要大量的Customer信息知识便于理解,以至于分离两个模块需要高度耦合才能使其发挥作用?这与拉里·康斯坦丁的名言有关。

这些问题代表了软件架构师工作核心的一种权衡分析。

出乎意料的是,考虑到内聚性的主观性,计算机科学家已经开发出一种好的结构度量来确定内聚性(或更具体地说,缺乏内聚性)。同名作者开发了一套著名的度量,称为Chidamber和Kemerer面向对象的度量套件,以度量面向对象的软件系统的特定方面。该套件包括许多常见的代码度量标准,例如圈复杂度(请参阅“圈复杂度”)和“耦合”中讨论的几个重要的耦合度量。

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

公式3-1LCOM版本1

对于不访问特定共享字段的任何方法,P增大1,对于确实共享特定共享字段的方法,Q减小1。作者同情那些不理解这个公式的人。更糟糕的是,随着时间的流逝,它变得越来越复杂。公式3-2中出现了1996年引入的第二个变化形式(因此命名为LCOM96B)。

公式3-2 LCOM 96b

由于下面的书面说明更加清楚,因此我们无需理方程3-2中的变量和运算符。事实上,LCOM度量公开类之间的偶发耦合。这是LCOM的更好定义:

LCOM

未通过共享字段共享的方法集的总和。

考虑一个具有私有字段a和b的类。许多方法只能访问a,而许多其他方法只能访问b。未通过共享字段(和)共享的方法的总和很高;因此,该课程的LCOM评分较高,表明该方法缺乏内聚的情况下评分较高。考虑图3-1所示的三个类。

3-1LCOM度量的图示,其中字段是八边形,方法是正方形

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

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

许多软件指标存在严重缺陷,LCOM也无法幸免。所有这些度量可以发现的是结构上缺乏凝聚力。它无法从逻辑上确定特定部分是否组合在一起。这反映回来的软件架构的我们的第二定律:为什么比如何做更重要。

耦合

幸运的是,我们有更好的工具来分析代码库中的耦合,部分基于图论:由于方法调用和返回是以调用图的形式进行的,基于数学的分析成为可能。1979年,爱德华·尤登(Edward Yourdon)和拉里·康斯坦丁(Larry

Constantine)出版了《结构化设计:计算机程序和系统设计学科的基础》(Prentice-Hall),定义了许多核心概念,包括输入输出的度量标准。输入耦合度量到代码工件(组件,类,函数等)的输入连接数。输出耦合测与其他代码工件的连接。实际上,几乎所有平台工具都可以使架构师分析代码的耦合特性,以帮助重组,迁移或理解代码库。

为什么对耦合度量使用如此相似的名称?

为什么在架构世界中,代表相反概念的两个关键指标实际上被命名为同一事物,只是在听起来最相似的元音上有所不同?这些术语源自Yourdon和Constantine的结构化设计。他们从数学中借用了概念,创造了现在常见的输入和输出耦合术语,这些术语应称为输入和输出耦合。然而,由于最初的倾向于数学对称而不是清晰,开发者想出了几个助记符来帮助他们:在英语字母表中,a出现在e之前,对应于输入在输出之前,或者观察到输出中的字母eexit中的首字母相匹配,对应于输出连接

抽象性,不稳定性以及与主序列的距离

虽然组件耦合的原始值对架构师有价值,但其他几个衍生指标也可以进行更深入的评估。这些度量标准是由Robert Martin为一本C ++书籍创建的,但是广泛适用于其他面向对象的语言。

抽象性是抽象工件(抽象类,接口等)与具体工件(实现)的比率。它代表了抽象性与实现性的度量。例如,考虑一个没有抽象的代码库,只是一个巨大的单一代码功能(如在单个main()方法中)。另一面是具有太多抽象的代码库,这使开发人员难以理解事物之间的连接方式(例如,开发人员需要一段时间才能弄清楚如何处理AbstractSingletonProxyFactoryBean)。

抽象性公式出现在公式3-3中

公式3-3 抽象性

在等式中

 

用模块表示抽象元素(接口或抽象类),并且

模块表示具体元素(非抽象类)。此度量标准寻找相同的条件。可视化此指标的最简单方法:考虑一个具有5,000行代码的应用程序,并且全部采用一种main()方法。抽象分子为1,而分母为5,000,产生的抽象度几乎为0。因此,此度量标准度量代码中抽象率。

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

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

公式3-4不稳定性

在等式中

 表示输出耦合,并且

表示输入耦合。

不稳定性度量决定代码库的波动。由于高度耦合,表现出高度不稳定性的代码库在更ycbgq改时更容易破坏。例如,如果一个类调用许多其他类来委派工作,则当一个或多个被调用方法发生更改时,被调用的类很容易发生损坏。

主序列的距离

架构师为架构提供的少数整体指标之一是与主序列的距离,即基于不稳定性抽象性的衍生指标,如公式3-5所示。

公式3-5距主要序列的距离

D=|A+I−1|

在等式中,A= 抽象,I = 不稳定性

请注意,抽象性不稳定性都是比率,这意味着它们的结果将始终落在0和1之间。因此,在绘制关系图时,我们看到的是图3-2中的

3-2主要序列定义了抽象性与不稳定性之间的理想关系

所述主序列距离度量不可能是抽象性和不稳定性之间的理想关系; 接近理想线的类将这两个相互关注的问题很好地融合在一起。例如,绘制特定的类可以使开发人员计算距主序列度量的距离如图3-3所示

3-3特定类与主序列的归一化距离

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

3-4无用和痛苦的区域

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

指标的局限性

尽管行业中有一些代码级指标可提供对代码库的宝贵见解,但与其他工程学科的分析工具相比,我们的工具非常迟钝。即使直接从代码结构中得出的指标也需要解释。例如,循环复杂度(请参阅“循环复杂度”)可以测量代码库中的复杂度,但无法与基本复杂度(因为底层问题很复杂)或意外复杂度区分开来。(代码比实际的要复杂得多)。几乎所有代码级度量都需要解释,但是为关键度量(例如圈复杂度)建立基线仍然很有用,以便架构师可以评估它们所显示的类型。我们将在“治理和适应度函数”中讨论设置此类测试。

请注意,Edward Yourdon和Larry Constantine先前提到的书(《结构化设计:计算机程序和系统设计学科的基础》)早于面向对象语言的流行,而侧重于结构化的编程构造,例如函数(不是方法)。它还定义了其他类型的耦合,在此不做介绍,因为它们已被共生性取代了。

共生性

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

如果一个组件的变更需要修改另一个组件以保持系统的整体正确性,则两个组件是相邻的。

梅里尔·佩奇·琼斯

他开发了两种类型的共生性:静态动态

静态共生性

静态共生性是指源代码级的耦合(与执行时耦合相对,在“动态共生性”中进行了介绍);它是对结构化设计定义的输入和输出耦合的改进。换言之,架构师将以下类型的静态连接视为某种事物的耦合程度,无论是传入的还是有效的:

名称共生性(CoN

多个组件必须在实体名称上达成一致。

方法名表示代码基耦合的最常见方式,也是最理想的方法,尤其是在现代重构工具的情况下,这些工具使得系统范围内的名称更改变得微不足道。

类型共生性(CoT

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

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

含义共生性(CoM)或常识共生性(CoC

多个组件必须在特定值的含义上达成共识。

在代码库中这种类型的共生性最常见的情况是硬编码数字而不是常量。例如,在某些语言中,通常考虑在某处定义int TRUE = 1; int FALSE = 0。想象一下,如果有人放弃了这些价值观,就会出现问题。

职位共生性(CoP

多个实体必须就价值顺序达成一致。

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

算法共生性(CoA

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

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

动态共生性

Page-Jones定义的另一种共生性是动态共生性,它在运行时分析调用。以下是对动态共生性的不同类型的描述:

执行共生性(CoE

多个组件的执行顺序很重要。

考虑以下代码:

email = new Email();

email.setRecipient("foo@example.com");

email.setSender("me@me.com");

email.send();

email.setSubject("whoops");

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

时机共生性(CoT

执行多个组件的时间很重要。

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

价值共生性(CoV

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

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

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

身份共生性(CoI

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

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

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

共生性属性

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

强度

架构师通过开发人员重构这种耦合的容易程度来确定连接的强度;不同类型的连接显然更可取,如图3-5所示。架构师和开发人员可以通过重构以获得更好的连接类型来改善代码库的耦合特性。

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

3-5实力connascence提供了良好的重构指南

区域性

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

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

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

Page-Jones提供了三种使用共生性改善系统模块化的准则 :

通过将系统分解为封装的元素来最大程度地减少总体占用

最小化跨越封装边界的所有剩余内容

最大化封装边界内的共生性

富有传奇色彩的软件架构创新者Jim Weirich重新流行了共生性的概念,并提供了两条很好的建议:

等级规则:将强力的自闭症转化为弱势的自闭症

局部性规则:随着软件元素之间的距离增加,请使用较弱的形式

统一耦合和共生性度量

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

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

3-6统一耦合和连续

1990年代出生的问题

架构师在将这些有用的指标用于系统分析和设计时存在一些问题。首先,这些措施着眼于低级别代码的细节,侧重于代码质量和健壮性,而不是架构。架构师倾向于更关心如何模块耦合,而不是耦合。例如,架构师关心同步通信与异步通信,而不关心实现方式。

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

从模块到组件

我们将术语模块始终用作捆绑相关代码的通用名称。但是,大多数平台都支持某种形式的组件,这是软件架构师的关键组成部分之一。自计算机科学成立以来,就已经存在逻辑分离或物理分离的概念和相应的分析方法。然而,尽管所有关于组件和分离的写作和思考,开发人员和架构师仍在努力取得良好的成果。

我们将在第8章中讨论从问题域派生组件,但首先必须讨论软件架构的另一个基本方面:架构特征及其范围。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值