关于组件化链路设计的分享

简介: 关于组件化链路设计的分享

🙋🏻‍♀️ 编者按:本文作者是支付宝技术部 ATeam 后端工程师肆合。支付宝技术部 ATeam 旨在挑选优秀技术应届生,通过集中「精兵辅导」+「项目历练」,打造支付宝技术的高潜作战力量团队,培养支付宝技术部未来的核心人才梯队!在这里我们以公司级核心战略项目为重要练兵场,围绕平台基建/数智化专项/业务战役,通过多维技术成长体系助力同学全面成长,打造高精尖战力的“未来之星”!


提高代码代码可读性、复用性、可扩展性,从而提高开发体验和效率是基础素养。减少重复代码,对重复代码进行抽象、下沉,遵守设计原则,应用设计模式,都有一个共同的目的:发现变化,封装变化,提高代码的可复用性,减少需求变化影响的范围,从而使软件、系统、云服务、网站等能够可控的修改与升级,具有更长的生命周期。

  一、最常见的三层架构

以我们平常接触较多的三层架构开始:biz-core-common。

在开发的过程中,从三层架构的角度考虑,最简单的认识便是对于业务逻辑,在 biz 层去编排处理。而对于一些与业务逻辑无关,可复用的逻辑,从业务逻辑中抽离出来,在 core 层进行处理。底层的模型、DAO 方法、外部服务的门面等,放在 common 层处理。这样对于新增的业务,可以复用 core 层的通用方法。

这是最简单的理解与要求,在这之上,很多应用都根据自身的特点,从不同的角度用不同的方法提高代码的可复用性。本文则介绍一种组件化链路设计思想。

  二、为什么要用组件化链路

假设这样一种场景,有一种产品,处理的逻辑很复杂,代码很长。随着业务的发展,这个产品衍生出很多其它具有类似性质,但处理链路各有差异的产品。此时,怎样去设计代码的结构,才能更好地提高代码的复用性呢?

想象一个产品,对于一个业务流程,就要涉及复杂的逻辑,要经历十余种不同的业务处理阶段,还要考虑一些不影响主链路的弱依赖逻辑。随着业务的发展,又衍生出了不同的产品,彼此之间的逻辑有相似但又有差异。例如对于一个专门处理码的系统,各种各样的条码、二维码、在线码、离线码,需要定制的逻辑就有百余种。每一种码的每一个流程之间都有差异性,但也有共性,若不做特殊处理,每一种码都在 biz 层编排业务逻辑,复用 core 层的方法,其开发的复杂性也是很大的,并且随着产品的持续增多,代码中的分支会越来越多,影响理解、维护与新增。

那么如何最大化的把通用逻辑抽离出来,提高其复用性、可维护性以及可拓展性呢?组件化链路的设计是其中的一种方式。

  三、组件化链路设计

3.1 组件化链路思想

对于涉及多个业务处理阶段的逻辑,若每个阶段之间有所联系,但又彼此独立,便可以把每个流程抽象成一个个节点,对于一个特定业务流程而言,每一个子流程都可作为一个节点,依次执行流程节点,传递节点参数即可。如下当我们把所有业务处理的子流程都抽象成节点以后,每一个业务流程的处理便可以通过节点间的排列组合去完成。

<!-- 处理流程一 -->
 <bean id="xxxProcess" class="com.xxx.xxx.component.service.model.ProcessModel">
  <property name="name" value="xxxProcess" />
  <property name="nodes">
   <list>
                 <!-- 第一个node -->
    <bean class="com.xxx.xxx.component.service.node.wrapper.NodeWrapper">
     <property name="node" ref="xxxnode" />
     <property name="nodeParametersMaping" ref="xxxMapping" />
    </bean>
                 <!-- 第二个node -->
    <bean class="com.xxx.xxx.component.service.node.wrapper.NodeWrapper">
     <property name="node" ref="xxxxxNode" />
     <property name="nodeParametersMaping" ref="xxxxxMapping" />
    </bean>
                 <!-- 第三个node -->
    <bean class="com.xxx.xxx.component.service.node.wrapper.NodeWrapper">
     <property name="node" ref="xxxxxxxxxxNode" />
     <property name="nodeParametersMaping" ref="xxxxxxxxxxMapping" />
    </bean>
    <bean class="com.xxx.xxx.component.service.node.wrapper.NodeWrapper">
     <!-- 等待跳转节点 -->
     <property name="node" ref="xNode" />
     <property name="nodeParametersMaping" ref="xMapping" />
    </bean>
    <bean class="com.xxx.xxx.component.service.node.wrapper.NodeWrapper">
     <property name="node" ref="xxNode" />
     <property name="nodeParametersMaping" ref="xxMapping" />
    </bean>
   </list>
  </property>
 </bean>

NodeWrapper 便是每个子流程抽像出来的节点,对于一个完整的业务处理流程xxxProcess,只需要去依次处理每个节点的业务逻辑就好了。通过这样的方式,便可以通过一个统一的流程处理方法,执行所有组件化的业务流程,如下所示。

public ProcessContext execute(ProcessContext context, ProcessModel process) {
        LoggerUtil.info(LOGGER, "开始执行流程, processName=", process.getName(), ".");
        /**
         * node处理
         */
        context = nodeProcess(context, process);
        /**
         * extension处理
         */
        extensionProcess(context, process);
        return context;
    }

通过业务上下文 context 保存每个节点执行的结果并向下传递,首先执行 nodeProcess 主链路强依赖的节点,节点的执行结果影响链路推进,而后 extensionProcess 执行弱依赖的节点,执行失败并不影响主流程。这样我们对于业务逻辑的处理就变为了根据传参识别业务身份->执行对应流程->返回业务执行流程上下文 context。而对于业务流程的处理,都只用在xml配置一下 node,便可复用现有的逻辑。

更进一步地,对于 ProcessModel 以及 NodeWrapper,我们应该怎么设计才能承载上述的功能实现?

3.2 流程节点的具体设计

3.2.1 节点设计

ProcessModel 与 NodeWrapper 之间的关系如下

processModel 内部其实就是一堆节点,是一种一对多的关系,将强依赖的主链路节点与弱依赖节点分开,如下

public class ProcessModel extends ToString {
    /**
     * 流程名称
     */
    private String name;
    /**
     * 普通节点列表
     */
    private List<NodeWrapper> nodes;
    /**
     * 扩展节点列表
     */
    private List<ExtensionWrapper> extensions;

自然,节点执行的能力要交予节点自己,一个节点的内部参数如下

public class NodeWrapper {
    /**
     * 业务节点名称
     */
    private String nodeName;
    /**
     * 业务节点
     */
    private ProcessNode node;
    /**
     * 参数转化
     */
    private NodeParametersMaping nodeParametersMaping = null;
}

一个节点会有节点名称 nodeName、真正的执行节点 node、以及参数转换的 nodeParametersMaping。每个参数的设计都有其特殊的考虑,首先是业务节点名称,这个参数有什么意义呢?其实是在业务执行的过程中,可能会有分支链路的出现,根据不同的结果,可能会跳过一些节点。此时便可以指定下一个执行节点的 nodeName,实现节点跳转的功能。

对于 ProcessNode 来说,它才是严格意义上的执行节点。

public interface ProcessNode {
    /**
     * 入参. 每个节点发布的时候,必须定义出入参数表.
     *
     * @param params
     * @return
     */
    ProcessNodeResult process(Map<String, Object> params);

processNode 是一个接口,每个节点给予其具体实现。此处的入参是Map<String, Object> params,上文已经说到,节点处理是通过 context 传递的,但对于每个节点而言,并不需要所有 context 的所有参数。所以对于下一节点执行的时候,只需通过 context 取出需要的部分执行即可。这引发了下一个问题,如何设计一种通用的方法,去转换参数呢,这就是 NodeParametersMaping所要做的。

public class NodeParametersMaping {
    /**
     * context to node 的转化配置
     */
    private Map<String, String> contextToNode = null;
    /**
     * node to context 的转化配置
     * Map<结果码,Map<结果属性,上下文目标设置属性>>
     */
    private Map<String, NodeToContextMaping> nodeToContextMapping = null;
    /**
     * 配置死的静态入参 key = params的key
     */
    private Map<String, Object> staticParamsToNode = null;
}

这里 contextToNode 便是用于从 context 中取出某些参数,转换成节点入参的配置。

nodeToContextMapping 便是节点执行结果放进 context 的配置。那这里为什么是一个 Map<String, NodeToContextMaping> 类型的数据呢,主要是因为节点执行可能成功可能失败,失败的结果也有很多,此处主要是根据不同的执行结果,取出相应的 NodeToContextMaping,往 context 里填充数据。NodeToContextMaping 如下

public class NodeToContextMaping {
    /**
     * Node中的result.resultCode转化成流程里的code
     */
    private int processCode = -1;
    /**
     * 后续如何处理
     */
    private NodeProcessMethod processMethod = NodeProcessMethod.nextNode();
    /**
     * Node中的result结果转化到contextdata中
     */
    private Map<String, String> nodeToContext = null;
    /**
     * 操作类的
     */
    private Map<String, String> contextToContext = null;
    /**
     * 静态参数设置到context中
     */
    private Map<String, Object> staticParamsToContext = null;
}

这里的 processMethod 便是用于指定下一个执行节点的,是执行下一个执行节点,还是跳转节点。nodeToContext 便是将节点执行结果放进 context 的配置文件,不同的执行结果会有不同的配置。该配置也是事先在 xml 中配置实现的

<property name="nodeToContextMapping">
   <map>
    <entry key="1">
     <bean class="com.xxx.xxx.component.service.node.wrapper.NodeToContextMaping">
      <property name="processCode" value="101" />
      <property name="processMethod">
       <bean class="com.xxx.xxx.component.service.node.wrapper.NodeProcessMethod">
        <property name="nodeProcessWay" value="ERROR_NODE"/>
       </bean>
      </property>
     </bean>
    </entry>
    <entry key="3">
     <bean class="com.xxx.xxx.component.service.node.wrapper.NodeToContextMaping">
      <property name="nodeToContext">
       <map>
        <entry key="bizObj.result" value="result"/>
       </map>
      </property>
     </bean>
    </entry>
   </map>
  </property>

以上述为例,当 key=1 时,说明发生错误,该节点 processMethod 是ERROR_NODE 不需要向下执行。当 key=3 时,将执行结果中的 result 放入context 中的 result,并且 processMethod 是默认值,执行下一节点。

至此,关于节点的设计思路便结束了。通过这样的设计,节点执行的顺序、不同的分支逻辑都可以覆盖到了,并且对于新增业务逻辑,在 xml 中配置即可,不需要重新编排复杂的业务逻辑。此时又有一个问题,context 和 node 间具体传递参数的逻辑是怎样实现的呢?

3.2.2 参数转换的实现

context 是一个业务执行上下文,上下文类内中包含了所有执行过的节点结果,那是如何设计一个通用的转换方法,使得只需要在 xml 中配置参数转换 map,就可以实现节点参数与 context 之间的参数转换的呢?以一个 context 中取参数转换 node 入参的 case 为例,一个 contexToNode 的定义如下

<bean id="xxxxxMapping"
  class="com.xxx.xxx.component.service.node.wrapper.NodeParametersMaping">
  <property name="contextToNode">
    <map>
      <entry key="modelInfo.index" value="index" />
    </map>
  </property>

这里的配置的 key:modelInfo.index,表示的含义是从 context 中取值,相当于取出 context.modelInfo.index,而 value:index 表示的含义是节点 node 的入参 params 的 key。大概的含义是 params.put(index, context.modelInfo.index)。具体的实现方式为

public void toNode(ProcessContext context, Map<String, Object> params)
    throws IllegalAccessException,
    InvocationTargetException,
    NoSuchMethodException {
    if (MapUtils.isNotEmpty(contextToNode)) {
        Iterator<Entry<String, String>> it = contextToNode.entrySet().iterator();
        while (it.hasNext()) {
            Entry<String, String> en = it.next();
            String sourceProperty = en.getKey();
            String targetProperty = en.getValue();
            Object val = PropertyUtils.getProperty(context, sourceProperty);
            PropertyUtils.setProperty(params, targetProperty, val);
        }
    }
}

通过 PropertyUtils.getProperty() 方法,取出 context.modelInfo.index 的值,再通过 PropertyUtils.setProperty() 方法,将值放入 params 当中。从而实现 params.put(index, context.modelInfo.index) 的作用。

  四、总结

组件化链路是一种解决节点间复用问题的设计思想,在老链路的基础上,提高了代码复用性、可维护性以及可拓展性。某种特定的方法并不是最重要的,最重要的还是提高代码复用性,解耦的思想。


相关文章
|
2月前
|
缓存 监控 安全
构建高效后端系统的最佳实践
本文将深入探讨如何构建一个高效的后端系统,从设计原则、架构选择到性能优化等方面详细阐述。我们将结合实际案例和理论分析,帮助读者了解在构建后端系统时需要注意的关键点,并提供一些实用的建议和技巧。
41 2
|
Java API 数据库
基于 SOA 的组件化业务基础平台
原文:基于 SOA 的组件化业务基础平台 前言 业务基础平台是业务逻辑应用和基础架构平台之间的一个中间层,解决 “应用软件的业务描述和操作系统平台、软件基础架构平台之间的交互与管理问题”。
2086 0
|
18天前
|
前端开发 JavaScript API
组件化设计有哪些缺点吗
【10月更文挑战第22天】组件化设计有哪些缺点吗
|
18天前
|
前端开发 JavaScript 物联网
组件化设计适用于哪些场景
【10月更文挑战第22天】组件化设计适用于哪些场景
|
18天前
|
前端开发 JavaScript UED
什么是组件化设计
【10月更文挑战第22天】什么是组件化设计
|
16天前
|
前端开发 API UED
深入理解微前端架构:构建灵活、高效的前端应用
【10月更文挑战第23天】微前端架构是一种将前端应用分解为多个小型、独立、可复用的服务的方法。每个服务独立开发和部署,但共同提供一致的用户体验。本文探讨了微前端架构的核心概念、优势及实施方法,包括定义服务边界、建立通信机制、共享UI组件库和版本控制等。通过实际案例和职业心得,帮助读者更好地理解和应用微前端架构。
|
2月前
|
移动开发 缓存 前端开发
构建高效的前端路由系统:从原理到实践
在现代Web开发中,前端路由系统已成为构建单页面应用(SPA)不可或缺的核心技术之一。不同于传统服务器渲染的多页面应用,SPA通过前端路由技术实现了页面的局部刷新与无缝导航,极大地提升了用户体验。本文将深入剖析前端路由的工作原理,包括Hash模式与History模式的实现差异,并通过实战演示如何在Vue.js框架中构建一个高效、可维护的前端路由系统。我们还将探讨如何优化路由加载性能,确保应用在不同网络环境下的流畅运行。本文不仅适合前端开发者深入了解前端路由的奥秘,也为后端转前端或初学者提供了从零到一的实战指南。
|
2月前
|
XML Java 数据库
在微服务架构中,请求常跨越多个服务,涉及多组件交互,问题定位因此变得复杂
【9月更文挑战第8天】在微服务架构中,请求常跨越多个服务,涉及多组件交互,问题定位因此变得复杂。日志作为系统行为的第一手资料,传统记录方式因缺乏全局视角而难以满足跨服务追踪需求。本文通过一个电商系统的案例,介绍如何在Spring Boot应用中手动实现日志链路追踪,提升调试效率。我们生成并传递唯一追踪ID,确保日志记录包含该ID,即使日志分散也能串联。示例代码展示了使用过滤器设置追踪ID,并在日志记录及配置中自动包含该ID。这种方法不仅简化了问题定位,还具有良好的扩展性,适用于各种基于Spring Boot的微服务架构。
49 3
|
2月前
|
JavaScript 前端开发 测试技术
动态组件化的优缺点是什么
【9月更文挑战第2天】动态组件化的优缺点是什么
46 4
|
3月前
|
存储 JavaScript 前端开发
如何在组件化中实现组件之间的通信
【8月更文挑战第13天】如何在组件化中实现组件之间的通信
49 3