代码耦合性

说到代码的耦合性,我想大多数人应该都没有好好的了解过什么是耦合,大多数是在自己开发时遇到了一些问题,或是听主管说起就大概的有一个了解。

我也是这样,我从没有去了解代码耦合,我只是一个大概的认知,大概就是一个方法不要重复的出现在多个模块,模块之间不要有太多的相互依赖。

在这之后我遇到了一个声明某函数类型,但在另外一个文件里实现的使用方式,一个小哥告诉我这是为了减少代码耦合,我就觉得奇怪,你既然在这个包声明了该函数,就算你在另一个包实现那也是一样要调用该包的,如何解耦?后面我觉的是大家根本就不理解耦合,都是胡乱猜想的,我的代码重复的逻辑提出来了,或是清晰了那就是解耦。对于什么是“耦合”、什么是“乱”,他们并不知道有什么客观标准可以度量。

我就上网查了一下资料,有一个解释甚合我意,文章把代码耦合划分成三个可以度量的标准:

依赖

依赖和耦合的最大区别在于,当我们说“A和B耦合”时,在字面含义中,A和B二者平等。然而,正确的模块关系根本不应该平等,而应该是单向依赖才对。所以我们应该说“A依赖B”,这样含义要清楚得多。A依赖B意味着,A模块可以调用B模块暴露的API,但B模块绝不允许调用A模块的API。单向依赖是红线,好的设计一定不会违反这条红线。注意:根据实质重于形式原则,本文中的“依赖”指人脑中的依赖而不是编译器的依赖。

只要程序员编写模块A时,需要知道模块B的存在,需要知道模块B提供哪些功能,A对B依赖就存在。甚至就算通过所谓的依赖注入、命名查找之类的“解耦”手段,让模块A不需要import B或者include “B.h”,人脑中的依赖仍旧一点都没有变化。唯一的作用是会骗过后文会提到的代码打分工具,让工具误以为两个模块间没有依赖。

其实在go语言中平级的模块是不该分包的,因为单向依赖的红线存在,同等级的模块并不能进行依赖即使你说我的A模块确实只依赖B,B并不依赖A,但事实是这种关系的限定只是在你的脑海中,并不能让其他人理解,在其他人想要定义一个方法让B依赖A时就会出现问题,所以go语言的解耦方式应该是进行层级分包,而不是模块分包,好的方式应该是拆分数据层,结构层,传输层,服务层,路由层等等层级之间有着明确的上下级依赖关系,不同的模块在不同的层级各自成文件。

所以之前那个函数类型的命名及多处使用不同逻辑实现并使用的做法其实是自娱自乐,和解耦是毫无关系的。

如果你的业务极其复杂有很主模块下又有许多子模块,这时候单纯的分级成包就会非常丑陋,这个时候定义平级包是无可厚非的,此时就要考虑下面的两点

正交性

是指一个模块提供的API中,多个方法之间是否有重复的功能。如果有重复功能,正交性就差。通常,正交性高的模块更稳定,不会因为上层业务变化而被迫修改代码。好的API内部的多个方法之间不应该有任何重复功能,只实现正交的机制。如果感觉拆得太细使用不便,应该在底层API之外包装出一层Helper、Utility组成的胶水层。胶水层调用底层原语API来实现常用模式供上层使用。对于胶水层中的模块,对正交性的要求可以稍低一些。注意上层代码既可以直接调用正交的底层API,又可以调用胶水层的常用模式。

架构中如果有平级成包,那真的难免需要相互依赖,因为我用的是go,go代码是严格的规范了包之间不可以相互依赖的,比如A依赖B,B依赖A 这是肯定不行,还有A依赖C,C依赖B,B依赖A,这也不行。所以平级包相互调用的问题是恨难受的当然这样也是从编译层面规范你的代码。为了快速解决这个问题,使用了拷贝函数副本的方式,其实就是一个方法多次重复定义。

试想如果我要改动这个方法,你还能记起还有另外一个包里实现并在使用这个方法吗,不可能记得。所以重复的方法最好是不要有的,除非你知道这个方法永远不会改动。这就是正交性的重要之处!

紧凑性

是指一个模块提供的API中,公有方法总数必须很少,每个方法的参数也必须很少。《Unix编程艺术》上说一个模块不要超过7个方法,不然就很难理解。

就拿刚刚的那个问题来说,后面我发现了使用重复方法的函数副本的种种问题,就又实用了另一个方式,搞一个common 来收集公用放法,这就引出了紧凑性的问题。

为什么紧凑性重要呢?如果一个common 中的方法经常改变,这时你就要考虑每个模块调用该方法的业务需要不需要改变,此时如果是一个新人接手的代码他根本不可能考虑到这个方法在多个模块形成依赖,改了会对其他模块造成影响。

所以最好不要使用业务级别的公有方法,因为你无法避免他的改动,改动造成的代码影响你是无法察觉的。

解决方法

可以抽出一个凌驾于这些平级模块的层级,让他来负责统一调用各个模块并形成最终结果。或是把这个模块单独成服务让其他模块通过协议来调用,抑或是独立出一个结果整合的服务,让他来负责需要相互依赖的方法的接口实现。