初探【决策树设计模式】完成对业务的重构

838 阅读6分钟

业务背景:

工作中有一个业务,一个类实现,在经过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种中的其中一种,这是机器学习中的一种预测模型,我在这还是使用这个名称。为什么会想到这个模式呢,我在梳理业务的时候,通过用树的结构去整理完整业务处理流,发现这就是一个多叉树的结构,依次判断每个节点是否满足,来找到下一层节点,最终在叶子节点执行业务逻辑。

  • 接下来就是怎么使用这个思路去解决问题了

由于并没有现成的代码可以抄,所以只能自己去瞎写,同时考虑设计模式抽象化,以后其他的复杂业务也可以复用框架,暂时抽出了如下的必须的类。

代码结构

image.png

  • 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);
               });
            }
        }
    }
}
总结
  1. 费时2天,反复推敲,通过这样的设计,一个大类被我拆成了33个类(窃喜,工时混到了);但也将业务逻辑清晰的呈现出来了,后续只需要配置好xml决策路线,梳理业务的时候就可以很方便的找到对应的处理器,对其进行维护,或者后续需要调整决策路线,也可以调整配置即可(一开始试过用代码构建,用json,用yml文件,都不具备结构化试过之后最终选择xml)
  2. 真实的业务没那么简单,单纯一个决策树没办法处理所有的问题,大部分时候都是多种设计模式组合使用,在业务上层我增加了模板设计(制定标准流程)、策略模式(容易区分并且差异性很大的逻辑)、工厂模式(与策略模式结合),对业务结构,并且将决策树的层级进行了减少。
  3. 目前实现比较简单,如果要给他其他业务通用,还需要打磨怎么让接入方一目了然,防呆;比如可以在叶子节点加上一些严格的校验,或者还有其他更加结构化的方式定义,比如落库等