知识简介:在需求开发过程中我们发现,有一些流程相似的业务,存在功能重复开发的问题,没有沉淀出一套可复用可扩展的代码,浪费了很多开发人力。本文以3个业务流程类似的营销活动为例,讨论如何提升营销活动代码复用性和扩展性。
背景
某组目前承接了xx大量的营销活动,在需求开发过程中我们发现,有很多活动的业务流程是类似的,但是并没有沉淀出一套可复用可扩展的代码,功能的重复开发浪费了很多人力。下面我们针对X1、X2、X3这3个业务流程相似的活动,讨论如何提升营销活动代码复用性和扩展性。
业务流程
大数据会根据用户历史寄件数据生成寄件任务目标件量,在活动页面展示。用户在活动周期内达成目标寄件量后,即可领取对应的礼包。产品或运营同事会根据客群、AB实验等条件配置不同的礼包策略,以便更精准地激励用户寄件。
代码流程
注:流程图中颜色相同的矩形代表相同的功能。
满赠
中高散
季节性
问题
以上3个活动在代码实现上存在以下问题:
1. 数据存储散乱
礼包领取记录、礼包策略维护在不同的库表里。
2. 代码复用性、扩展性低
从上面的流程图可以看出,活动首页数据的组装都会经历以下流程。但是3个流程大致相同的活动,却维护了3套代码在2个应用里。
因此,我们需要对代码进行重构,来提升复用性和扩展性。
目标
代码重构的目标是沉淀一套针对于寄件任务活动场景的通用代码实现,既提供可直接复用的通用能力,也提供一定的扩展点来支持定制化的业务逻辑。
解决方案
方案一:设计模式:模版模式+工厂模式+策略模式
- 抽象出寄件任务活动的模版方法,一些通用功能可以在该抽象类实现。
@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; } }
- 工厂类
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); } }
- 活动类型枚举
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; } }
- 每个活动可以通过重写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; } }
- 根据活动类型获取对应的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)); } }
方案二:流程引擎
流程引擎可以将瀑布流式的代码,转变成以组件为核心概念的代码结构。这种结构的好处是可以任意编排,组件与组件之间是解耦的,组件之间的流转全靠规则来驱动。
由于我们的业务逻辑较为简单,常见的开源流程引擎都能支持,因此选型时着重考虑了易用性、侵入性、性能和社区活跃度。
LiteFlow的实现更加轻量,并且使用方式更简单,学习门槛低,更适合于我们这种基于逻辑流转的C端业务。
<dependency> <groupId>com.yomahub</groupId> <artifactId>liteflow-spring-boot-starter</artifactId> <version>2.10.1</version> </dependency>
定义数据上下文用于组件间的数据传递
@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; }
查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); }
在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>
@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()); } }
方案对比
方案 | 优点 | 缺点 |
---|---|---|
方案一:设计模式 | 代码直观易理解,没有学习成本。 | 不够灵活,业务流程改变就无法支持。比如领取奖励的流程多了一些判断的步骤,例子中构建首页数据的模版就无法使用。 |
方案二:流程引擎 | 有一定的学习成本。 | 支持业务流程的灵活编排。 |
目前我们采用设计模式的方式先抽象出了通用的寄件任务类活动接口,后续考虑逐步引入流程引擎进一步优化。