架构整洁之道笔记 第二篇 组件构建原则

相关文章

整洁架构之道笔记 第一篇 编程范式及单体模块设计原则-CSDN博客

        上一篇总结到了SOLID 模块设计原则,这一篇我们将进行进阶版组件构建原则及软件架构设计原则的总结

组件构建原则

       如果说SOLID原则是用于指导我们如何将砖块砌成墙与房间的,那么组件构建原则就是用来指导我们如何将这些房间组合成房子的。

什么是组件

       组件是软件的部署单元,是整个软件系统在部署过程中可以独立完成部署的最小实体。

      我们可以将多个组件链接成一个独立可执行文件,也可以将它们汇总成类似.war文件这样的部署单元,又或者,组件也可以被打成.jar、.dll或者.exe文件,并以可动态加载的插件形式来独立部署。但无论采用哪种部署形式,设计良好的组件都应该永远保持可被独立部署的特性,这同时也意味着这些组件应该可以被单独开发。

组件的由来

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

       当时是如何调用库函数呢?程序员们需要将所有要调用的库函数源代码包含到自己的程序代码中,然后再进行整体编译。库函数文件都是以源代码而非二进制的形式保存的。在那个年代,存储设备十分缓慢,而内存则非常昂贵,也非常有限。编译器在编译程序的过程中需要数次遍历整个源代码。由于内存非常有限,驻留所有的源代码是不现实的,编译器只能多次从缓慢的存储设备中读取源代码。这样做是十分耗时的——库函数越多,编译就越慢。大型程序的编译过程经常需要几个小时。

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

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

        从20世纪80年代开始,因为磁盘的物理尺寸一直在不断缩小,速度在不断提高,同时内存的造价也一直在不断降低,以至于大部分存放在磁盘上的数据都可以被缓存在内存中了。而计算机时钟频率则从1MHz上升到了100MHz。

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

       与此同时,编程领域中还诞生了Active-X、共享库、.jar文件等组件形式。由于计算与存储速度的大幅提高,我们又可以在加载过程中进行实时链接了,链接几个.jar文件或是共享库文件通常只需要几秒钟时间,由此,插件化架构也就随之诞生了。

       如今,我们用.jar文件、DLL文件和共享库方式来部署应用的插件已经非常司空见惯了。如果现在我们想要给Minecraft增加一个模块,只需要将.jar文件放到一个指定的目录中即可。同样的,如果你想给Visual Studio增加Resharper插件,也只需要安装对应的DLL文件即可。

       会在程序运行时插入某些动态链接文件,这些动态链接文件所使用的就是软件架构中的组件概念。在经历多年的演进之后,组件化的插件式架构已经成为我们习以为常的软件构建形式了。

单组件构成原则

       究竟是哪些类应该被组合成一个组件呢?这是一个非常重要的设计决策,应该遵循优秀的软件工程经验来行事。但不幸的是,很多年以来,我们对于这么重要的决策经常是根据当下面临的实际情况临时拍脑门决定的。

        三个与构建组件相关的基本原则:

        REP:复用/发布等同原则。

        CCP:共同闭包原则。

        CRP:共同复用原则。

REP:复用/发布等同原则

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

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

       这其中的一个原因是,如果没有设定版本号,我们就没有办法保证所有被复用的组件之间能够彼此兼容。另外更重要的一点是,软件开发者必须要能够知道这些组件的发布时间,以及每次发布带来了哪些变更。

       只有这样,软件工程师才能在收到相关组件新版本发布的通知之后,依据该发布所变更的内容来决定是继续使用旧版本还是做些相应的升级,这是很基本的要求。因此,组件的发布过程还必须要能够产生适当的通知和发布文档,以便让它的用户根据这些信息做出有效的升级决策。

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

CCP:共同闭包原则

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

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

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

       CCP原则和开闭原则(OCP)也是紧密相关的。CCP讨论的就是OCP中所指的“闭包”。OCP原则认为一个类应该便于扩展,而抗拒修改。由于100%的闭包是不可能的,所以我们只能战略性地选择闭包范围。在设计类的时候,我们需要根据历史经验和预测能力,尽可能地将需要被一同变更的那些点聚合在一起。

       对于CCP,我们还可以在此基础上做进一步的延伸,即可以将某一类变更所涉及的所有类尽量聚合在一处。这样当此类变更出现时,我们就可以最大限度地做到使该类变更只影响到有限的相关组件。

       总而言之,CCP的主要作用就是提示我们要将所有可能会被一起修改的类集中在一处。

CRP:共同复用原则

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

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

       通常情况下,类很少会被单独复用。更常见的情况是多个类同时作为某个可复用的抽象定义被共同复用。CRP原则指导我们将这些类放在同一个组件中,而在这样的组件中,我们应该预见到会存在着许多互相依赖的类。

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

       但是CRP的作用不仅是告诉我们应该将哪些类放在一起,更重要的是要告诉我们应该将哪些类分开。因为每当一个组件引用了另一个组件时,就等于增加了一条依赖关系。虽然这个引用关系仅涉及被引用组件中的一个类,但它所带来的依赖关系丝毫没有减弱。也就是说,引用组件已然依赖于被引用组件了。

        由于这种依赖关系的存在,每当被引用组件发生变更时,引用它的组件一般也需要做出相应的变更。即使它们不需要进行代码级的变更,一般也免不了需要被重新编译、验证和部署。哪怕引用组件根本不关心被引用组件中的变更,也要如此。

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

      CRP原则实际上是ISP原则(接口隔离原则)的一个普适版。ISP原则是建议我们不要依赖带有不需要的函数的类,而CRP原则则是建议我们不要依赖带有不需要的类的组件。

组件构成原则

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

      只关注REP和CRP的软件架构师会发现,即使是简单的变更也会同时影响到许多组件。相反,如果软件架构师过于关注CCP和REP,则会导致很多不必要的发布。

        优秀的软件架构师应该能在上述三角张力区域中定位一个最适合目前研发团队状态的位置,同时也会根据时间不停调整。例如在项目早期,CCP原则会比REP原则更重要,因为在这一阶段研发速度比复用性更重要。

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

多组件间耦合

接下来要讨论的三条原则主要关注的是组件之间的关系。

无依赖环原则

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

       当你花了一整天的时间,好不容易搞定了一段代码,第二天上班时却发现这段代码莫名其妙地又不能工作了。这通常是因为有人在你走后修改了你所依赖的某个组件

       当一个大型研发项目存在多个组件时,推荐的解决办法是将研发项目划分为一些可单独发布的组件,这些组件可以交由单人或者某一组程序员来独立完成。当有人或团队完成某个组件的某个版本时,他们就会通过发布机制通知其他程序员,并给该组件打一个版本号,放入一个共享目录。这样一来,每个人都可以依赖于这些组件公开发布的版本来进行开发,而组件开发者则可以继续去修改自己的私有版本。

       每当一个组件发布新版本时,其他依赖这个组件的团队都可以自主决定是否立即采用新版本。若不采用,该团队可以选择继续使用旧版组件,直到他们准备好采用新版本为止。

        这样就不会出现团队之间相互依赖的情况了。任何一个组件上的变更都不会立刻影响到其他团队。每个团队都可以自主决定是否立即集成自己所依赖组件的新版本。更重要的是,这种方法使我们的集成工作能以一种小型渐进的方式来进行。程序员们再也不需要集中在一起,统一集成相互的变更了。

       如你所见,上述整个过程既简单又很符合逻辑,因而得到了各个研发团队的广泛采用。但是,如果想要成功推广这个开发流程,就必须控制好组件之间的依赖结构,绝对不能允许该结构中存在着循环依赖关系。

        我们可以打破这些组件中的循环依赖,并将其依赖图转化为有向无环图(Directed Acyclic Graph,简写为DAG)。目前有以下两种主要机制可以做到这件事情。

        1. 应用依赖反转原则(DIP)

        2.创建一个新的组件,并让相互依赖的组件都依赖于它。将现有的多个组件中互相依赖的类全部放入新组件

       组件依赖结构图并不是用来描述应用程序功能的,它更像是应用程序在构建性与维护性方面的一张地图。这就是组件的依赖结构图不能在项目的开始阶段被设计出来的原因——当时该项目还没有任何被构建和维护的需要,自然也就不需要一张地图来指引。然而,随着早期被设计并实现出来的模块越来越多,项目中就逐渐出现了要对组件依赖关系进行管理的需求,以此来预防“一觉醒来综合征”的爆发。除此之外,我们还希望将项目变更所影响的范围被限制得越小越好,因此需要应用单一职责原则(SRP)和共同闭包原则(CCP)来将经常同时被变更的类聚合在一起。

       组件结构图中的一个重要目标是指导如何隔离频繁的变更。我们不希望那些频繁变更的组件影响到其他本来应该很稳定的组件,例如,我们通常不会希望无关紧要的GUI变更影响到业务逻辑组件;我们也不希望对报表的增删操作影响到其高阶策略。出于这样的考虑,软件架构师们才有必要设计并且铸造出一套组件依赖关系图来,以便将稳定的高价值组件与常变的组件隔离开,从而起到保护作用。

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

稳定依赖原则

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

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

       这就是软件开发的困难之处,我们精心设计的一个容易被修改的组件很可能会由于别人的一条简单依赖而变得非常难以被修改。即使该模块中没有一行代码需要被修改,但是整个模块在被修改时所面临的挑战性也已经存在了。而通过遵守稳定依赖原则(SDP),我们就可以确保自己设计中那些容易变更的模块不会被那些难于修改的组件所依赖。

       稳定指的是“很难移动”。所以稳定性应该与变更所需的工作量有关。例如,硬币是不稳定的,因为只需要很小的动作就可以推倒它,而桌子则是非常稳定的,因为将它掀翻需要很大的动作。

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

稳定抽象原则

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

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

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值