随着业务渠道及产品的增加, 你的代码是否开始陷入 IF-ELSE 组成的泥潭, 难以脱身?
持续增加的渠道特性
小码同学一来到新公司, 就负责起了一个新开始, 但具有无限想象空间的后台开发项目. 就像所有的互联网项目一样, 业务变化极其迅速, 为了减少初期试错成本, 小码同学选用了流行, 便捷的贫血模型, 也就是 Service+DAO/RPC 结构, 做了简单的关注点分离 -- 业务以及基础设施 (存储 / 远程服务) 的分离.
业务很给力, 主要流程模式已逐渐成型, 同时也增加了很多的营销渠道, 有公司内部的 App, 有公众号, 小程序, H5, 也有各类外部的合作伙伴的渠道, 小码同学一直都在高负荷地工作着, 完全来不及思考要怎么优雅地解决这些渠道增加带来的问题. 然而, 每个渠道会有一个渠道相关的小特性, 这意味着在 登录, 注册, 做业务等等各个 Service 里, 每增加一个渠道时, 都要增加一段关于渠道判断的 IF-ELSE 判断语句. 量变引起质变, 当渠道加到近十个时, 小码懵逼了, 理清代码逻辑脉络变得极为困难, 因为看一遍代码, 需要将要受到近十个不同渠道分支代码的干扰. 同时代码也变得难以并行开发, 多个渠道的拓展会因为同一个 Service 的修改而更容易发生冲突.
其实这里小码可能会采取另外一种做法, 陷入另外一种困境, 小码同学每增加一个渠道就将原来的代码复制一份, 然后针对渠道进行简单的修改, 然后就可以安全高效地完成业务需求了. 然而一时爽, 一直复制一直爽, 当我们需要修改一些渠道公共实现, 理清不同渠道实现的区别并修改时, 近十个渠道就会让我们就变得痛不欲生了.
公用逻辑下沉解决方案
小码同学想了下, 无论多忙都要从这个困境中破局, 于是他想出了以下方案:
将公共的逻辑下沉, 将各个渠道特有的判断及逻辑都上提. 如此一来我们可以从代码中分离渠道和公用业务逻辑 -- 要理解渠道特性, 我们可以从渠道所在模块 (微服务 / package/service) 的代码得知, 如果要理解通用的信息, 则到公共
业务逻辑层查看对应的实现.
但若要使用本解决方案解决目前系统的问题, 则需要引入大量的重构, 因为该实现需要将大量已有存在的渠道逻辑变更其发生的逻辑时间点, 这需要大量的开发及测试人力支持.
扩展点解决方案
于是小码同学开始在网上搜索相关的解决方案, 了解到阿里有个可以解决类似问题的框架实现 COLA, 并以此为参考开展了自己的扩展点设计
https://github.com/alibaba/COLA
其引入了一种名为 扩展点 / 插件 的机制(扩展点是一个 Interface, 扩展点实现为 interface 的一个特定实现), 让我们可以达到以下效果:
要实现这个效果, 强制我们把相关业务语义显式化, 例如 通用业务逻辑 Service 在没有引入扩展点前, 写的校验身份的代码为
if(is 渠道 B) {
渠道 B 的一大堆代码进行身份校验
} else {
默认实现的一大堆代码进行身份校验
}
而引入扩展点后, 在通用业务逻辑 Service 写的则是
校验身份扩展点. 执行校验();
其显式化了业务语义, 并像 公用逻辑下沉 的解决方案一样 分离了主干的代码逻辑和特定实现的代码逻辑, 还能保证原有特定渠道逻辑执行的相对位置不变.
在扩展点机制的支持下我们只需要定下规范 -- 在通用业务逻辑层不能出现任何关于 Channel 渠道的 IF-ELSE 判断, 这样就可收获大量有基础抽象的通用业务逻辑代码, 提高识别基础抽象的能力.
扩展点解决方案的本质
扩展点本质上就是个带有自动路由功能的策略模式, 其根据业务上下文信息, 自动推断出应该选用哪个具体的扩展点实现.
扩展点的机制和原理简单, 使用也很简单, 但其给业务系统代码带来却是变革性的.
多维扩展问题的出现
小码同学利用扩展点已经阻止了渠道增加带来的代码腐化加剧问题, 小码以保守起见:
对于没有任何业务变化的代码保持不变
对于新增的渠道使用扩展点来防止代码进一步腐化
对于存在业务变动的代码, 在测试资源的支持下, 利用扩展点重构原有代码, 令代码变得新鲜
但新的问题出现了, 项目中也有一个与渠道类似的, 会不断扩展实现的概念 -- 产品.
在小码的系统中, 每增加一个产品也是按照类似先前增加渠道的形式, 以 IF-ELSE 完成扩展. 于是, 小码希望直接套用之前的扩展点机制, 然而事情并没有这么顺利.
在上一幅图中, contextCode 为
companyY.channelB
其表征的是渠道维度的身份信息, 我们以该信息为依据, 来匹配最适合的渠道相关的实现. 与此类似, 我们需要引入产品维度的身份信息, 同时也要将不同维度的 contextCode 加以区分, 然而 COLA 的实现中并不支持此类多维扩展实现, 于是小码开始设计了自己的 ConextCode 及 ImplementCode 规范, 增加了维度标识符:
- channel:companyY.channelB
- product:companyY.ProductO.subProductE
通过维度标志, 小码分开了不同维度的扩展点以及其对应的实现, 解决了大部分的问题.
多维扩展点冲突问题
引入多维扩展点后, 大多数扩展实现都在各自的维度良好运行, 井水不犯河水. 然而, 有少数扩展点却出现了需要多维同时决定实现的场景, 如:
当前若是渠道 A 且是产品 X 的情况下需要使用特定的扩展实现.
目前基于维度隔离的扩展点感觉无法支持此类需求, 于是, 小码继续查找相关资料, 了解到了阿里 TMF2.0 框架的实现:
https://segmentfault.com/a/1190000012541958
TMF2.0 按文中介绍, 其为一个二维的扩展点实现, 这两维分别是:
行业维度
一个行业维度的代码即为 TMF2.0 的业务身份
其等价于小码之前设计的没带维度标志的 contextCode
产品维度
与本文中的产品设定完全不同, 请勿套入理解
与小码之前设计的维度隔离扩展点不同, TMF2.0 中行业维度与产品维度会共享扩展点, 然而当出现扩展点冲突时, TMF2.0 会以业务身份为线索, 通过可视化界面配置特定业务身份在遇到扩展实现冲突时应该选择哪一个扩展实现, 并将其固化成配置, 运行时依据配置选择最终扩展实现.
然而该设定并不符合项目的现状, 相关 UI 的开发设计也是一个巨大的工程, 因此小码设计了另外一种折中的设定:
扩展点依然按维度隔离
当出现扩展点路由需要多维信息决策时, 采用嵌套形式
不同扩展点不同维度的嵌套的次序, 都按照固定的次序进行
如下图所示
这个设定的实现虽然繁琐, 但是实际情况下, 出现多维共同干预扩展实现选择的情况应该相对少, 相对于扩展点维度隔离得到的好处, 其应该可以接受.
舒心的小码
扩展点的理论简单易用, 其使得
业务主流程代码拥有基础抽象, 使得脉络清晰明了
各个渠道, 产品的特性高度内聚于各自的 package 中
业务主流程成熟后, 新增的产品, 渠道再也不需要动业务主流程代码, 只需增加新的渠道包及产品包
业务主流程代码没变动, 减少了全局风险, 减少了测试量
扩展只涉及对应的渠道, 产品的增加, 这天然隔离了不同开发组的工作, 提高了并行度
从此小码和他的小伙伴们从此摆脱了 996, 与基友们过上了幸福快乐的生活.
来源: https://www.cnblogs.com/skyesx/p/10995730.html