老系统改造不是一蹴而就的,从2022接手版权资产管理-财资系统之后,一直在进行架构重构和稳定性建设。将新吸纳的优秀架构经验融合,以业务为中心随着需求迭代进行老系统的架构升级。过程中实践和沉淀了一些不错的B端开发方法论,整理成“老房改造系列”分享给大家。
《老房改造系列--上线十年,81万行Java代码的老系统如何重构》
《老房改造系列--如何用一套流程接入所有业务线》
前言
ToB业务没有太多高并发的挑战,但同一套流程往往可能需要承载各种差异化的复杂业务需求,所以如何让系统具备良好的扩展性成为ToB业务系统最大的挑战。以版权资产管理-财资系统举例,横向需要承接财、法、商、boss四类角色,纵向要支持十几条不同频道业务线,这样粗略计算会有近百维差异化业务需求,这会比C端的用户画像要复杂的多。如何用一套流程接入所有业务线?
之前在《上线十年,81万行Java代码的老系统如何重构》一文中有提到过,老系统重构的时候通过自上而下的方法进行流程拆解,再通过模板模式,用继承重写差异化method的方法进行差异化扩展。这种方法可以解决代码臃肿问题,也可以进行快速的扩展。但是当接入流程逐渐增多、流程差异化大小不一,会再一次引入一些坏味道。
问题
还是以付款模块举例,付款作为整个业务流程中最末端的节点,从流程图上可以看出来付款本身并没有特别复杂的业务逻辑,但是需要支持的业务却特别的多,那就意味着付款这个服务必须要有很好的扩展能力才能支持快速接入。
第一版重构时使用的是模板模式进行扩展,但这样设计会产生一些问题,首先从业务扩展实操上来说,使用模板模式进行扩展会出现俩个问题。
1、继承关系复杂:
随着接入业务逐渐增多继承关系会越来越“胖”或者越来越“高”,当一个新的扩展品类来的时候,我们都需要决策一件事情,新来的品类是继承最根部base类(变胖),还是找一个实现逻辑最相近的类来继承(变高)。变胖带来的后果就是复用性不好,一样的实现逻辑可能会出现在多个流程中;变高带来的后果是父类实现的修改有可能会影响子类的业务。
2、粒度过粗
当使用继承重写的方式进行扩展的时候,必须重写整个方法。假设流程某一个步骤的method的实现中有3条规则,而新的流程步骤中只有第1条规则与父流程不同,但由于继承关系的限制另外2条规则需要复制过来,这就又产生了重复逻辑代码,影响代码复用性。
设计模式中也提到过“多用组合,少用继承”的建议,原因如下
总结来说,“多用组合,少用继承”强调的是以聚合的方式来组合对象的能力,这样可以构造出更灵活、松散耦合、易于维护和扩展的设计模式。当然,在具体实践中,并非完全排除继承,而是倡导根据实际需求权衡使用,对于“is-a”的语义关系合理使用继承,而对于“has-a”或者“can-do”的能力可以通过组合来实现。
扩展体系建设
为了解决上面的问题,做了几件事情:梳理流程差异点,梳理领域模型,二次抽象隔离层,基于SPI的扩展体系建设。
梳理流程差异点
还是前面那张图,重新梳理后把有差异的部分标出来。
业务流程:
- 付款发起的来源很多,而且后续会越来越多;
- 基于不同业务对于付款单的校验逻辑也会有差异;
- 提交审批后由于业务线不同,审批流初始化和审批流程肯定有差异。
View层:
- 付款单中要展示各自的业务单据信息,用来辅助审批人决策;
- 填写表单的时候,下拉字典根据业务需求会有不同的选择范围;
- 风险提示的卡片也会跟各自业务相关。
权限控制:
- 基于身份的权限控制是固定的,但是有业务单据权限必然要有能查看基于该单据发起付款的权限。
消息同步:
- 每个业务对于付款流程中的消息的消费也各有不同。
有了这张图之后我们就知道流程中所有的业务差异点,可以辅助无遗漏的抽象扩展点。
梳理领域模型
第二步进行了领域模型的梳理,梳理后可以看出来付款的核心域中,付款单据是聚合根。各个业务都可以发起付款,所以其他领域都算是付款的支撑域。但由于之前的付款单中会聚合引入其他域的单据作为付款依据,所以会导致每次接入新的业务类型,都需要重新引入新的业务单据,这也就导致了付款核心领域是不稳定的。核心域不稳定带来的结果就是每次大量的升级和兼容,那怎么样把不稳定因素隔离开?
二次抽象隔离层
如上图所示,这里二次抽象出了付款凭证,付款单据的后续流程只依赖付款凭证,从其他业务域单据到付款凭证的Translator可以放在业务域来实现。这样当付款域接入新业务时,核心域的代码是稳定不变的,即减少了兼容逻辑代码也可以保证付款流程的稳定性。
现在解决了核心域代码稳定的问题,但还是会有很多不同业务带来的流程差异问题,如下:
基于SPI的扩展体系建设
既然这些都是根据不同业务会带来的差异,那我们可以将定义和实现倒置,由付款域定义接口,业务域来实现差异的部分,所以直接使用SPI的方式来扩展。行业上有很多SPI的实现方案,如何选择?
方案需要满足俩个条件:
1、根据业务身份获取扩展实现
2、接入成本:由于我们是老系统改造,所以改造要考虑ROI
JAVA SPI | Spring-SPI | Dubbo\Hsf-SPI | TMF-SPI | COLA-SPI | |
优势 | 原生 | 自动装配 | 天然支持RPC调用、分布式功能 | 支持业务身份自动映射、支持批量扩展点与顺序等,功能强大 | 支持业务身份自动映射、实现简单、支持本地和HSF扩展点 |
劣势 | 每次加载全量、不支持业务身份自动映射、非线程安全 | 不支持业务身份自动映射 | 不支持业务身份自动映射 | 配置复杂、封装能力层带来理解成本、老系统改动成本高 | 不支持批量扩展点与顺序 |
由于前三个不是天然满足需求1,如果想用需要二次开发,付款中对于批量扩展点的需求很少,所以基于以上方案选型对比最后选择COLA-SPI进行扩展体系建设。大家可以根据自身的业务来选择合适的方案,没有最完美只有最合适。
最终扩展体系建设之后的架构图如下:
付款的应用架构上在流程编排层和Domian层之间加入了扩展层,付款流程中调用的差异化部分,付款域来定义接口,接入业务域来做实现;在过程当中用到的一些付款域的通用方法,付款域来定义接口,付款域来实现。
每个接入的业务域,都将业务差异化逻辑封装到二方包里,通过maven仓库加载到付款域中。付款域应用启动时会将扫描所有扩展点实现,并将业务身份与扩展点实现绑定( Map<Scenario,I***ExtPt> ),放入SpringContext中。一个付款流程运行时,会根据业务身份来mapping响应扩展点实现。cola-spi支持三维坐标系,具体使用可以参考cola文档。
扩展点的方案毕竟是要引入外部代码和调用外部服务,所以一定保证安全性和稳定性:
开放原则:如图不详述
业务身份安全:二方包是通过maven仓库引入进来,所以必须要保证二方包中有配置业务身份,并且保证引入的是release版本,这样可以保证在不修改版本的前提下,业务身份是安全的。
数据隔离:大部分B端业务系统都是要求有严格的数据隔离的,一旦被破坏会带来数据泄露、合规等风险。
MQ消息隔离:传统的解决方案是把所有消息有生产方发送,统一发送到一个topic下,通过tag信息来做隔离,但这样就意味着可以接收到所有业务身份下的消息。为了解决这个问题,这里使用的方案是把消息发送的职责交给接入方,生产方只负责在可以发消息的流程节点去调用扩展方法,传入单据信息,至于发送到哪个topic、发送哪些信息、是否发送都有接入方扩展实现。
接口查询隔离:由于扩展方法在实现过程中,一定会用到一些通用方法,比如反查历史单据信息等。此时如果接口不做数据隔离,那就有可能返回其他业务身份下的数据。所以需要拦截器进行方法参数的拦截,并且和业务身份进行校对。
环境隔离:如图不详述
逻辑稳定性:线上二方包引入release版本,可以保证包内逻辑的稳定和调用的RPC服务版本稳定,并且在所有扩展方法外进行异常处理。但还是会有RPC服务升级带来的逻辑问题,目前没有万无一失的方法,只能从三方面来努力。
1、前:严格遵守开放原则,并且在接入文档中说清楚返回结果的用途。
2、中:对扩展点接入自动化测试手段
3、后:加入扩展点坐标系粒度的开关控制,发现问题及时止损流程依赖扩展点:如图不详述
总结
为了进一步解决流程差异化给B端服务带来的挑战,在老系统改造的场景下通过以下四个步骤来升级架构:
①梳理流程差异点,找到所有业务流程有差异的部分
②梳理领域模型,找出核心域和支撑域的边界
③二次抽象隔离层,保证核心域的逻辑稳定
④基于SPI的扩展体系建设,实现基于不同业务身份的流程差异
提升了业务流程的可扩展性,解决了之前架构使用模板模式带来的变“高”变“胖”的问题。同时扩展体系将接入的业务逻辑交给接入方,也解决了对核心域开发人员对支撑业务域理解不审和核心域开发人力有限的问题。
我相信这也不会是最完美的解决方案,随着业务量的增多一定会带来新的问题,我们需要做的就是不断的学习和吸纳,将新的思想融入到解决方案中,让系统架构和开发者都处在一个良性的循环中。
来源 | 阿里云开发者公众号
作者 | 墨诩