关于组件聚合张力图的讨论
周三的午休时间,我在ThoughtWorks北京办公室分享了一场《架构整洁之道导读》。当谈到分享组件聚合原则的时候,很多同事表示难以理解。究其缘由,是我们无法将组件违反原则的后果对应到真实项目的问题上,这就导致原则和实践之间的不一致。讨论的过程异常激烈,但是很遗憾地最终并没有得到一个服众的结论。所以为了进一步澄清这些争议点,我决定专门组织一场针对组件聚合原则张力图的讨论会。在吴大师的鼓动下,时间定在下周四晚上的8点半,与会人员大多是咨询团队的技术教练,也有我们项目上的客户。
在这场长达两个半小时的讨论会上,没想到首先出现争议的点居然是组件的定义。
组件是软件部署的最小单元,是整个软件系统在部署过程中可以独立完成部署的最小实体。
对于这样的定义,大魔头提出了质疑:library(库)并不能独立部署。但凡出现明显的逻辑漏洞的时候,我们最好的方式是抛开译文回去看原文。
Components are the units of deployment. They are the smallest entities that can be deployed as part of a system.
阅读原文之后,我们发现“组件是软件部署的最小单元。”这句话翻译得并没有太大问题,但是第二句就有损原意了,原意是说可以作为系统的一部分被部署的最小实体,而没有强调部署过程这种动态的概念,否则就和前一句是同义反复。所以这个定义里面并没有说组件可以独立部署。后面提到组件可以被链接到一个独立可执行文件或者归档文件,又或者,可以被打包成.jar、.dll或者.exe文件,并以动态加载的插件形式实现独立部署。
解读组件的定义
来自原文:
Components can be linked together into a single executable. Or they can be aggregated together into a single archive, such as a .war file. Or they can be independently deployed as separate dynamically loaded plugins, such as.jar or .dll or .exe files.
来自讨论:
20:56:56 From tianjie : These dynamically linked files, which can be plugged together at runtime, are the software components of our architectures.
联系上下文理解之后,我们知道:组件可以被设计成独立部署的,但是并不是所有的组件都是可以独立部署的。这是要澄清的,不然讨论聚合原则的时候容易出现偏差。
吴大师接着解释说,组件应该是个逻辑单元,而不是物理单元。强制某个代码模块就是一个物理的部署单元是不合适的。另外,鲍勃大叔在介绍架构边界时,也表明了一样的观点:架构的边界并不是服务的边界。
解读REP原则
我按照自己的思路解释过REP、CCP和CRP原则[1]之后,讨论的焦点很快聚集到REP原则的解读和实践意义上。
吴大师认为REP原则如果简单解读成没有发布过程就不能复用,它就和CCP、CRP原则的排斥力量不均衡,无法形成稳定的三角关系,那么这个张力图就显得有点鸡肋。
尚奇受到CAP(分布式系统基本原理,一致性,可用性和分区容错性)原则的启发提出了另一个解读方向。他说,CAP原则在分布式系统的实践里,都会先站住P原则,然后在C和A中权衡。那么在REP、CCP和CRP三角关系里,REP原则就相当于这里的P原则,必须先满足然后再去取舍CCP和CRP。
大魔头理解REP的意思是可复用性就是组件是独立可复用的。假如回到没有Maven这些工具,没有依赖管理的年代,如果我们所依赖的包还依赖其它第三方包,那么这个包就不能叫做独立可复用。
21:13:04 From YangYun : 我倒是理解REP的意思是你发布出来的一个可重用的包就是独立可重用的,你不能让我必须带着别的jar包才能用它。
21:14:04 From YangYun : The granule of reuse is the granule of release
他接着说,假如有两个提供同样功能的包,其中一个没有第三方的依赖,而另一个有,那我当然选择前者。
技术教练Sara举出了一个相对复杂但是很有启发性的例子。
21:46:35 From Qian Ping : 假设项目包含sub module ABC
- 如果ABC单纯sub module没有打成jar,又互相直接复用了,就是违反了REP
- 如果每个sub module,打成jar,互相复用的时候是通过对方特定版本的jar(如snapshot版本),就是符合REP
- 如果符合REP了,而所有sub module是跟随整个项目一起升级版本,就是符合CCP因为他们是一体一起发布的
- 这时假如A依赖B和C,我这次单纯想改C,他们一起升版本了。但其实B的Jar完全没有变化,这个对B来说就是一个不必要的发布,B又貌似应该分离出去,但如果它分离出去了,就又离REP和CCP远了
对于最后一句的表述,她澄清道:
之前有遇到一个情况,比如组件A,然后它里面需要用到一个common library, lib里面其实包含了比如3个sub module(1/2/3),全部都是A需要复用的, 这时候如果要改1/2/3里面任意的东西,都会一起升级lib,然后在A里面对应升级版本。
后来,有一些新组件B,它只需要用到common lib里面的3,不需要1/2,于是3一直被改和打包版本。 此时1/2会跟着升版本号,但其实1/2内容本身是完全没有变化的,只是版本号升了。
这个场景中引入了两个组件A和B分别依赖common library的某些模块。在我们讨论一个组件依赖时,面临的约束要简单很多,但是复用的初衷就是给多个组件去依赖,所以这个假设是很有价值。
Sara分析的思路如下:
如果分离出去,等于我有两个common lib(1/2 和 3), 对于B来说,B只需要3这么一个lib是比较完美的,反正改了3再改B就好了。
但对于A来说,它就需要同时升级1/2的lib和3的lib,等于要3个发布,而它原来只需要2个发布(1/2/3 + A),所以离CRP远了,同时它也要分别维护两个lib分别的版本升级,所以CCP也比原来差了。
在她的分析下,我们发现CRP和CCP不单是互相排斥的,还有可能两者都无法满足。造成这种结果的原因在于1/2/3模块形成的这个common library对于A组件而言都符合CCP和CRP原则,但是对于B组件而言,是不满足REP和CRP原则的,因为每次想要依赖3模块,就得全部依赖1/2/3整个common library(复用困难)。反之,如果我们将3从1/2/3中拆出来成为独立的组件,那就几乎宣告对于A组件而言势必违反CCP和CRP原则,但是B组件却获得了符合REP和CRP原则的好处。
她接着补充道:
其实后来说起对应微服务的时候有另外一个想法,就是比如说我系统里面多个组件需要用计提(Mark to market[2])这么一个功能,说白了就是一条公式,那通常可以有几个做法
- 直接把这个公式复制到要用的组件,code level的复用,没有版本 -> REP bad, CCP bad, but CRP not bad (因为要更改时候发布次数还是一样的)
- 把公式写到一个common lib里面再进行复用 -> REP good, CCP good, CRP bad(多发布一次)
- 把公式放在一个独立service -> REP good, CCP bad(因为要维护多一个服务), CRP good
这个观点就上升到不同层次的复用性上,可以算是对组件聚合原则的普适性的探索。
当话题再次被聚焦到复用性时,技术教练MoMo提出一个观点:我们现在讨论就是可复用组件应该遵循的原则,而REP是对复用粒度的定义。至于那些那些常年采用SNAPSHOT(Java项目里Maven常用的开发版本号),没有发布概念的组件,就不该纳入复用的考虑范围内,那些也就不是REP的反模式。
与此同时,阎王指出了一个翻译上的失误。组件粘合张力图中REP原则的简短描述是“为复用性而组合”,而原文其实是"Group for reusers",翻译过来应该是为了复用者而组合,复用性的英文是 Reusability。所以为了复用者发布,考虑的就是对外部的承诺。
外部资料
大魔头在加班写方案和讨论的间隙,快速查阅了一些资料,比如wiki上对于REP原则的定义:
21:45:05 From YangYun : Reuse-release Equivalence Principle (REP)
REP essentially means that the package must be created with reusable classes – “Either all of the classes inside the package are reusable, or none of them are”. The classes must also be of the same family. Classes that are unrelated to the purpose of the package should not be included. A package constructed as a family of reusable classes tends to be most useful and reusable. - wiki百科里
在wiki的定义里,可以看到REP原则包含CRP和CCP原则的成分,如此看来,这三大原则并不符合MCME分类原则,就连鲍勃大叔在书中也是模棱两可的态度——REP维护共同的大主题,组件中的类和模块也必须紧密相关,这基本是CCP和CRP的简版描述。
然后大魔头查找到“粒度”这个词在软件设计中详细定义,这是对REP原则定义(软件复用的最小粒度等同于其发布的最小粒度)的分解和再认知。
21:57:03 From YangYun : http://condor.depaul.edu/dmumaugh/OOT/Design-Principles/granularity.pdf
21:58:29 From YangYun : https://fi.ort.edu.uy/innovaportal/file/2032/1/design_principles.pdf
这些观点和学术建议很有代表性,值得大家反复揣摩和思考。
反模式
软件工程师一般有个“正难则反”的习惯。原则较抽象,但是模式很具体,反模式更能指导实践。接下来,大家开始讨论哪些是违反了REP原则的反模式。
首当其冲的就是git submodule
,在某些项目中,这种通过源代码划分模块并共享的方式还是挺常见的。因为共享的是代码,所以每次共享代码更新,势必要让依赖方重新编译,发布和部署。这种做法对于复用是痛苦的。
其次是常年使用SNAPSHOT版本的某些项目。这些项目的特点一般都是某个产品团队底下,内部团队之间有复用的要求。缺点其实也很明显,常年SNAPSHOT等于没有版本和发布的流程。使用者并不知道SNAPSHOT中哪些是稳定的,哪些是修改的,拿到的版本到底是最新的还是遗留的,我需要的功能在这个功能有包含,还是你包含了太多我不需要的升级。这种也是复用痛苦的。
REP原则小结
综合以上两个例子以及其它讨论,我们得出了一个好玩的结论:软件工程发展到现在,REP原则已经是基本的要求,它的存在有可能是鲍勃大叔年代感老了的体现。
[1] 架构整洁之道导读(一)编程范式
[2] 架构整洁之道导读(三)组件耦合
于 2018-11-12