作者:小傅哥 博客:https://bugstack.cn
❝沉淀、分享、成长,让自己和他人都能有所收获!😜 ❞
大家伙,我是技术UP主小傅哥。
在我们做Java业务代码开发时,经常会碰到大量的流程判断,验证场景渠道
、验证用户身份
、验证会员级别
等等10来个甚至几十个分支节点。对于这样的编码80%的T2码农都是 if...else 编码。那除了贴膏药一样的写代码还有别的什么办法吗?
if...else 写代码有什么问题吗?
往近了看,if...else 写的代码交付的快,但工程腐化的也快。有点像买的米面粮油不区分,全倒入一个桶里。让后面的兄弟在迭代需求时,都想铲了重写。但实际铲的成本太高,所以就挑挑拣拣、复制粘贴。
往远了看,是个人发展。如果昨天做的事、今天做的事和明天做的事,都是一个事。反复的重复,没有脑力思考,只是提高手速。那面试、述职、分享的时候真没的讲,你总不能告诉面试官我就写 if...else 了,遇到问题查百度。那离毕业🎓也不远了!
文末可以获得实战项目课程&源码,在源码中实战架构、场景和设计模式运用。提升能力嘎嘎快!
Java 开发,尤其是 Java 业务开发的,就离不开大量的流程分支处理。产品给的需求,也是在编码中调用一系列的接口做流程验证处理。而且需求是频繁变化的,这也就间接的导致了程序员的 if...else 代码也要跟着一次次调整,从原来的几十行编程几百几千行。这个过程中还有一些要去掉的、要根据流程类型选择切换的、要覆盖所有的之前的需求不能出问题的。所以腐化越来越严重,开发成本越来越高。
一个接口、一个实现,
一个实现,代码一片。
一片一片、又一片,
代码行数、两三千。
这是我们在承接产品需求时候经验能看到的一种流程图,在没有太强的代码驾驭能力时,很多伙伴的都只能写 if...else 一个个分支节点判断,所有的代码都平铺到一个类中。如果注意到你会发现,不具有设计模式能力的程序员,代码是没有立体化的。都是扁平平铺下来的。
那么对于这样的场景,我们完全可以通过设计模式的知识进行分治和抽象,这也是软件设计的第一原则,康威定律所倡导的。通过设计模式解耦流程,让编码的呈现出立体化,通过类来划分职责和执行过程。
对于大量的有衔接关系的 if...else 判断流程,有两种设计手段对应这3种编码方式。包括;责任链和规则树。
接下来,小傅哥就分别举例下这样的代码实现。程序员还是看代码,学习起来更有感觉。
抽奖规则过滤场景,分为黑名单用户、权重抽奖和默认抽奖,三个节点。如果一个用户是黑名单范围用户,则直接返回兜底奖品。而权重用户是一个用户已完成了N次抽奖后,在权重范围内可以获得一个固定的奖品。最后是兜底抽奖,这两个条件都不是,则进行默认兜底流程。
public abstract class AbstractLogicChain implements ILogicChain{
private ILogicChain next;
@Override
public ILogicChain next() {
return next;
}
@Override
public ILogicChain appendNext(ILogicChain next) {
this.next = next;
return next;
}
protected abstract String ruleModel();
}
@Slf4j
@Component("rule_default")
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class DefaultLogicChain extends AbstractLogicChain {
@Resource
protected IStrategyDispatch strategyDispatch;
@Override
public DefaultChainFactory.StrategyAwardVO logic(String userId, Long strategyId) {
Integer awardId = strategyDispatch.getRandomAwardId(strategyId);
log.info("抽奖责任链-默认处理 userId:{} strategyId:{} ruleModel:{} awardId:{}", userId, strategyId, ruleModel(), awardId);
return DefaultChainFactory.StrategyAwardVO.builder()
.awardId(awardId)
.logicModel(ruleModel())
.build();
}
@Override
protected String ruleModel() {
return DefaultChainFactory.LogicModel.RULE_DEFAULT.getCode();
}
}
public ILogicChain openLogicChain(Long strategyId) {
ILogicChain cacheLogicChain = strategyChainGroup.get(strategyId);
if (null != cacheLogicChain) return cacheLogicChain;
StrategyEntity strategy = repository.queryStrategyEntityByStrategyId(strategyId);
String[] ruleModels = strategy.ruleModels();
// 如果未配置策略规则,则只装填一个默认责任链
if (null == ruleModels || 0 == ruleModels.length) {
ILogicChain ruleDefaultLogicChain = applicationContext.getBean(LogicModel.RULE_DEFAULT.getCode(), ILogicChain.class);
// 写入缓存
strategyChainGroup.put(strategyId, ruleDefaultLogicChain);
return ruleDefaultLogicChain;
}
// 按照配置顺序装填用户配置的责任链;rule_blacklist、rule_weight 「注意此数据从Redis缓存中获取,如果更新库表,记得在测试阶段手动处理缓存」
ILogicChain logicChain = applicationContext.getBean(ruleModels[0], ILogicChain.class);
ILogicChain current = logicChain;
for (int i = 1; i < ruleModels.length; i++) {
ILogicChain nextChain = applicationContext.getBean(ruleModels[i], ILogicChain.class);
current = current.appendNext(nextChain);
}
// 责任链的最后装填默认责任链
current.appendNext(applicationContext.getBean(LogicModel.RULE_DEFAULT.getCode(), ILogicChain.class));
// 写入缓存
strategyChainGroup.put(strategyId, logicChain);
return logicChain;
}
当我们在实现业务流程编码时看到有些流程是带有判断和分支走向的,那么就不太适合用单一的责任链处理。比如一个流程中需要对抽奖的奖品进行交叉判断,抽中后判断是否满足中奖条件,满足后走库存处理,不满足走兜底处理。另外库存不足则也要走兜底处理。那么这样就是一个分叉的流程了。可以使用规则树进行实现。
public interface ILogicTreeNode {
DefaultTreeFactory.TreeActionEntity logic(String userId, Long strategyId, Integer awardId, String ruleValue, Date endDateTime);
}
@Component("rule_lock")
public class RuleLockLogicTreeNode implements ILogicTreeNode {
@Resource
private IStrategyRepository repository;
@Override
public DefaultTreeFactory.TreeActionEntity logic(String userId, Long strategyId, Integer awardId, String ruleValue, Date endDateTime) {
log.info("规则过滤-次数锁 userId:{} strategyId:{} awardId:{}", userId, strategyId, awardId);
long raffleCount = 0L;
try {
raffleCount = Long.parseLong(ruleValue);
} catch (Exception e) {
throw new RuntimeException("规则过滤-次数锁异常 ruleValue: " + ruleValue + " 配置不正确");
}
// 查询用户抽奖次数 - 当天的;策略ID:活动ID 1:1 的配置,可以直接用 strategyId 查询。
Integer userRaffleCount = repository.queryTodayUserRaffleCount(userId, strategyId);
// 用户抽奖次数大于规则限定值,规则放行
if (userRaffleCount >= raffleCount) {
log.info("规则过滤-次数锁【放行】 userId:{} strategyId:{} awardId:{} raffleCount:{} userRaffleCount:{}", userId, strategyId, awardId, userRaffleCount, userRaffleCount);
return DefaultTreeFactory.TreeActionEntity.builder()
.ruleLogicCheckType(RuleLogicCheckTypeVO.ALLOW)
.build();
}
log.info("规则过滤-次数锁【拦截】 userId:{} strategyId:{} awardId:{} raffleCount:{} userRaffleCount:{}", userId, strategyId, awardId, userRaffleCount, userRaffleCount);
// 用户抽奖次数小于规则限定值,规则拦截
return DefaultTreeFactory.TreeActionEntity.builder()
.ruleLogicCheckType(RuleLogicCheckTypeVO.TAKE_OVER)
.build();
}
}
@Override
public DefaultTreeFactory.StrategyAwardVO process(String userId, Long strategyId, Integer awardId, Date endDateTime) {
DefaultTreeFactory.StrategyAwardVO strategyAwardData = null;
// 获取基础信息
String nextNode = ruleTreeVO.getTreeRootRuleNode();
Map<String, RuleTreeNodeVO> treeNodeMap = ruleTreeVO.getTreeNodeMap();
// 获取起始节点「根节点记录了第一个要执行的规则」
RuleTreeNodeVO ruleTreeNode = treeNodeMap.get(nextNode);
while (null != nextNode) {
// 获取决策节点
ILogicTreeNode logicTreeNode = logicTreeNodeGroup.get(ruleTreeNode.getRuleKey());
String ruleValue = ruleTreeNode.getRuleValue();
// 决策节点计算
DefaultTreeFactory.TreeActionEntity logicEntity = logicTreeNode.logic(userId, strategyId, awardId, ruleValue, endDateTime);
RuleLogicCheckTypeVO ruleLogicCheckTypeVO = logicEntity.getRuleLogicCheckType();
strategyAwardData = logicEntity.getStrategyAwardVO();
log.info("决策树引擎【{}】treeId:{} node:{} code:{}", ruleTreeVO.getTreeName(), ruleTreeVO.getTreeId(), nextNode, ruleLogicCheckTypeVO.getCode());
// 获取下个节点
nextNode = nextNode(ruleLogicCheckTypeVO.getCode(), ruleTreeNode.getTreeNodeLineVOList());
ruleTreeNode = treeNodeMap.get(nextNode);
}
// 返回最终结果
return strategyAwardData;
}
在我们的业务场景中,有时候既不是走责任链,也不是走配置到库上的规则树,而是介于两者直接。由代码控制的节点走向,根据每个节点实现逻辑,动态处理下一个节点的实现。
如,一个流程中进入总人口,之后判断是否开量、账户数据、之后从账户数据开始又有3个级别判断。这3级别是根据账户数据的结果判断的。
最后,这里还要有一个上下文数据记录,所有的节点完成后填充数据。
public interface StrategyMapper {
/**
* 获取策略处理器
*/
StrategyHandler get(DefaultStrategyFactory.MaterialVO materialVO, DefaultStrategyFactory.DynamicContext dynamicContext);
}
public interface StrategyHandler {
/**
* 处理最终返回结果
*/
StrategyHandler DEFAULT = (materialVO, dynamicContext) -> {
DefaultStrategyFactory.DecisionOutcomeVO decisionOutcomeVO = new DefaultStrategyFactory.DecisionOutcomeVO();
decisionOutcomeVO.setLevel(dynamicContext.getLevel());
return decisionOutcomeVO;
};
/**
* 受理策略处理
*/
DefaultStrategyFactory.DecisionOutcomeVO apply(DefaultStrategyFactory.MaterialVO materialVO, DefaultStrategyFactory.DynamicContext dynamicContext) throws Exception;
}
public abstract class AbstractStrategyRouter implements StrategyMapper, StrategyHandler {
@Getter
@Setter
protected StrategyHandler defaultStrategyHandler = StrategyHandler.DEFAULT;
/**
* 行为路由
*/
public DefaultStrategyFactory.DecisionOutcomeVO router(DefaultStrategyFactory.MaterialVO materialVO, DefaultStrategyFactory.DynamicContext dynamicContext) throws Exception {
StrategyHandler strategyHandler = get(materialVO, dynamicContext);
if (null != strategyHandler) return strategyHandler.apply(materialVO, dynamicContext);
return defaultStrategyHandler.apply(materialVO, dynamicContext);
}
}
@Component
public class AccountNode extends AbstractStrategyRouter {
private final MemberLevel0Node memberLevel0Node;
private final MemberLevel1Node memberLevel1Node;
private final MemberLevel2Node memberLevel2Node;
public AccountNode(MemberLevel0Node memberLevel0Node, MemberLevel1Node memberLevel1Node, MemberLevel2Node memberLevel2Node) {
this.memberLevel0Node = memberLevel0Node;
this.memberLevel1Node = memberLevel1Node;
this.memberLevel2Node = memberLevel2Node;
}
@Override
public DefaultStrategyFactory.DecisionOutcomeVO apply(DefaultStrategyFactory.MaterialVO materialVO, DefaultStrategyFactory.DynamicContext dynamicContext) throws Exception {
log.info("【账户节点】规则决策树 userId:{}", materialVO.getUserId());
// 1. 模拟查询用户级别
int level = new Random().nextInt(3);
log.info("模拟查询用户级别 level:{}",level);
dynamicContext.setLevel(level);
return router(materialVO, dynamicContext);
}
@Override
public StrategyHandler get(DefaultStrategyFactory.MaterialVO materialVO, DefaultStrategyFactory.DynamicContext dynamicContext) {
switch (dynamicContext.getLevel()){
case 0:
return memberLevel0Node;
case 1:
return memberLevel1Node;
case 2:
return memberLevel2Node;
default:
return defaultStrategyHandler;
}
}
}
这样的编码是不很爽,设计上即防腐又仿佛开启了新世界的大门!代码原来还能写的这么优雅!加入小傅哥即可获得整套项目代码学习。
像是这样的场景解决方案,设计模式运用,在小傅哥的星球「码农会锁」都有非常多的项目可以让你锻炼学习,积累让人傲娇的编程实力。在面试、述职、分享中脱颖而出。
小傅哥的星球「码农会锁」有非常多的实战项目,包括业务的5个;大营销(大课)
、OpenAI 大模型应用
、Lottery
、IM
、AI 问答助手
。组件的7个;OpenAI 代码评审
、BCP 透视业务监控
、动态线程池
、支付SDK设计和开发
、API网关
、SpringBoot Starter
、IDEA Plugin 插件开发
。