当前位置: 首页 > news >正文

【读书笔记】架构整洁之道 P4 组件构建原则

第4部分 组件构建原则

第12章 组件

设计良好的组件都应该永远保持可被独立部署的特性,这同时也意味着这些组件应该可以被单独开发

组件发展史

在早期的软件开发中,程序员可以完全掌控自己编写的程序所处的内存地址和存放格式。在那时,程序中的第一条语句被称为起源(origin)语句,它的作用是声明该程序应该被加载到的内存位置。

现在的程序员一般不需要考虑程序要加载的内存地址。但这的确是早期程序员们在编程初期就要做的一个重要决策,因为当时的程序基本不能被重定位(relocate)。

时是如何调用库函数呢?上述代码演示了具体调用过程。程序员们需要将所有要调用的库函数源代码包含到自己的程序代码中,然后再进行整体编译[1]。库函数文件都是以源代码而非二进制的形式保存的。

库函数越多,编译就越慢。大型程序的编译过程经常需要几个小时。

为了缩短编译时间,程序员们改将库函数的源代码单独编译。而库函数的源代码在单独编译后会被加载到一个指定位置,比如地址2000(八进制)。然后,编译器会针对该库文件创建一个符号表(symbol table),并将其和应用程序代码编译在一起。当程序运行时,它会先加载二进制形式的库文件[2],再加载编译过的应用程序,其内存布局如图12.1所示。

图12.1:早期程序的内存布局

当然,只要应用程序的代码能够完全存放在地址0000~1777(八进制)内,这种组织方式就没有任何问题。但是,应用程序代码的大小很快就会超出这个范围。为了解决这个问题,程序员们必须将应用程序代码切分成两个不同的地址段,以跳过库函数存放的内存范围(具体如图12.2所示)。

图12.2 将应用程序内存地址切分成两段

很显然,这种方案也是不可持续的。因为随着函数库中函数的增加,它的大小也随之增加,我们同样也需要为此划分新的区域,譬如在上述例子中,我们需要在7000(八进制)左右的位置往后追加地址空间。这样一来,程序和函数库的碎片化程度会随着计算机内存的增加而不断增加。

重定位技术

该解决方案就是生成可重定位的二进制文件。其背后的原理非常简单,即程序员修改编译器输出文件的二进制格式,使其可以由一个智能加载器加载到任意内存位置。当然,这需要我们在加载器启动时为这些文件指定要加载到的内存地址,而且可重定位的代码中还包含了一些记号,加载器将其加载到指定位置时会修改这些记号对应的地址值。一般来说,这个过程只不过就是将二进制文件中包含的内存地址都按照其加载到的内存基础位置进行递增。

除此之外,程序员们还对编译器做了另外一个修改,就是在可重定位二进制文件中将函数名输出为元数据并存储起来。这样一来,如果一段程序调用了某个库函数,编译器就会将这个函数的名字输出为外部引用(external reference),而将库函数的定义输出为外部定义(external definition)。加载器在加载完程序后,会将外部引用和外部定义链接(link)起来

这就是**链接加载器(linking loader)**的由来。

链接器

链接加载器让程序员们可以将程序切分成多个可被分别编译、加载的程序段。在程序规模较小、外部链接也较少的情况下,这个方案一直都很好用。然而在20世纪60年代末期到70年代初期的那段时间里,程序的规模突然有了大幅的增长,情况就有所不同了。

显然在这种情况下,链接加载器的处理过程实在是太慢了。且不说函数库当时还存储在磁带卷这样的低速存储设备上,即使是存储在磁盘上,其存取速度也是很慢的。毕竟,链接加载器在加载处理过程中必须要读取几十个甚至几百个二进制库文件来解析外部引用。

程序员们只能将加载过程和链接过程也进行分离。他们将耗时较长的部分——链接部分——放到了一个单独的程序中去进行,这个程序就是所谓的链接器(linker)。链接器的输出是一个已经完成了外部链接的、可以重定位的二进制文件,这种文件可以由一个支持重定位的加载器迅速加载到内存中。这使得程序员可以用缓慢的链接器生产出可以很快进行多次加载的可执行文件。

时间继续推移到了20世纪80年代,程序员们在那时已经用上了C这样的高级编程语言,程序的规模也得到了进一步的扩大,源代码行数超过几十万行在当时已经是很普遍的事了。

于是,源代码模块会从.c文件被编译成.o文件,然后再由链接器创建出可被快速加载的可执行文件。那时,虽然编译每个单独模块的速度相对较快,但所有模块的累计编译时间较长,链接过程则耗时更久,整个修改编译周期经常会超过数个小时。

到了20世纪90年代中期,链接速度的提升速度已经远远超过了程序规模的增长速度。在大部分情况下,程序链接的时间已经降低到秒级。这对一些小程序来说,即使使用链接加载器也是可以接受的了。

第13章 组件聚合

那么,究竟是哪些类应该被组合成一个组件呢?在本章中,我们会具体讨论以下三个与构建组件相关的基本原则:

  • REP:复用/发布等同原则。
  • CCP:共同闭包原则。
  • CRP:共同复用原则。

复用/发布等同原则

软件复用的最小粒度应等同于其发布的最小粒度。

REP原则初看起来好像是不言自明的。毕竟如果想要复用某个软件组件的话,一般就必须要求该组件的开发由某种发布流程来驱动,并且有明确的发布版本号。

从软件设计和架构设计的角度来看,REP原则就是指组件中的类与模块必须是彼此紧密相关的。也就是说,一个组件不能由一组毫无关联的类和模块组成,它们之间应该有一个共同的主题或者大方向

但从另外一个视角来看,这个原则就没那么简单了。因为根据该原则,一个组件中包含的类与模块还应该是可以同时发布的。这意味着它们共享相同的版本号与版本跟踪,并且包含在相同的发行文档中,这些都应该同时对该组件的作者和用户有意义。

这层建议听起来就比较薄弱了,毕竟说某项事情的安排应该“合理”的确有点假大空,不着实际。该建议薄弱的原因是它没有清晰地定义出到底应该如何将类与模块组合成组件。

共同闭包原则

我们应该**将那些会同时修改,并且为相同目的而修改的类放到同一个组件中,而将不会同时修改,并且不会为了相同目的而修改的那些类放到不同的组件中**。

这其实是SRP原则在组件层面上的再度阐述。正如SRP原则中提到的“一个类不应该同时存在着多个变更原因”一样,CCP原则也认为一个组件不应该同时存在着多个变更原因。

对大部分应用程序来说,可维护性的重要性要远远高于可复用性。如果某程序中的代码必须要进行某些变更,那么这些变更最好都体现在同一个组件中,而不是分布于很多个组件中[4]。因为如果这些变更都集中在同一个组件中,我们就只需要重新部署该组件,其他组件则不需要被重新验证、重新部署了。

另外,CCP原则和开闭原则(OCP)也是紧密相关的。CCP讨论的就是OCP中所指的“闭包”。OCP原则认为一个类应该便于扩展,而抗拒修改。

如前所述,CCP原则实际上就是SRP原则的组件版。在SRP原则的指导下,我们将会把变更原因不同的函数放入不同的类中。而CCP原则指导我们应该将变更原因不同的类放入不同的组件中。简而言之,这两个原则都可以用以下一句简短的话来概括:将由于相同原因而修改,并且需要同时修改的东西放在一起。将由于不同原因而修改,并且不同时修改的东西分开

共同复用原则

不要强迫一个组件的用户依赖他们不需要的东西。

共同复用原则(CRP)是另外一个帮助我们决策类和模块归属于哪一个组件的原则。该原则建议我们将经常共同复用的类和模块放在同一个组件中。

一个简单的例子就是容器类与其相关的遍历器类,这些类之间通常是紧密相关的,一般会被共同复用,因此应该被放置在同一个组件中。

因此,当我们决定要依赖某个组件时,最好是实际需要依赖该组件中的每个类。换句话说,我们希望组件中的所有类是不能拆分的,即不应该出现别人只需要依赖它的某几个类而不需要其他类的情况。否则,我们后续就会浪费不少时间与精力来做不必要的组件部署。

因此在CRP原则中,关于哪些类不应该被放在一起的建议是其更为重要的内容。简而言之,CRP原则实际上是在指导我们:不是紧密相连的类不应该被放在同一个组件里

与ISP原则的关系——CRP原则实际上是ISP原则的一个普适版。ISP原则是建议我们不要依赖带有不需要的函数的类,而CRP原则则是建议我们不要依赖带有不需要的类的组件。

组件聚合张力图

读到这里,读者可能已经意识到上述三个原则之间彼此存在着竞争关系。REP和CCP原则是黏合性原则,它们会让组件变得更大,而CRP原则是排除性原则,它会尽量让组件变小。软件架构师的任务就是要在这三个原则中间进行取舍。

下面我们来看一下图13.1。这是一张组件聚合三大原则的张力图[5],图的边线所描述的是忽视对应原则的后果。

图13.1:组件聚合原则的张力图

图13.1:组件聚合原则的张力图

一般来说,一个软件项目的重心会从该三角区域的右侧开始,先期主要牺牲的是复用性。然后,随着项目逐渐成熟,其他项目会逐渐开始对其产生依赖,项目重心就会逐渐向该三角区域的左侧滑动。换句话说,一个项目在组件结构设计上的重心是根据该项目的开发时间和成熟度不断变动的,我们对组件结构的安排主要与项目开发的进度和它被使用的方式有关,与项目本身功能的关系其实很小。

第14章 组件耦合

接下来要讨论的三条原则主要关注的是组件之间的关系。在这些原则中,我们同样会面临着研发能力和逻辑设计之间的冲突。

无依赖环原则

组件依赖关系图中不应该出现环。

我们一定都有过这样的经历:当你花了一整天的时间,好不容易搞定了一段代码,第二天上班时却发现这段代码莫名其妙地又不能工作了。这通常是因为有人在你走后修改了你所依赖的某个组件。在过去几十年中,针对这个问题逐渐演化出了两种解决方案,它们都来自电信行业。第一种是“每周构建”,第二种是“无依赖环原则(ADP)”。

  • 每周构建:在每周的前四天中,让所有的程序员在自己的私有库上工作,忽略其他人的修改,也不考虑互相之间的集成问题;然后在每周五要求所有人将自己所做的变更提交,进行统一构建。
  • 消除循环依赖:将研发项目划分为一些可单独发布的组件,这些组件可以交由单人或者某一组程序员来独立完成。当有人或团队完成某个组件的某个版本时,他们就会通过发布机制通知其他程序员,并给该组件打一个版本号,放入一个共享目录。这样一来,每个人都可以依赖于这些组件公开发布的版本来进行开发,而组件开发者则可以继续去修改自己的私有版本。

但是,如果想要成功推广这个开发流程,就必须控制好组件之间的依赖结构,绝对不能允许该结构中存在着循环依赖关系。

下面让我们来看看图14.1,该图展示了一个典型应用程序的组件结构。不管我们从该图中的哪个节点开始,都不能沿着这些代表了依赖关系的边最终走回到起始点。也就是说,这种结构中不存在环,我们称这种结构为有向无环图(Directed Acyclic Graph,简写为DAG)。

图14.1:典型的组件结构图

现在,如果负责Presenters组件的团队需要发布一个新版本,我们就应该很容易判断出哪些组件会受这个变更的影响——只需要按其依赖关系反向追溯即可。另外值得注意的是,当Main组件发布新版本时,它对系统中的其他组件根本就没有影响,既没有一个组件依赖于Main,也就没有人需要关心Main组件上发生的变更。

假设某个新需求使我们修改了Entities组件中的某个类,而这个类又依赖于Authorizer 组件中的某个类。例如,Entities 组件中的 User 类使用了Authorizer组件中的Permissions类。这就形成了一个循环依赖关系,如图14.2所示。

图14.2:循环依赖

当组件结构依赖图中存在循环依赖时,想要按正确的顺序构建组件几乎是不可能的。这种依赖关系将会在Java这种需要在编译好的二级制文件中读取声明信息的语言中导致一些非常棘手的问题。

打破循环依赖

  1. 应用依赖反转原则(DIP):在图14.3中,我们可以创建一个User类需要使用的接口,然后将这个接口放入Entities组件,并在Authorizer组件中继承它。这样就将Entities与Authorizer之间的依赖关系反转了,自然也就打破了循环依赖关系。

图14.3:将Entities与Authorizer之间的依赖关系反转

  1. 创建一个新的组件,并让Entities与Authorize这两个组件都依赖于它。将现有的这两个组件中互相依赖的类全部放入新组件(如图14.4所示)。

图14.4:让Entities与Authorizer共同依赖于一个新组件

自上而下的设计

根据上述讨论,我们可以得出一个无法逃避的结论:组件结构图是不可能自上而下被设计出来的。它必须随着软件系统的变化而变化和扩张,而不可能在系统构建的最初就被完美设计出来

有些读者可能会觉得这个结论有些反直觉。人们通常会直观地认为,代表项目粗粒度的结构单元,也就是组件,应该与顶层设计中的功能单元是相对应的。

同样的,人们也普遍认为项目粗粒度的组件分组规则所产生的就是组件的依赖结构,也应该在某种程度上与项目的系统功能分解的结果相互对应。但是很明显,组件依赖关系图其实不具备这样的属性。

事实上,组件依赖结构图并不是用来描述应用程序功能的,它更像是应用程序在构建性与维护性**方面的一张地图。这就是组件的依赖结构图不能在项目的开始阶段被设计出来的原因。除此之外,我们还希望将项目变更所影响的范围被限制得越小越好,因此需要应用单一职责原则(SRP)和共同闭包原则(CCP)来将经常同时被变更的类聚合在一起**。

组件结构图中的一个重要目标是指导如何隔离频繁的变更。我们不希望那些频繁变更的组件影响到其他本来应该很稳定的组件

另外,随着应用程序的增长,创建可重用组件的需要也会逐渐重要起来。这时CRP又会开始影响组件的组成。最后当循环依赖出现时,随着无循环依赖原则(ADP)的应用,组件依赖关系会产生相应的抖动和扩张。

如果我们在设计具体类之前就来设计组件依赖关系,那么几乎是必然要失败的。因为在当下,我们对项目中的共同闭包一无所知,也不可能知道哪些组件可以复用,这样几乎一定会创造出循环依赖的组件。因此,组件依赖关系是必须要随着项目的逻辑设计一起扩张和演进的。

稳定依赖原则

依赖关系必须要指向更稳定的方向。

设计这件事不可能是完全静止的,如果我们要让一个设计是可维护的,那么其中某些部分就必须是可变的。通过遵守共同闭包原则(CCP),我们可以创造出对某些变更敏感,对其他变更不敏感的组件。这其中的一些组件在设计上就已经是考虑了易变性,预期它们会经常发生变更的。

任何一个我们预期会经常变更的组件都不应该被一个难于修改的组件所依赖,否则这个多变的组件也将会变得非常难以被修改。

通过遵守稳定依赖原则(SDP),我们就可以确保自己设计中那些容易变更的模块不会被那些难于修改的组件所依赖。

稳定性

让软件组件难于修改的一个最直接的办法就是让很多其他组件依赖于它。带有许多入向依赖关系的组件是非常稳定的,因为它的任何变更都需要应用到所有依赖它的组件上。

在图14.5中,X是一个稳定的组件。因为有三个组件依赖着X,所以X有三个不应该被修改的原因。这里就说X要对三个组件负责。另一方面,X不依赖于任何组件,所以不会有任何原因导致它需要被变更,我们称它为“独立”组件。

图14.5:X,稳定的组件

下面再来看看图14.6中的Y组件,这是一个非常不稳定的组件。由于没有其他的组件依赖Y,所以Y并不对任何组件负责。但因为Y同时依赖于三个组件,所以它的变更就可能由三个不同的源产生。这里就说Y是有依赖性的组件。

图14.6:Y,一个非常不稳定的组件

稳定性指标

那么,究竟该如何来量化一个组件的稳定性呢?其中一种方法是计算所有入和出的依赖关系。通过这种方法,我们就可以计算出一个组件的位置稳定性(positional stability)。

  • Fan-in:入向依赖,这个指标指代了组件外部类依赖于组件内部类的数量。
  • Fan-out:出向依赖,这个指标指代了组件内部类依赖于组件外部类的数量。

I:不稳定性,I=Fan-out/(Fan-in+Fan-out)。该指标的范围是[0,1],I=0意味着组件是最稳定的,I=1意味着组件是最不稳定的。在这里,Fan-in和Fan-out这两个指标[6]

是通过统计和组件内部类有依赖的组件外部类的数量来计算的,具体如图14.7所示。

图14.7:我们的例子

在这里,我们想要计算组件Cc的稳定性指标,可以观察到有3个类在Cc外部,它们都依赖于Cc内部的类,因此Fan-in=3。此外,Cc中的一个类也依赖于组件外部的类,因此Fan-out=1,I=1/4。

在C++中,这些依赖关系一般是通过#include语句来表达的。事实上,当每个源文件只包含一个类的时候,I指标是最容易计算的。同样在Java中,I指标也可以通过Import语句和全引用名字的数量来计算。

当I指标等于1时,这是组件最不稳定的一种情况,我们认为这种组件是“不负责的(irresponsible)、对外依赖的(dependent)”。由于这个组件没有被其他组件依赖,所以自然也就没有力量会干预它的变更,同时也因为该组件依赖于其他组件,所以就必然会经常需要变更。

相反,当I=0的时候,我们通常认为这样的组件是“负责的(responsibile)、不对外依赖的(independent)”。这是组件最具稳定性的一种情况,其他组件对它的依赖关系会导致这个组件很难被变更,同时由于它没有对外依赖关系,所以不会有来自外部的变更理由。

稳定依赖原则(SDP)的要求是让每个组件的I指标都必须大于其所依赖组件的I指标。也就是说,组件结构依赖图中各组件的I指标必须要按其依赖关系方向递减

并不是所有组件都应该是稳定的

如果一个系统中的所有组件都处于最高稳定性状态,那么系统就一定无法再进行变更了,这显然不是我们想要的。事实上,我们设计组件架构图的目的就是要决定应该让哪些组件稳定,让哪些组件不稳定。譬如在图14.8中,我们所示范的就是一个具有三个组件的系统的理想配置。

在该系统组件结构图中,可变更的组件位于顶层,同时依赖于底层的稳定组件。将不稳定组件放在该结构图的顶层是很有用的,因为这样我们就可以很容易地找出箭头向上的依赖关系,而这些关系是违反SDP(以及后面将会讨论的ADP)的。

图14.8:一个具有三个组件的系统的理想配置

下面再通过图14.9来看看违反SDP的情况:

图14.9:违反SDP的情况

在图14.9中,Flexible是在设计中要确保其易于变更的组件,因此我们会希望Flexible是不稳定的。然而,Stable组件的开发人员却引入了对Flexible组件的依赖。这种情况就违反了SDP,因为Stable组件的I指标要远小于Flexible的I指标。这将导致Flexible组件的变更难度大大增加,因为对Flexible组件的任何修改都必须要考虑Stable组件及该组件自身存在的依赖关系。

如果想要修复这个问题,就必须要将Stable与Flexible这两个组件之间的依赖关系打破。

图14.10:Stable组件中的U使用了Flexible组件中的C

我们可以利用DIP来修复这个问题。具体来说就是创造一个UServer组件,并在其中设置一个US接口类。然后,确保这个接口类中包含了所有U需要使用的函数,再让C实现这个接口,如图14.11所示。这样一来,我们就将从Stable到Flexible的这条依赖关系打破了,强迫这两个组件都依赖于UServer。现在,UServer组件会是非常稳定的(I=0),而Flexibile组件则会依然保持不稳定的状态(I=1),结构图中所有的依赖关系都流向I递减的方向了。

图14.11:让C实现接口类US

抽象组件

读者可能会觉得创造新组件(譬如上述例子中的UService组件,它其实只包含了一个接口类)这种做法挺奇怪的。因为这样的组件中几乎不包含任何可执行的代码!但事实上,这种做法在C#或者Java这种静态类型语言中是非常普遍的,而且也必须这样做。因为这些抽象组件通常会非常稳定,可以被那些相对不稳定的组件依赖。

而当我们使用Ruby和Python这种动态类型语言时,这些抽象接口事实上并不存在,因此也就没有对它们的依赖。动态类型语言中的依赖关系是非常简单的,因为其依赖反转的过程并不需要声明和继承接口

稳定抽象原则

一个组件的抽象化程度应该与其稳定性保持一致。

高阶策略应该放在哪里

在一个软件系统中,总有些部分是不应该经常发生变更的。这些部分通常用于表现该系统的高阶架构设计及一些策略相关的高阶决策。我们不想让这些业务决策和架构设计经常发生变更,因此这些代表了系统高阶策略的组件应该被放到稳定组件(I=0)中,而不稳定的组件(I=1)中应该只包含那些我们想要快速和方便修改的部分。

然而,如果我们将高阶策略放入稳定组件中,那么用于描述那些策略的源代码就很难被修改了。这可能会导致整个系统的架构设计难于被修改。如何才能让一个无限稳定的组件(I=0)接受变更呢?开闭原则(OCP)为我们提供了答案。这个原则告诉我们:创造一个足够灵活、能够被扩展,而且不需要修改的类是可能的,而这正是我们所需要的。哪一种类符合这个原则呢?答案是抽象类。

稳定抽象原则简介

稳定抽象原则(SAP)为组件的稳定性与它的抽象化程度建立了一种关联。一方面,该原则要求稳定的组件同时应该是抽象的,这样它的稳定性就不会影响到扩展性。另一方面,该原则也要求一个不稳定的组件应该包含具体的实现代码,这样它的不稳定性就可以通过具体的代码被轻易修改。

因此,如果一个组件想要成为稳定组件,那么它就应该由接口和抽象类组成,以便将来做扩展。如此,这些既稳定又便于扩展的组件可以被组合成既灵活又不会受到过度限制的架构。

将SAP与SDP这两个原则结合起来,就等于组件层次上的DIP。因为SDP要求的是让依赖关系指向更稳定的方向,而SAP则告诉我们稳定性本身就隐含了对抽象化的要求,即依赖关系应该指向更抽象的方向。

衡量抽象化程度

下面,假设A指标是对组件抽象化程度的一个衡量,它的值是组件中抽象类与接口所占的比例。那么:

  • Nc:组件中类的数量。
  • Na:组件中抽象类和接口的数量。
  • A:抽象程度,A=Na÷Nc。

A指标的取值范围是从0到1,值为0代表组件中没有任何抽象类,值为1就意味着组件中只有抽象类。

主序列

现在,我们可以来定义组件的稳定性I与其抽象化程度A之间的关系了,具体如图14.12所示。在该图中,纵轴为A值,横轴为I值。如果我们将两个“设计良好”的组件绘制在该图上,那么最稳定的、包含了无限抽象类的组件应该位于左上角(0,1),最不稳定的、最具体的组件应该位于右下角(1,0)。

当然,不可能所有的组件都能处于这两个位置上,因为组件通常都有各自的稳定程度和抽象化程度。例如一个抽象类有时会衍生于另一个抽象类,这种情况是很常见的,而这个衍生过程就意味着某种依赖关系的产生。因此,虽然该组件是全抽象的,但它并不是完全稳定的,上述依赖关系降低了它的稳定程度。

既然不能强制要求所有的组件都处于(0,1)和(1,0)这两个位置上,那么就必须假设A/I图上存在着一个合理组件的区间。而这个区间应该可以通过排除法推导出来,也就是说,我们可以先找出那些组件不应该处于的位置(请参考图14.13)。

图14.13:排除区

痛苦区

在图14.13中,假设某个组件处于(0,0)位置,那么它应该是一个非常稳定但也非常具体的组件。这样的组件在设计上是不佳的,因为它很难被修改,这意味着该组件不能被扩展。这样一来,因为这个组件不是抽象的,而且它又由于稳定性的原因变得特别难以被修改,我们并不希望一个设计良好的组件贴近这个区域,因此(0,0)周围的这个区域被我们称为痛苦区(zone of pain)。

当然,有些软件组件确实会处于这个区域中,这方面的一个典型案例就是数据库的表结构(schema)它在可变性上可谓臭名昭著,但是它同时又非常具体,并被非常多的组件依赖。这就是面向对象应用程序与数据库之间的接口这么难以管理,以及每次更新数据库的过程都那么痛苦的原因

另一个会处于这个区域的典型软件组件是工具型类库。虽然这种类库的I指标为1,但事实上通常是不可变的。例如String组件,虽然其中所有的类都是具体的,但由于它被使用得太过普遍,任何修改都会造成大范围的混乱,因此String组件只能是不可变的。

这里可能有笔误,工具类库I指标应该接近0

不可变组件落在(0,0)这一区域中是无害的,因为它们不太可能会发生变更。正因为如此,只有多变的软件组件落在痛苦区中才会造成麻烦,而且组件的多变性越强,造成的麻烦就会越大。其实,我们应该将多变性作为图14.13的第三个轴,这时图14.13所展示的便是多变性=1时的情况,也就是最痛苦的切面。

无用区

现在我们来看看靠近(1,1)这一位置点的组件。该位置上的组件不会是我们想要的,因为这些组件通常是无限抽象的,但是没有被其他组件依赖,这样的组件往往无法使用。因此我们将这个区域称为无用区。

对于这个区域中的软件组件来说,其源码或者类中的设计问题通常是由于历史原因造成的。例如我们常常会在系统的某个角落里看到某个没有人实现的抽象类,它们一直静静地躺在那里,没有人使用。

避开这两个区域

很明显,最多变的组件应该离上述两个区域越远越好。在图14.13中,我们可以将距离两个区域最远的点连成一条线,即从(1,0)连接到(0,1)。我将这条线称为主序列线(mainsequence)。[7]

坐落于主序列线上的组件不会为了追求稳定性而被设计得“太过抽象”,也不会为了避免抽象化而被设计得“太过不稳定”。这样的组件既不会特别难以被修改,又可以实现足够的功能。对于这些组件来说,通常会有足够多的组件依赖于它们,这使得它们会具有一定程度的抽象,同时它们也依赖了足够多的其他组件,这又使得它一定会包含很多具体实现。

在整条主序列线上,组件所能处于最优的位置是线的两端。一个优秀的软件架构师应该争取将自己设计的大部分组件尽可能地推向这两个位置。然而,以我的个人经验来说,大型系统中的组件不可能做到完全抽象,也不可能做到完全稳定。所以我们只要追求让这些组件位于主序列线上,或者贴近这条线即可。

离主序列线的距离

接下来介绍最后一个指标:如果让组件位于或者靠近主序列是可取的目标,那么我们就可以创建一个指标来衡量一个组件距离最佳位置的距离。

D指标[8]:距离D=|A+I-1|,该指标的取值范围是[0,1]。值为0意味着组件是直接位于主序列线上的,值为1则意味着组件在距离主序列最远的位置。

通过计算每个组件的D指标,就可以量化一个系统设计与主序列的契合程度了。另外,我们也可以用D指标大于0多少来指导组件的重构与重新设计。

在图14.14中,我们可以看到大部分的组件都位于主序列附近,但是有些组件处于平均值的标准差(Z=1)以外。这些组件值得被重点分析,它们要么过于抽象但依赖不足,要么过于具体而被依赖得太多。

图14.14:组件分散图

D指标的另外一种用法是按时间来跟踪每个组件的值,下面用图14.15来做一个示范。在该图中可以看到,Payroll组件在最近几次发布中累积了一些意外的对外依赖。图中的D=0.1是组件的达标红线,R2.1这个值已经超出了红线范围,这就告诉我们现在值得花一些精力来找出这个组件偏离主序列线的原因了。

图14.15:针对单组件的D值时间趋势图

http://www.dtcms.com/a/411413.html

相关文章:

  • (20)ASP.NET Core2.2 EF创建模型(必需属性和可选属性、最大长度、并发标记、阴影属性)
  • 优化软件哪个好优化技术
  • 【Python】微博超话一键签到工具
  • 关于网站推广wordpress啥时候出现的
  • 软件测试面试八股文:测试技术 10 大核心考点(二)
  • 雷达目标跟踪中扩展卡尔曼滤波(EKF)算法matlab实现
  • 初识网络:网络基础
  • 济南快速网站制作公司百度秒收录的网站
  • GitHub 热榜项目 - 日榜(2025-09-26)
  • 网站开发做什么简单制作表白网站
  • Java面试宝典:网络协议与Netty一
  • LinuxWindows环境下Nacos3.1.0详细安装配置指南:从零到生产就绪
  • 微信微网站 留言板网络营销的模式主要有
  • 实战训练1笔记
  • 网站制作程序下载ngo网页模板下载
  • C++学习记录(13)二叉排序树
  • TongWeb下如何获取数据源的物理连接?
  • 保险资料网站有哪些三网合一网站建设报价
  • 网站建设系统分析ai的优点和缺点
  • 三网合一网站百度一下免费下载
  • 坤驰科技携数据采集解决方案,亮相中国光纤传感大会
  • 可以做免费的网站吗广州平面设计工作室
  • 【文献阅读】基于机器学习的网络最差鲁棒性可扩展快速评估框架
  • 【复习】计网每日一题--PPP协议透明传输
  • 【训练技巧】torch.amp.GradScaler 里面当scale系数为0或者非常小的时候,详细分析与解决思路
  • 一站式服务logo设计深圳网站建设服务商哪些好?
  • 专业的网站建设公司电话做商城网站要什么手续
  • mdBook 开源笔记
  • 【1、Kotlin 基础语法】2、Kotlin 变量
  • TorchV知识库安全解决方案:基于智能环境感知的动态权限控制