在讨论标题问题之前,可以先看一个 VS Code 调试器使用适配器例子:https://code.visualstudio.com/api/extension-guides/debugger-extension

若要实现一个 VS Code 的调试器,调试器进程通过 Debuger Adapter 库以调试协议与真实的 VS Code 通讯交互来实现。
通过中间的适配器层,调试器与 VS Code 调试UI相互不知道对面的具体实现。
VS Code 只要关注适配器发出的所有协议来作调试响应,而调试器不关注 VS Code 是如何处理这些调试命令的。双端都不知道对面如何处理这些信息,也不必关心。
线性开发逻辑
以游戏中系统中最最基本的背包系统为例子,背包系统中所有的道具物品可以划分为两大类:
1.以标记ID和数量就能描述的普通道具 (id-count items) 。
2.来源于同个配置模板但是实际会发生其它扩展变化的实例道具 (instance items)。

按照线性开发逻辑,道具模块会提供添加和删除的接口提供给外部模块使用。在这里外部模块A调用了普通道具模块进行添加了普通道具1000,和调用英雄模块添加了一个新的英雄。

但是实际情况会比这个复杂。因为这里没有处理当英雄无法被添加的情况。这样子正常应该要么都添加,要么都不添加,不够事务性。所以正确的版本应该是这样:

这里模块A承担了太多东西:
1.模块A直接关注了背包的具体类型,直接操作了各种不同类型实例道具的接口(它们的用法可能一样也可能不一样),耦合了他们。重构这些接口时,需要关注这些接口的直接使用者的使用方式是否兼容新写法,如果不兼容则需要重新修改它们,带来额外测试成本。
2.模块A额外处理了失败情况,这部分代码并不能被其它逻辑复用。
3.添加新的类型的实例道具类型时,需要修改模块A得以支持新增的实例道具添加支持。
4.若某种道具添加的逻辑过于特殊时(例如此道具永远不会溢出,自动转化为货币),这些特殊操作的额外关注成本会分摊到模块A中。
来计算一下所有道具操作的成本,成本=操作数*道具总类型数量(普通道具+实例道具种类):1*2=2。当操作来源扩充到20时,道具种类为10时,成本为200。
当然实际开发并不会是这样非得每个操作道具的地方一个个全写上所有类型的判断。
根据需求驱动,开发这些功能的行为模式更像是下面这样:第一个人加上普通道具,第二个人加上英雄类道具,第三个人加上产出英雄和普通道具的地方,第四个人加上武器类道具,测试第四个人的功能时发现第三个人加上的地方无法产出武器,第三个人返工(这时可能还不用加班,顺手的事情)。
当有多个地方都以各种排列组合的方式固定类型:ABCD,BD,AC,AD, … 来写代码时(这些各种排列组合的来源往往是当时的需求导致的,因为当时只要这样,够用就好了)出现了应该需要在各个地方应该也要支持新的类型道具添加的,但是没有支持。
提需求的人默认应该要支持,实现功能的人默认不支持。工作量空档就此出现,当这些空档积攒到一定程度时,奇迹发生了!
没有人愿意一次还掉这个技术债(比这个还可怕的是,用的是脚本语言写的这个功能。更可怕的是这个功能是写在服务器的),永远只能打补丁,除了上述的流程。这些工作量还会奇迹的变麻烦:
1.整理出哪里需要支持新增的道具类型的代码。
2.找出来的地方全部加上。(新增道具的人加?他也不清楚对应的地方的逻辑额外需求理解成本。原有功能的负责人加?我写的时候可是好好的。)
3.回归全测试一遍。
4.上线胆战心惊。
上面所有步骤大概率还要带上一个加班,工作量花出去了,而且实际问题从未解决。
添加一个适配器层
其实解决上述问题只要添加一级背包适配器层:

有了这个适配器层。
对于适配器功能提供者:
1.并不关心哪些外部功能使用了适配器,新增的功能只要好好服务于适配器即可。
2.重构相关道具时,外部模块并不可见,不会带来额外工作量。
对于适配器接口使用者:
1.未来提供的新的功能,一定会在原有接口上得到支持。外部模块只需要关注适配器提供的接口,而且适配器的接口数量并不会根据需求增长。
2.无论有多少个其它外部模块,使用适配器接口的位置永远是线性增长的。减少了外部模块需要关注的问题。
结尾
其实这个问题只要首次写的那个人注意到此问题,或者还没变成更大的问题通过比较小的工作量即可解决。
这些问题有一个明显的特征:功能使用者,和功能提供者的数量都会不停增长。新增的功能可以通过统一的参数去描述这些功能,而不是必须直接使用新增的接口。

对于文章开头提到的 VS Code 的例子并不是解决这个问题,而更类似于实现一个跨进程的interface。
对于游戏系统开发,除了背包之外,还有这些系统有类似的问题:条件判断、任务等。提前发现问题解决起来其实并不困难。