架构整洁之道--跟开源三方软件划清界限实战案例

背景:

假设有一个开源/第三方的软件模块ModuleA,我们要基于其上做特性的扩展。我见过的较多的做法是二者的代码实现揉在一起,这样会导致一个问题。当ModuleA做升级或者ModuleA替换成另一款ModuleB的时候,需要有大量的代码做Merge适配。这样带来的一个比较大的问题是,每次升级开源/三方软件,极大概率会出现漏合错合代码的现象。(我所经历的项目中,有因为漏合入一行代码,引入概率性问题,攻关一个月之久)。因此我们需要有一种方法,至少让自研代码和原始开源/三方软件的代码隔离开来,以插件式拼接的方式面对这种升级问题,而不是琐碎的侵入式穿插修改。

案例:

下面我们就以一个例子为主线,讲述一种在实战中使用的案例。实实在在解决了上述痛点问题。
假设原生开源/三方软件中有一个ModuleA,其中包含Func1,和Func2两个功能,见图1。


图1 ModuleA

假设此时,我们需要往ModuleA中增加一个功能FuncEx,以扩展ModuleA的逻辑从而满足我们的需求。其中调用关系为Func1->FuncEx->Func2。见图2


图2 ModuleA扩展

图2修改方法,其实已经进入了本文讲的问题痛点范畴,自研代码和原生代码纠缠在一起。我们想办法把它解耦出来。ModuleEx作为一个独立的模块提取出来,只包含FuncEx功能,放在独立的文件和目录中,见图3


图3 提取ModuleEx

但此时出现了明显的循环依赖。循环依赖倒不是关键问题,因为我上面说了,我们只是从代码层面解耦开来,ModuleEx是无法脱离ModuleA单独存在,从本质上来讲,它只是附属于ModuleA的一个片段。所以从编译构建和运行的角度,它们还是一个整体,循环依赖属于组件模块内部的依赖。

我们之所以需要继续改造它,是因为,它不够稳定,设想一下,如果由于升级ModuleA,Func2有变化(名称变更或者实现逻辑变化需要找另一个函数替代),必然导致FuncEx跟着变化。而正如架构整洁之道中讲的,好的架构需要保护自己的核心域不受影响。因此解决方法也是使用Bob大叔提供的方法,增加接口实现依赖倒置,从而也解决了循环依赖的问题。这样从架构边界上看,就变成了开源/三方依赖自研。见图4

图4 增加接口隔离

但实际上,这里还有一个问题,我们没有办法撼动开源/三方的ModuleA实现我们的接口Func2Inf。我们只能自己来做这件事情,毕竟受益的是我们自己。因此我们增加了ModuleAAdapter,来实现接口Func2Inf。见图5


图5 增加ModuleA的适配层

从图5看,好像对ModuleA的依赖又变成了循环的,但其实不然。我们要把ModuleAAdapter看作ModuleA的一部分,则ModuleA和ModuleEx之间仍然是单向依赖。只不过ModuleAAdapter的实现由ModuleA的使用方来进行。ModuleAAdapter会随着ModuleA的变化而变化,但是也仅限于ModuleAAdapter的变化,我们用维护ModuleAAdapter这一层的代价,保护了ModuleEx的稳定性。

我们继续演化,假设需要用ModuleB替代ModuleA,则ModuleB可能没有办法直接调用ModuleEx中的FuncEx,需要额外增加一点修改才能供ModuleB使用。这是一种典型的需要增加适配层的场景。因此,增加一层对ModuleEx的适配层。见图6


图6 增加ModuleEx的适配层

至此,整个问题的解决方案已经讲述完毕,最后,我们需要用不同的层次来看待这张图,从代码维护者的角度划分和从依赖关系的角度划分,会存在两个架构边界,见图7。很多人可能纠结于维护边界上的循环依赖而裹足不前,而本案例中,承认这种循环依赖的无害性,完美地解决了面临的问题。

图7 两个边界

有人会追问,这种场景下到底能不能设计成明确的单向依赖?我们通常意义上说的单向依赖,一般是组件间的依赖,它们之间应该有较少的接口依赖,往往比较容易做成易变的依赖稳定的组件。即便有少量的循环依赖,也可以通过插入接口等方式实现依赖导致。

本文中的场景,更多的是同一组件内的模块间的依赖,理论上讲,如果一个组件属于同一团队开发,内部模块间的依赖,应该属于正常依赖。但是由于本文的特殊场景,开源三方和自研特性的开发者不属于同一个团队,所以通过增加接口和适配层,让变化局限在适配层这部分,从而最大限度保护核心业务逻辑不受影响。从编译构建的角度看,它们其实仍然是一个整体,任何一块都无法单独构建运行,其实归根结底还是存在依赖,只不过从开发的视角,代码隔离开来了。但是把代码隔离开来,正是本文场景中最迫切要解决的诉求。

你可能感兴趣的:(架构整洁之道--跟开源三方软件划清界限实战案例)