聊聊组件设计原则
0. 前言
我们在前文《聊聊面向对象的设计(OOD)原则》中介绍过类的设计原则,本篇文章我们进一步介绍组件的设计原则,进而再进一步探讨一些架构相关的设计思想。
从函数、类、组件、系统架构是事物聚合的过程,也是一个具象到抽象的过程。关注具象是为了打好基础,一个系统的如果充斥的大量的晦涩的、混乱的代码,能实现业务需求都很难保证。关注抽象是为了保证软件是“软的”,可以随业务需求以最小的代价调整适配。只有将底层具象和上层抽象都兼顾,才能让我们的系统始终有序迭代向前。这也是架构师的主要职责。
组件作为底层和上层的中间层,是我们进行架构设计的主要场所。划分好组件职责和范围,定义好组件关系,确定好组件的交互形式就完成了系统架构上层设计的全部工作。
1. 组件的定义
1.1 什么是组件
组件是软件部署单元,是整个软件系统在部署过程中可以独立完成部署的最小实体。
这里面我们关注部署单元,也就是说组件是一个物理上的概念,如果对于Java组件就是一个jar包维度。
这里还要注意组件和我们常说的模块有所不同。
1.2 组件和模块的区别
对于面向对象的设计语言,组件和模块都类的容器,是类的某种分类形式。但是组件是物理划分概念,处在不同组件中的类一定部署在不同的地方。模块是逻辑划分概念,一个物理的组件可以有多个逻辑上的模块构成,处于不同模块中的类有可能部署在一个组件中。
在不严格的情况下,这两个概念其实可以通用。
2. 组件的设计原则
如果我们设计一个类,通常情况下我们关注两点:
这个类是什么?:类包含什么属性,有什么行为方法
这个类和其他类如何交互?:类与类之间的关系
所在在设计组件的过程中,我们也考虑两个方面:
这个组件是什么?:组件由哪些类组成
这个组件与其他组件如何交互?:组件之间的关系
其实类和组件的设计都高度依赖于具体的业务。而分析业务最好的方式是通过DDD的设计方法。但是除了个性的业务之外,我们也可以在类和组件中抽象出一线共性的指导方法,这些就是设计原则。我在以前的文章已经聊过了面向对象的设计原则(OOP),主要是在设计类中需要遵循的设计方针。组件设计方面也有类似的原则,并且和OOP中某些原则有相似的地方,接下来我逐一介绍。
2.1 组件的聚合
究竟哪些类应该被组合在一个组件呢?这个问题需要深究。通常情况下我们都是通过经验拍脑门的方式划分组件(或模块),比如对于一个电商系统,我们会划分为:购物车、交易、支付、物流、用户、促销等等组件(或模块)。这些这是单纯从业务上划分,但是业务也不是非黑即白的,往往没有和清晰的界定。这个时候我们需要一些技术层次上的原则指导。
2.1.1 复用/发布等同原则(REP)
软件复用的最小粒度应该等同于其发布的最小粒度
这个原则好像很有道理,又好像什么都没说。
从字面意思理解就是我们希望组件复用的维度和组件发布的维度相同(感觉没说一样)。
从架构设计角度说就是希望组件中的类和模块必须彼此紧密相关(高内聚思想)。
从实际操作角度说的就是组件中包含的类和模块应该同时发布,并且公用一个版本号和版本追踪,且对用户提供一套说明文档。组件的功能可以对用户某一类功能提供很好的复用支撑。
但是除了“理所当然”的实际指导原则和“模糊不清”的高内聚思想之外,REP并没有给我们清晰的定义出到底应该如何将类与模块组成组件。一旦我们违反了REP有会从主观上感觉很“别扭”(因为违反了高内聚思想),这就造成了无法判断组件划分是否正确,只能凭主观判断划分有问题但也不知道如何变正确的状态。因为对于高内聚思想通常情况下我们容易证伪,但是很难遵循。接下来两个原则可以进一步补充,帮助我们进一步改正问题。
2.1.2 共同闭包原则(CCP)
我们应该将那些会同时修改,并且为相同的目的而修改的类放到同一个组件中,而将不会同时修改,并且不会为了相同目的而修改的那些类放到不同的组件中。
其实CCP原则就行SRP原则在组件维度的再次重申。正如SRP原则提到的“一个类不应该同时存在多个变更的理由”一样,CCP原则希望一个组件也不应该同时存在着多个变更的原因。
这个原则对组件构成有了一些更加实质性的指导,引入了“变更”场景。“变更”在日常的开发工作中是一个很常见的场景,虽然我们都不喜欢变更,但是变更是无法避免的。所以我们希望这些变更收敛于一处,便于系统维护。
所以CCP告诉我们,如果两个类紧密相关(REP),并且相同的修改理由导致一起被修改,那么他们应该在一个组件内。所以这个原则在REP上更近一步,给了我们更加具体的指导意见。
2.1.3 共同复用原则(CRP)
不要强迫一个组件的用户依赖谈么不需要的东西
这个原则是另一个帮我我们决策类和模块属于那个组件的原则。和ISP原则类似。
从字面意思我们可以感觉到这个原则是告诉我们应该将那些类从组件中拆分出去。组件中留下的类应该是高度关联并且不可拆分的。将不应该被依赖的类从组件中拆分出去。降低了组件的复杂度、提升组件的稳定性,为依赖组件的用户带来了方便。
2.1.4 三个原则的关系
如图所示,这三个原则是互有侧重,三个原则构成了一个三角形的交互区域,组件的组成就在这个三角区域之内选择侧重。
我们在《聊赖微服务架构》中介绍过系统架构有单体架构微服务架构演化的过程,
对应到组件的范围定义也是类似,一个系统中组件的范围也是从合到分的演进过程。项目伊始,我们队业务料及的比较浅显,选择组件的通常范围通常比较大,职责划分的不是很清晰,倾向于REP原则,这样的好处是方便业务快速开发落地。随着我们对业务的了解的深入,发现原来的划分方式有不合理的地方,可能我们组件的粒度过大,包含的相关度没有这么高的类的,也可能组件之间会联动修改,不利于维护。所以我们会进行组件之间的拆分和二次组合工作,组件构成的依据偏向于CRP和CCP原则。
从这个例子也说明了架构不是一蹴而就且固定不变的,系统的架构需要随着业务的变化而演化的,否者架构就不再满足业务要求,系统也就无法在尽可能节约成本的基础上保持灵活可变。
2.2 组件的耦合
说过了组件的构成,接下来我们在介绍一下组件的关系。同样有三条原则为我们提供指导:
无依赖环原则
稳定依赖原则
稳定抽象原则
2.2.1 无依赖环原则
组件依赖关系图中不应该出现环
不言自明,这个应该是一个“强”原则,必须遵循,如果系统中组件依赖有环,那么环内的组件就称为了相互依赖的共同体。他们的开发、测试、构建、发布等流程都产生了强关联,从某种意义上这些组件组成了一个大“组件”。
虽然在架构设计阶段我们保证了这个原则,但是随着项目深入,组件越来越多,调用关系越来越复杂,有可能出现依赖环。这个时候我们需要处理,我们可以组件中依赖拆分进新的组件,组成新的依赖关系,去掉依赖环;或者使用DIP原则,改变环中两个组件的依赖方向,一样的可达到去掉依赖环的目的。
2.2.2 稳定依赖原则
依赖关系必须指向更稳定的方向
组件A依赖于组件B,那么组件A就被组件B所影响,组件B的变更会使组件A被迫变更,所以如果组件A是一个不经常变更的组件,组件B是一个经常变更的组件,这样的依赖关系就会破坏组件A的“稳定性”。
一个不经常变更的组件(A)不应该依赖经常变更的组件(B)。
如果出现这样的依赖我们如何处理,改变依赖最好的方式就是遵循DIP原则,通过引入一个“抽象组件”,让组件A依赖抽象组件,组件B来实现抽象组件。这样就改变了依赖关系。
2.2.3 稳定抽象原则
一个组件的抽象化程度应该与其稳定性保持一致
我们知道在面向对象的设计中,接口的抽象化程度最高也最稳定、不宜变化,而接口实现类是具象的最不稳定,经常改变。组件由接口和类构成,组件中抽象的接口和具象的类的构成比例直接影响组件的稳定性。所以希望一个组件稳定,组件中抽象的接口比例就相对高一些,而如果希望一个组件不稳定,组件中具体的实现类就会占的更多。我们需要根据一个组件的职责调整组件中接口和类的构成比例。
2.2.4 稳定和变化
稳定性和变化性使我们要区别的对待的。架构设计的一个关键点就是区分出什么是稳定点、什么是变化点,区分的目的就是用不同的架构设计放啊将他们区别对待。这样架构才是相对稳定且方便扩展的。现实中也不存在绝对稳定的架构和时刻变化的架构。
3. 总结
本文主要介绍了组件的设计原则,组件设计的好坏关乎整个架构设计的优劣,值得我们认真对待。最后在强调两点:
架构是稳定且变化的:架构师需要区分出架构中的变化点和稳定点,区别对待,让自己的架构更加灵活并可落地。
架构是需要演进的:架构师需要时刻关注业务变化,对组件的构成及关系作出调整,让自己的架构满足日益发展的业务需求。
版权声明: 本文为 InfoQ 作者【Jerry Tse】的原创文章。
原文链接:【http://xie.infoq.cn/article/d68a6618d0bbc1597c317760d】。文章转载请联系作者。
评论