上一篇文章讲了单体体系中模块间的解耦。确实,在一个单体系统中,由于所有模块往往在开发时共享一个代码库,运行时又共享同一个运行时环境,所以更容易造成耦合。但这并不意味着组成系统的各部分变成分布式以后,就可以消灭耦合了。当我们在更大的尺度来看这个问题的时候,反而更容易看清耦合的本质。
前言里说过,耦合是系统结构中各模块间相互联系紧密程度的一种度量。我们希望这种联系越松散越好,但是显然一点儿联系也没有是不可能的。耦合有多种类型,不同类型的耦合代表的模块间的紧密程度也是不一样的。
这里所说的模块是指系统的组成部分,系统组成包括多个模块以及模块间的关系。在技术层面模块可能存在于一个单体应用中,也可能以分布式的系统存在,这不影响我们耦合类型的讨论。
从上图中可以看到,各种耦合类型导致的系统紧密程度不一样,越松散就越好。下面我们解释几种常见的类型。
Content Coupling:内容耦合是最糟糕的耦合。假如有A和B两个模块,B模块直接访问了A模块内部的数据,那么A对B的依赖就太重了,耦合就太紧了。这是对封装或者叫数据隐藏最直接的破坏,这就像你不是去银行柜台取钱,而是直接进入银行的金库去拿钱。且不说法律和安全上是否允许,你能不能拿到钱,很大程度上依赖于银行金库内钱的存储的结构以及你对它的熟悉程度。微服务倡导的,一个服务应用应该独享自己的数据存储,服务间要通过远程通信来协作,正很大程度上能避免了这个问题。
Common Coupling:这种耦合比上一种好一些,也好不到哪里去。A和B两个模块都依赖一份数据,只是这个数据即不属于A也不属于B。当然,严格遵守微服务的原则也能避免这种耦合。
Stamp Coupling:比共享数据更好的方式是通过参数传递数据。银行柜台的营业员就是一个接口,你要取钱的时候,去找他,把你的需要告诉他,而不是直接闯入金库去拿钱,这就是一种参数传递。但怎么描述你的需求也很关键,我们举个栗子,你来到柜台前,告诉营业员,下周你要带女盆友去国外旅游,你把你的行程表给他看,然后,聪明的营业员从你的定期账户里解冻了2万元到活期账户里,然后又把它转换成美元交给了你。这个是一个参数传递的例子,你的行程表就是入参,但这样并不好,原因是营业员直接需要的不是这个行程表,他需要从你的输入中提炼信息才能完成任务,这就是Stamp Couping。
Data Coupling:更好的是Data Coupling,它也是通过参数传递数据,与Stamp Coupling不同的是,交流的参数结构是由接口提供者定义的。作为客户,你直接告诉营业员,从你的定期账户里解冻2万元到活期账户,然后再把活期账户里的2万元转换成美元给你,这就可以了,你在用银行规定的概念和银行的营业员交流,至于你去干什么,银行并不关心,你也无需赘述。
当我们使用微服务架构构建的是分布式系统的时候,显式的数据共享造成的耦合基本避免了,但Stamp Coupling还是很常见。
注意,我这里说的是显式的数据共享造成的耦合基本避免了,还有一些隐性的,后面会举个例子。
DDD(Domain Driven Design,领域驱动设计),对于避免Stamp Coupling很有帮助。
在DDD中,最关键的一个概念是限界上下文
,每个上下文内的概念是完整的,上下文是闭合的。比如A,B两个上下文都有顾客
的概念,但彼此没有任何关系。
只有在上下文集成的时候,概念间才会出现转换,然而这种转换只发生在集成的部分,即耦合是控制在胶合地带的。
同时,上下文间定义了映射关系,在上下文的关系中,又定义了上游
和下游
。下游需要理解上游的概念,而上游不需要理解下游的概念,甚至不应该知道下游的存在。
DDD中上下文的关系有好几种,不是每一种都能明确上下游,我推荐尽可能使用有上下文定义的关系,比如“客户-供应商”关系
前面的例子里
,取款的顾客是下游,而银行是上游,顾客需要理解银行的那些概念,诸如,定期账户、活期账户、取款等等,而银行不关心顾客的私人生活。
举个更实际的例子,销售上下文中有订单这个概念,用户提交一个订单,然后销售上下文把订单给到生产上下文,生产上下文去完成拣货打包,这就构成了Stamping Coupling。
如果按照DDD的思想,首先,生产上下文中,不会有订单的概念,只会有生产单的概念,所以,你给它一个订单让它去生产,它无法接收。其次,这种场景,订单上下文是下游,它应该负责把订单转换为上游生产上下文的生产单,生产上下文对此甚至是不知情的,这就变成了Data Coupling。
如果想让下游和上游也保持一定的独立性,可以使用DDD中的防腐层的概念,这个有点类似我们上一篇中讲的DIP原则。很多面向对象的原则在更大尺度的系统设计层面也是适用的。
上游,下游,以及依赖,体现的是对概念,对知识理解上的依赖。这个东西是无法用技术手段来解决的。有人认为使用消息来通信可以解决这个问题,确实,使用了像MQ这样的技术,一个系统的奔溃,不至于导致另一个系统随之坍塌。这是一种解耦,但不是在业务功能上的解耦。我就见过一个用MQ实现的Stamping Coupling的例子。
生产系统打包完成后,需要配送,按道理生产系统是下游,配送系统是上游;然而在系统实现时,配送系统监听了生成系统打包完成的消息,然后生成了自己的运单,事实上,这还是产生了Stamp Coupling。
所以说,技术手段有助于避免不恰当的耦合,但很难彻底避免。哪怕是我们用了微服务,有时候还是会造成最差劲的Content Coupling或Common Coupling的。
我见过有的电商系统,在订单上记录了很多的“标记”数据,接下来,有几十个系统的内部逻辑都依赖于从订单系统中读取的某些标记。虽然他们没有访问订单的数据库,虽然这些系统是通过服务访问来获取订单数据的,但是他们依赖了订单这个中心化的数据。
行文支持,基本把如何解耦说的差不多了。最后要说的是,解耦,尤其是功能解耦,他不是一个纯粹的技术问题,很难有一个工具或框架就能帮你做好,就算现在做的最好的DDD,也是在思想或方法层面在给我们做指导,所以,解决起来往往比技术问题更难。另外,松耦合设计也会带来额外的技术实现的难度和成本,这个也是需要权衡考虑的。
一切都是权衡,不过,在权衡之前,掌握的东西越多越好。
codeasy,让编码变得容易。让编码变的容易,很多功夫往往要下在编码之外。
往期推荐:
长按2秒,识别二维码,关注我