在 Golang 中是不允许出现两个 Package 有循环引用的情形,这种情况编译器会编译报错:import cycle not allowed
编译器一般通过 Kahn 算法来确定源代码文件的编译依赖关系,如果一个 Package 被重复的访问则说明存在环也就是循环引用的情况。
假设 我们存在 Package A,Package B,Package C
他们之间的依赖关系如下图所示: (A → B 表示 Package A 依赖 Package B)
A -> B
B -> C
C -> A
JSON
我们可以显而易见的看出这三个 Package 之间存在循环引用的情况
由于依赖具有传递性,我们无法简单的通过依赖转嫁的方式解循环引用的问题, 即我们无法通过把 A → B 的引用,替换为一个封装了Package B 的 Package D 来解决循环引用的问题:
A -> D
D -> B
B -> C
C -> A
JSON
抽象接口
在上面依赖转嫁的例子中,我们可不可以让 D 不在依赖 B 从而截断这个循环引用的过程呢。答案就是我们可以对 B 进行抽象提取一个接口放置在 Package Ber 这样 Package A 不直接依赖Package B 而是依赖 B 的抽象接口 Ber
A -> Ber
B -> C
C -> A
JSON
这个方案看似很完美,但实际上这里有一些细节的问题需要我们考虑:
A 中使用的 Ber Package 的接口何时被实例化,如果放在 A Package 中必然直接导致了循环引用。我们必须使用像依赖注入的方式,在使用 A Package 的别的 Package 中实例化化好 Ber Package 的接口,以接口的方式传入 Package A 中,在这个例子中使用 A Package 的就是 C。
A -> Ber
B -> C
C -> A
C -> B
C -> Ber
JSON
那最终还是会导致循环依赖
这个循环依赖产生的原因在于 C Package 需要实例化 B Package 造成了 BC 相互引用的情况。
如果我们不存在一个 C Package 依赖 A package 且 C Package 在这个循环链中。最简单的情况就是我们只有 AB 两个 Package 且他们相互依赖。 Out 为外部 Package
A -> Ber
B -> A
Out -> A
Out -> B
Out -> Ber
JSON
在这种情况下通过抽象接口的方式确实可以解开循环依赖
但是它还存在一些小问题,当我们相对 Package A 写单测的时候我们就会发现非常的棘手。因为 Package A 中对于 B 的依赖是通过依赖注入的方式传入的,但在 Package A 中的测试代码无法应用 Package B 来实例化 Ber Package 中的接口。那这样我们可能不得不把单测放在A,B Package 的外部,网站起来不是特别优雅。
总的来说通过抽象接口的方式我们可以解开相互引用的情况,但是解不开超过2个循环依赖链的问题,并且我们的单测必须放在循环引用 Package 的外部。如果我们一定要通过抽象接口的方式去解开超过2个循环依赖链的问题,我们可以考虑将一些 Package 进行合并这样我们就只有两个 Package 相互依赖了。
拆分子包的方式
如果我们能在上述 Package ABC 找到一个子 Package ,且这个子 Package 聚合了所有关于另一个 Package 的引用操作,那我们将这个子 Package 拆分出来作为一个独立的 Package B+。我们以 Package B 来举例,则拆分后的依赖关系大致如下:
A -> B
B+ -> C
C -> A
JSON
但这里还有几个依赖关系不太明朗就是:1. A 和 B+ 的依赖关系 2. B+ 和 B 之间的依赖关系
如果 A 还依赖 B+ 那么这个 B + 和原来的 B 在依赖关系上是等价的,循环依赖依然存在
B+ 和 B 肯定不能相互依赖,否则自己就构成循环引用的问题。如果 B 依赖 B+ 则这个依赖关系的拓扑图和我们依赖关系转嫁的方式等价也不能解决循环依赖的问题。
A -> B
B+ -> B
B+ -> C
C -> A
JSON
我们可以看到 B+ 和 B 两个 Package 拆分实际上将原来的 Package B 按 被 A 依赖的和依赖 C 的拆分为了连个部分,这两个部分中 B+ 的部分在这个循环链中一定是入度为零的也就是不被(ABC)中任何一个 Package 依赖。
合并相互引用的包
如果我们发现在上述依赖的 Package ABC 中实在难以拆分出类似 B+ 这种子 Package, 这通常说明我们的 Package 内部依赖非常的紧密,而 ABC 也出现了循环依赖的情况说明 Package ABC 的功能联系也非常紧密。根据低耦合高内聚的原则其实我们可以将 ABC 合并成为一个新的完整的 Package,这样在一个 Package 中就无所谓循环依赖的问题。
结论
基本上我们在划分 Package 的时候尽量做到单一职责,高内聚,分层处理等原则还是很少会碰到循环依赖的问题的。但是往往我们为了代码网站优雅,开发效率或者业务极度复杂的情况时候,还是会多多少少遇到循环依赖的问题。虽然我们用了上面的办法解决了,循环依赖的问题但是或多或少会带来其它的问题。文件结构不优雅了,单测位置奇怪,划分的 Package 业务看上去有点奇怪不符合常理,我们都需要进行综合的权衡取舍。