代码重构 复用+扩展

知识简介:在需求开发过程中我们发现,有一些流程相似的业务,存在功能重复开发的问题,没有沉淀出一套可复用可扩展的代码,浪费了很多开发人力。本文以3个业务流程类似的营销活动为例,讨论如何提升营销活动代码复用性和扩展性。

背景

某组目前承接了xx大量的营销活动,在需求开发过程中我们发现,有很多活动的业务流程是类似的,但是并没有沉淀出一套可复用可扩展的代码,功能的重复开发浪费了很多人力。下面我们针对X1、X2、X3这3个业务流程相似的活动,讨论如何提升营销活动代码复用性和扩展性。

业务流程

大数据会根据用户历史寄件数据生成寄件任务目标件量,在活动页面展示。用户在活动周期内达成目标寄件量后,即可领取对应的礼包。产品或运营同事会根据客群、AB实验等条件配置不同的礼包策略,以便更精准地激励用户寄件。

 

代码流程

注:流程图中颜色相同的矩形代表相同的功能。

满赠

 

中高散

 

季节性

 

问题

以上3个活动在代码实现上存在以下问题:

1. 数据存储散乱

礼包领取记录、礼包策略维护在不同的库表里。

2. 代码复用性、扩展性低

从上面的流程图可以看出,活动首页数据的组装都会经历以下流程。但是3个流程大致相同的活动,却维护了3套代码在2个应用里。


因此,我们需要对代码进行重构,来提升复用性和扩展性。

 

目标

代码重构的目标是沉淀一套针对于寄件任务活动场景的通用代码实现,既提供可直接复用的通用能力,也提供一定的扩展点来支持定制化的业务逻辑。

 

解决方案

方案一:设计模式:模版模式+工厂模式+策略模式

  1. 抽象出寄件任务活动的模版方法,一些通用功能可以在该抽象类实现。
 

@Slf4j public abstract class AbstractSendTaskCommonService<T> implements InitializingBean { public IndexDataRespDTO buildIndexData(BaseRequest baseRequest) { // 省略部分代码 List<T> bdpData = queryBDPData(bdpDataReqDTO); SendNumDTO sendNumDTO = querySendNum(sendNumReqDTO); Map<String, Boolean> stateMap = queryPacketReceiveState(receiveStateReqDTO); Map<String, PacketStrategyDTO> strategyMap = queryPacketStrategy(strategyReqDTO); Map<String, List<PacketInfoDTO>> packetInfoMap = queryPacketInfo(strategyMap); return buildResult(bdpData, sendNumDTO, stateMap, packetInfoMap); } @Override public void afterPropertiesSet() { SendTaskServiceFactory.register(getType(), this); } public abstract String getType(); /** * 查询bdp数据接口 */ public abstract List<T> queryBDPData(BdpDataReqDTO reqDTO); /** * 查件量 */ public SendNumDTO querySendNum(SendNumReqDTO reqDTO) { // 省略部分代码 return sendNumDTO; } /** * 查领取状态 */ public Map<String, Boolean> queryPacketReceiveState(ReceiveStateReqDTO reqDTO) { // 省略部分代码 return stateMap; } /** * 查礼包策略 */ public Map<String, PacketStrategyDTO> queryPacketStrategy(PacketStrategyReqDTO reqDTO) { // 省略部分代码 return strategyMap; } /** * 查礼包信息 */ public Map<String, List<PacketInfoDTO>> queryPacketInfo(Map<String, PacketStrategyDTO> map) { // 省略部分代码 return packetInfoMap; } /** * 封装结果 */ private IndexDataRespDTO buildResult(List<T> bdpData, SendNumDTO sendNumDTO, Map<String, Boolean> stateMap, Map<String, List<PacketInfoDTO>> packetInfoMap) { // 省略部分代码 return indexDataRespDTO; } }

  1. 工厂类
 

public class SendTaskServiceFactory { private static Map<String, AbstractSendTaskCommonService> serviceMap = new ConcurrentHashMap<>(); public static AbstractSendTaskCommonService getService(String activityType) { if (!serviceMap.containsKey(activityType)) { throw new BusinessException(SERVICE_NOT_EXIST); } return serviceMap.get(activityType); } public static void register(String activityType, AbstractSendTaskCommonService sendTaskCommonService) { if (serviceMap.containsKey(activityType)) { throw new BusinessException(SERVICE_EXIST); } serviceMap.put(activityType, sendTaskCommonService); } }

  1. 活动类型枚举
 

public enum SendTaskActivityEnum { FULL_GIVE("FULL_GIVE", "满赠"), HIGH_DISP("HIGH_DISP", "中高散"), SEASONAL("SEASONAL", "季节性"), ; private String code; private String describe; SendTaskActivityEnum(String code, String describe) { this.code = code; this.describe = describe; } public String getCode() { return code; } public String getDescribe() { return describe; } }

  1. 每个活动可以通过重写AbstractSendTaskCommonService类方法的方式,实现自己的特殊业务逻辑
 

@Service public class HighDispService extends AbstractSendTaskCommonService<HighDispBdpDTO>{ @Autowired private BdpServiceRpcService bdpServiceRpcService; @Override public String getType() { return SendTaskActivityEnum.HIGH_DISP.getCode(); } @Override public List<HighDispBdpDTO> queryBDPData(BdpDataReqDTO reqDTO) { JSONArray jsonArray = bdpServiceRpcService.queryBDPData(reqDTO); if(SfCollectionUtil.isNotEmpty(jsonArray)){ return jsonArray.toJavaList(HighDispBdpDTO.class); } return null; } }

  1. 根据活动类型获取对应的Service,执行首页数据的组装
 

@Service public class HighDispRetrieveNewServiceImpl implements HighDispRetrieveNewService { @Override public Result<IndexDataRespDTO> index(BaseRequest reqDTO) { AbstractSendTaskCommonService service = SendTaskServiceFactory.getService(SendTaskActivityEnum.HIGH_DISP.getCode()); return SFResultUtil.success(service.buildIndexData(reqDTO)); } }

方案二:流程引擎

流程引擎可以将瀑布流式的代码,转变成以组件为核心概念的代码结构。这种结构的好处是可以任意编排,组件与组件之间是解耦的,组件之间的流转全靠规则来驱动。

 

由于我们的业务逻辑较为简单,常见的开源流程引擎都能支持,因此选型时着重考虑了易用性、侵入性、性能和社区活跃度。

名称

易用性

侵入性

社区活跃度

性能

适用场景

Activiti 7

有一定的学习成本,需要理解BPMN的流程定义规范

需要创建25张表保存流程相关信息

start:9.3K

无数据

属于工作流引擎,面向OA场景,适用基于角色的工作流,例如审批流等。

库表关联操作非常多,性能不可避免有一定损失,不适用于C端场景。

Flowable 6

有一定的学习成本,需要理解BPMN的流程定义规范

需要创建19张表保存流程相关信息

start:6.2K

无数据

Camunda 7

有一定的学习成本,需要理解BPMN的流程定义规范

需要创建15张表保存流程相关信息

start:3.2K

Camunda 7性能比Flowable 6提升最小10%,最大39%

flowable与camunda性能对比测试

Compileflow

阿里巴巴

原生只支持淘宝BPM规范,仅支持部分BPMN 2.0元素。

无需额外建表

一般

start:1.4K

无数据

业务支持模块化拆分、有序执行,并且有多变的编排诉求,适用于C端场景。

Turbo

滴滴

有一定的学习成本,需要理解BPMN的流程定义规范

需要创建5张表保存流程相关信息

一般

start:426

无数据

LiteFlow

规则文件语法简单,实现轻量,易上手

无需额外建表

start:1.6K

50多个业务组件组成的链路,在实际压测中单点达到了1500的TPS

性能表现

适用于拥有复杂逻辑的业务,业务流程完全可以按照业务粒度拆分成一个个独立的组件,进行装配复用变更。

只做基于逻辑的流转,而不做基于角色任务的流转。

LiteFlow的实现更加轻量,并且使用方式更简单,学习门槛低,更适合于我们这种基于逻辑流转的C端业务。

使用样例

1. 引入依赖

<dependency> <groupId>com.yomahub</groupId> <artifactId>liteflow-spring-boot-starter</artifactId> <version>2.10.1</version> </dependency>

2. 数据上下文定义

定义数据上下文用于组件间的数据传递

@Data public class CustomContext { /** * 寄件量 */ private int sendNum; /** * 领取状态 */ private Map<String, Boolean> stateMap; /** * 礼包策略 */ private Map<String, PacketStrategyDTO> strategyMap; /** * 礼包信息 */ private Map<String, List<PacketInfoDTO>> packetInfoMap; /** * 首页结果数据 */ private IndexDataRespDTO indexDataRespDTO; }

3. 定义组件(省略部分组件代码)

查bdp数据

@LiteflowComponent("queryBDPData") public class BDPDataCmp extends NodeComponent { @Override public void process() { BaseRequest request = this.getRequestData(); // 查询bdp数据 JSONArray bdpData = queryBdpData(request.getMobile()); // 将结果写入数据上下文 CustomContext context = this.getContextBean(CustomContext.class); context.setBdpData(bdpData); } }

查寄件量

@LiteflowComponent("querySendNum") public class SendNumCmp extends NodeComponent { @Override public void process() { BaseRequest request = this.getRequestData(); // 查询bdp数据 int sendNum = querySendNum(request.getUserId()); // 将结果写入数据上下文 CustomContext context = this.getContextBean(CustomContext.class); context.setSendNum(sendNum); } }

封装结果

@LiteflowComponent("buildResult") public class BuildResultCmp extends NodeComponent { @Override public void process() { CustomContext context = this.getContextBean(CustomContext.class); // 从数据上下文中取出数据封装结果 IndexDataRespDTO indexDataRespDTO = doBuildResult(context.getBdpData(), context.getSendNum(), context.getStateMap(), context.getPacketInfoMap()); // 将结果写入数据上下文 context.setIndexDataRespDTO(indexDataRespDTO); }

4. 定义流程配置文件

在resources下的config/flow.el.xml中定义规则:

<?xml version="1.0" encoding="UTF-8"?> <flow> <chain name="sendTaskchain"> THEN(queryBDPData, querySendNum, queryPacketReceiveState, queryPacketStrategy, queryPacketInfo, buildResult); </chain> </flow>

5. 执行流程

@Service public class HighDispRetrieveNewServiceImpl implements HighDispRetrieveNewService { @Resource private FlowExecutor flowExecutor; @Override public Result<IndexDataRespDTO> index(BaseRequest reqDTO) { LiteflowResponse response = flowExecutor.execute2Resp("sendTaskchain", reqDTO, CustomContext.class); return SFResultUtil.success(response.getFirstContextBean()); } }

方案对比

方案优点缺点
方案一:设计模式代码直观易理解,没有学习成本。不够灵活,业务流程改变就无法支持。比如领取奖励的流程多了一些判断的步骤,例子中构建首页数据的模版就无法使用。
方案二:流程引擎有一定的学习成本。支持业务流程的灵活编排。

目前我们采用设计模式的方式先抽象出了通用的寄件任务类活动接口,后续考虑逐步引入流程引擎进一步优化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值