业务背景:
工作中有一个业务,一个类实现,在经过N次迭代,多个同事在其中改动后,已经变成了传说中屎山;主要表现为,if else switch穿插各种非标的判断,这就导致维护起来非常麻烦,只看注释很难屡清楚具体的逻辑,单纯靠文档靠流程图无法保持实时性,在最近出现一次bug去排查后才感受到了折磨人。针对这种情况,并且闲的蛋疼,我就开始思考怎么让代码更好维护。
思考思路:
v1版本--大类拆小类
抽公共逻辑,分类组合,由一个大类拆分成多个小类
- 结果:并没有产生很明显的作用,仍然是拼凑,并且有些逻辑会存在跨类重复,放弃!!
v2版本--策略设计模式
策略模式关键的是要找到互斥性的key,通过策略工厂计算key找到指定的handler
- 结果:策略模式在我的业务中是使用很普遍的,也使用得心应手,但是我们这个业务中存在非常多的嵌套判断if esle switch,如果需要找出唯一的key,就需要将多层if扁平化,举例如下这种代码结构,真实的代码层次更复杂。
if (test1) {
switch(type) {
case type1:
if (test1) {
// todo
} else if (test2) {
// todo
} else {
// todo
}
break;
case type2:
// todo
break;
case type3:
// todo
break;
}
} else {
// todo
}
- 上面这段代码会有非常多的处理方式,那如果按照策略模式去设计,想要拿到唯一key就非常难;进而考虑使用多层策略模式,尝试设计代码,也能实现,但是我并不满意,主要原因是结构不清晰,无法有一个全局的观测整个业务流。 到此为止,还没想到好办法。
终极版本--决策树设计模式
严格来说这种模式并不是我们熟悉的21种中的其中一种,这是机器学习中的一种预测模型,我在这还是使用这个名称。为什么会想到这个模式呢,我在梳理业务的时候,通过用树的结构去整理完整业务处理流,发现这就是一个多叉树的结构,依次判断每个节点是否满足,来找到下一层节点,最终在叶子节点执行业务逻辑。
- 接下来就是怎么使用这个思路去解决问题了
由于并没有现成的代码可以抄,所以只能自己去瞎写,同时考虑设计模式抽象化,以后其他的复杂业务也可以复用框架,暂时抽出了如下的必须的类。
代码结构
- Decision
决策器,getDecision返回String的结果,拿到结果后从DecisionTreeNode#childrenNodes中找到下一级
public interface Decision<T extends DecisionContext> {
String getDecision(T t);
}
- DecisionProcessor
决策树叶子节点的处理器,业务通过实现process方法去处理对应的逻辑即可
public interface DecisionProcessor<T extends DecisionContext, E extends ProcessResult> {
/**
* 最终执行处理的结果
* @param t
*/
E process(T t);
}
- DecisionTreeNode
决策树节点,节点包含决策器、处理器、下一级节点
@Data
public class DecisionTreeNode<T extends DecisionContext, E extends ProcessResult> {
// 非叶子节点用户找子节点的决策器
private Decision<T> decision;
// 这个主要是为了日志打印出决策结果的名称,方便检查决策树构建是否正确
private String desc;
// 下一级
private Map<String, DecisionTreeNode<T, E>> childrenNodes = new HashMap<>();
// 叶子节点才有的处理器
private DecisionProcessor<T, E> processor;
private DecisionTreeNode(Decision<T> decision) {
this.decision = decision;
}
private DecisionTreeNode(String desc, DecisionProcessor<T, E> processor) {
this.desc = desc;
this.processor = processor;
}
public DecisionTreeNode<T, E> child(String nodeKey, DecisionTreeNode<T, E> child) {
if (childrenNodes.containsKey(nodeKey)) {
throw new RunTimeException("决策树key重复: " + nodeKey);
}
childrenNodes.put(nodeKey, child);
return this;
}
public void setChildrenNodes(Map<String, DecisionTreeNode<T, E>> childrenNodes) {
this.childrenNodes = childrenNodes;
}
public static <T extends DecisionContext, E extends ProcessResult> DecisionTreeNode<T, E> build(Decision<T> decision) {
Assert.notNull(decision, "决策不能为空");
return new DecisionTreeNode<>(decision);
}
public static <T extends DecisionContext, E extends ProcessResult> DecisionTreeNode<T, E> buildLeaf(String desc, DecisionProcessor<T, E> processor) {
FcAssert.notNull(processor, "决策处理器不能为空");
return new DecisionTreeNode<>(desc, processor);
}
// 每一层都可以直接进行决策,递归找到对应的决策器
public DecisionResult<T, E> decision(T t) {
if (this.processor != null) {
return new DecisionResult<>(this.processor, this.desc);
}
String decision = this.decision.getDecision(t);
DecisionTreeNode<T, E> treeNode = childrenNodes.get(decision);
if (treeNode == null) {
return null;
}
return treeNode.decision(t);
}
}
- DecisionTreeService
这个主要是为了接入到spring管理,实现提供bean的创建,实现中需要初始化加载决策树、调用决策等逻辑
public interface DecisionTreeService<T extends DecisionContext, E extends ProcessResult> {
E process(T context);
}
@Component
@Slf4j
public class TestDecisionTreeService implements DecisionTreeService<TestContext, EmptyProcessResult> {
private DecisionTreeNode<TestContext, EmptyProcessResult> decisionTree;
@PostConstruct
public void init() {
// 放在resources下即可
String path = "/decision/TestDecisionTree.xml";
try {
ClassPathResource resourceReader = new ClassPathResource(path);
decisionTree = new DecisionXmlParser<TestContext, EmptyProcessResult>().parse(resourceReader.getInputStream());
if (decisionTree == null) {
throw new RunTimeException("决策树配置有问题,请检查:" + path);
}
} catch (Exception e) {
log.info("决策树配置加载文件发生异常", e);
throw new RunTimeException("决策树配置加载文件发生异常,请检查:" + path);
}
}
@Override
public EmptyProcessResult process(TestContext context) {
DecisionResult<TestContext, EmptyProcessResult> decision = decisionTree.decision(context);
if (decision != null && decision.getProcessor() != null) {
log.info("【决策结果】 {} : {}", decision.getDesc(), decision.getProcessor().getClass().getName());
return decision.getProcessor().process(context);
}
return null;
}
}
- DecisionXmlParser
解析决策树配置,构建树
@Slf4j
public class DecisionXmlParser<T extends DecisionContext, E extends ProcessResult> {
public DecisionTreeNode<T, E> parse(InputStream inputStream) {
try {
Document document = XmlUtil.readXML(inputStream);
Element root = XmlUtil.getRootElement(document);
DecisionTreeNode<T, E> rootNode = parseNode(root);
Assert.notNull(rootNode, "决策树配置有问题");
List<Element> list = XmlUtil.getElements(root, "node");
Map<String, DecisionTreeNode<T, E>> childrenNodes = parseChildren(list);
rootNode.setChildrenNodes(childrenNodes);
log.info(DecisionPrinter.toString(rootNode));
return rootNode;
} catch (Exception e) {
log.error("解析决策树发生异常", e);
throw new RunTimeException("初始化决策树失败");
}
}
private Map<String, DecisionTreeNode<T, E>> parseChildren(List<Element> list) {
if (CollectionUtils.isEmpty(list)) {
return null;
}
Map<String, DecisionTreeNode<T, E>> map = new HashMap<>(list.size());
for (Element element : list) {
DecisionTreeNode<T, E> treeNode = parseNode(element);
if (treeNode != null) {
String value = element.getAttribute("value");
Assert.notNull(value, "没有配置决策结果value值");
map.put(value, treeNode);
treeNode.setChildrenNodes(parseChildren(XmlUtil.getElements(element, "node")));
}
}
return map;
}
private DecisionTreeNode<T, E> parseNode(Element element) {
if (element == null) {
return null;
}
try {
DecisionTreeNode<T, E> node = null;
String decisionName = element.getAttribute("decision");
if (StringUtils.isNotBlank(decisionName)) {
Class<?> decisionClz = Class.forName(decisionName);
Object newInstance = decisionClz.newInstance();
if (newInstance instanceof Decision) {
node = DecisionTreeNode.build((Decision) newInstance);
}
}
String processorName = element.getAttribute("processor");
String desc = element.getAttribute("desc");
if (StringUtils.isNotBlank(processorName)) {
Class<?> processorClz = Class.forName(processorName);
Object newInstance = processorClz.newInstance();
if (newInstance instanceof DecisionProcessor) {
node = DecisionTreeNode.buildLeaf(desc, (DecisionProcessor) newInstance);
}
}
return node;
} catch (Exception e) {
return null;
}
}
}
- TestDecisionTree.xml
除了跟节点只能一个,子节点都支持多个,同一个父节点的子节点value必须唯一。
<?xml version="1.0" encoding="UTF-8" ?>
<node decision="com.xx.Decision1">
<node value="result1" decision="com.xx.Decision2">
<node value="true" decision="com.xx.Decision3">
<node value="true" desc="处理器1"
processor="com.xxx.Processor1"/>
<node value="false" desc="处理器2"
processor="com.xxx.Processor2"/>
</node>
<node value="false" desc="处理器3"
processor="com.xxx.Processor3"/>
</node>
<node value="result2" decision="com.xx.BoolDecision">
<node value="true" decision="com.xx.decision4">
<node value="true" desc="处理器4"
processor="com.xxx.Processor4"/>
<node value="false" desc="处理器5"
processor="com.xxx.Processor5"/>
</node>
<node value="false" desc="处理器6"
processor="com.xxx.Processor6"/>
</node>
</node>
- DecisionPrinter
单纯为了打印输入,方便确认树是否正确,自由发挥
@UtilityClass
public class DecisionPrinter {
public String toString(DecisionTreeNode tree) {
List<String> list = new ArrayList<>();
list.add("\n==================决策树打印开始===========================:\n");
cycle(list, tree);
list.add("==================决策树打印结束===========================:\n");
return String.join("\n\r", list);
}
private void cycle(List<String> list, DecisionTreeNode node) {
if (node == null) {
return;
}
if (node.getProcessor() != null) {
list.add("\t" + node.getDesc() + ": " + node.getProcessor().getClass().getName());
} else {
Map<String, DecisionTreeNode> decisionMap = node.getChildrenNodes();
if (decisionMap != null && decisionMap.size() > 0) {
decisionMap.forEach((s, decisionTreeNode) -> {
cycle(list, decisionTreeNode);
});
}
}
}
}
总结
- 费时2天,反复推敲,通过这样的设计,一个大类被我拆成了33个类(窃喜,工时混到了);但也将业务逻辑清晰的呈现出来了,后续只需要配置好xml决策路线,梳理业务的时候就可以很方便的找到对应的处理器,对其进行维护,或者后续需要调整决策路线,也可以调整配置即可(一开始试过用代码构建,用json,用yml文件,都不具备结构化试过之后最终选择xml)
- 真实的业务没那么简单,单纯一个决策树没办法处理所有的问题,大部分时候都是多种设计模式组合使用,在业务上层我增加了模板设计(制定标准流程)、策略模式(容易区分并且差异性很大的逻辑)、工厂模式(与策略模式结合),对业务结构,并且将决策树的层级进行了减少。
- 目前实现比较简单,如果要给他其他业务通用,还需要打磨怎么让接入方一目了然,防呆;比如可以在叶子节点加上一些严格的校验,或者还有其他更加结构化的方式定义,比如落库等