组合模式(Composite Pattern)

概念

组合模式(Composite Pattern):有时候又叫做部分-整体模式,它使我们树型结构的问题中,模糊了简单元素和复杂元素的概念,客户程序可以像处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦

组合模式让你可以优化处理递归或分级数据结构。有许多关于分级数据结构的例子,使得组合模式非常有用武之地。关于分级数据结构的一个普遍性的例子是你每次使用电脑时所遇到的:文件系统。文件系统由目录和文件组成。每个目录都可以装内容。目录的内容可以是文件,也可以是目录。按照这种方式,计算机的文件系统就是以递归结构来组织的。如果你想要描述这样的数据结构,那么你可以使用组合模式Composite。 — from 百度百科

简介

作用及优势

    - 将复杂的逻辑拆分,使得客户端调用复杂元素的时候变得简单,就是封装起来给客户端调用

    - 扩展性强,需要新增规则时,只需要动态的增加树节点即可

劣势

  • 每个节点、包括连接节点的树枝,都是具体实现类,没法做到接口层次的抽象,可能会有点违背依赖倒置原则;

  • 在需求不是很复杂的场景下,逻辑相对来说会过于复杂,节点也多,维护类的成本也上来了;

场景

    - 文件夹目录

    - 部门的层级关系

    - 树形菜单

    

这里举个例子:

组合模式,光看概念可能会很模糊,那就举个栗子:什么天气穿什么衣服;首先我们有很多衣服 长袖、短袖、五分裤、九分裤(这里可以通过桥接、建造者去组合);然后有几个条件:温度有大于、小于30°,太阳分 有、无,这里可能还有风量大小之类的,就先不说了;然后会出现很多组合:

  • 温度>30°,有太阳 ==》 穿短袖、五分裤

  • 温度>30°,无太阳 ==》 穿短袖、九分裤

  • 温度<30°,有太阳 ==》 穿长袖、五分裤

  • 温度<30°,无太阳 ==》 穿长袖、九分裤

看上面我们可以这么做,将每个条件抽离出来(温度、太阳)作为一个个节点,然后再将 >、<、=、有、无这一类作为对比规则保存起来,用一个switch+枚举保存;然后开始组合 温度节点 → 规则与界限(>, 30°)→ 太阳节点 → 规则与界限(=, 有太阳)→ 果实节点(这里就是具体返回 == 穿短袖、五分裤);这样就可以组合出很多场景来了,这样虽然看起来没有if else来的快,可是之后需要加 温度界限为 10°、 20°等情况的时候,有需要加很多if else,这样维护起来就相当麻烦,而组合模式,这需要加上一个条件节点、一个规则节点和果实节点,也不会对之前的代码有很大的侵入性;

代码

案例描述

现在有一组规则:年龄(age)和性别(gender),现在需要根据决策树去得到对应组合的果实,比如:man / 25 = 推荐电子产品,就是类似这种组合,然后age、gender对应的就是规则节点;man、25这种对应的就是“树枝”,25就是其界限值,然后“树枝”的指向就是对应的表达式(=、>、<)等,如果大于就指向大于25的那个节点(nodeIdTo字段指向的规则节点),直至找到果实节点为止;

详细逻辑看图:

组合模式决策树png.png

工程目录

image.png

项目类图

组合模式.jpg

具体实现

note :很多注释写在代码

规则节点(TreeNode): 也即是树节点;


/**
 * 功能描述: 决策树节点
 *
 * @author: WuChengXing
 * @create: 2021-06-26 11:43
 **/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class TreeNode {
    /**
     * 树id
     */
    private Long treeId;

    /**
     * 树节点id
     */
    private Long treeNodeId;

    /**
     * 节点类型:1=叶子节点,2=果实节点
     */
    private Integer treeNodeType;

    private Object treeNodeValue;

    /**
     * 这里是节点规则:对应是按什么去过滤数据,比如(性别(gender)、年龄(age))
     */
    private String ruleKey;

    private String ruleDesc;

    /**
     * 这里是维持 两个节点之前的关系的“树枝”,可能有多种情况
     */
    private List<TreeNodeLink> treeNodeLinks;
}

树枝节点(TreeNodeLink):

/**
 * 功能描述: 节点之间的链路指向
 *
 * @author: WuChengXing
 * @create: 2021-06-26 11:45
 **/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class TreeNodeLink {
    /**
     * 上一个节点的id
     */
    private Long nodeIdFrom;

    /**
     * 下一个节点id
     */
    private Long nodeIdTo;

    /**
     * 这个是过滤的类型;这个是枚举,查看 == ExpressionEnum
     */
    private Integer ruleLimitType;

    /**
     * 这个是具体的值:比如 man、women、19、52等之类的限定值
     */
    private String ruleLimitValue;
}

这里说下,“树枝”起到了承上启下的作用,关联着两个节点,当“树枝”节点的校验通过了,就会指向下一个节点,直到最后结了果实;

存放节点信息的TreeRich和根节点信息TreeRoot,以及返回值EngineResult:

/**
 * 功能描述:
 *
 * @author: WuChengXing
 * @create: 2021-06-26 11:55
 **/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TreeRich {

    private TreeRoot treeRoot;

    private Map<Long, TreeNode> treeNodeMap;
}

------------------------------------------------

/**
 * 功能描述: 决策树根
 *
 * @author: WuChengXing
 * @create: 2021-06-26 11:48
 **/
@Data
public class TreeRoot {
    /**
     * 该决策书的id
     */
    private Long treeId;

    /**
     * 该决策树的根节点,也是第一个规则节点的id
     */
    private Long treeRootNodeId;

    /**
     * 树的名称
     */
    private String treeName;

    private TreeRoot treeRoot;
}

-------------------------------------------------

/**
 * 功能描述: 返回值
 *
 * @author: WuChengXing
 * @create: 2021-06-26 11:59
 **/
@Data
@Builder
public class EngineResult {
    private String userId;

    private Long treeId;

    private Long treeNodeId;

    private String treeNodeValue;

}

决策树过滤器(LogicFilter):

/**
 * 功能描述: 决策树过滤器
 *
 * @author: WuChengXing
 * @create: 2021-06-26 12:15
 **/
public interface LogicFilter {

    /**
     * 逻辑过滤器
     *
     * @param matterValue   决策值
     * @param treeNodeLinks 决策节点
     * @return
     */
    Long filter(String matterValue, List<TreeNodeLink> treeNodeLinks);

    /**
     * 获取决策值方法: 及获取map中的值 age -> 25
     *
     * @param treeId
     * @param userId
     * @param decisionMatter 决策物料
     * @return
     */
    String matterValue(Long treeId, String userId, Map<String, String> decisionMatter);
}

实现类-抽象是实现类,提供相同的方法供调用(BaseLogicFilter):

/**
 * 功能描述: 基础决策过滤器,提供一些基础服务
 *
 * @author: WuChengXing
 * @create: 2021-06-26 12:19
 **/
public abstract class BaseLogicFilter implements LogicFilter {

    /**
     * 过滤到下一个节点(即第一个规则通过了,就指向下一个规则)
     * @param matterValue   决策值
     * @param treeNodeLinks 决策节点
     * @return
     */
    @Override
    public Long filter(String matterValue, List<TreeNodeLink> treeNodeLinks) {
        for (TreeNodeLink treeNodeLink : treeNodeLinks) {
            // 匹配上了这些规则,继续往下走(这里是“树枝”,里面有界限值和指向)
            if (decisionLogic(matterValue, treeNodeLink)) {
                // “树枝”上面的校验通过了,则返回下一个规则节点的id
                return treeNodeLink.getNodeIdTo();
            }
        }
        return 0L;
    }

    public abstract String matterValue(Long treeId, String userId, Map<String, String> decisionMatter);

    /**
     * 校验传入的表达式 =、>、<等
     * @param matterValue
     * @param treeNodeLink
     * @return
     */
    public Boolean decisionLogic(String matterValue, TreeNodeLink treeNodeLink) {
        switch (Objects.requireNonNull(ExpressionEnum.getByIndex(treeNodeLink.getRuleLimitType()))) {
            case EQUAL:
                return matterValue.equals(treeNodeLink.getRuleLimitValue());
            case GRANT:
                return Double.parseDouble(matterValue) > Double.parseDouble(treeNodeLink.getRuleLimitValue());
            case LESS:
                return Double.parseDouble(matterValue) < Double.parseDouble(treeNodeLink.getRuleLimitValue());
            case LESS_EQUAL:
                return Double.parseDouble(matterValue) <= Double.parseDouble(treeNodeLink.getRuleLimitValue());
            case GRANT_EQUAL:
                return Double.parseDouble(matterValue) >= Double.parseDouble(treeNodeLink.getRuleLimitValue());
            default:
                return false;
        }
    }
}

这里存在过滤规则,也就是找到对应的节点关系,filter() 方法可以找到下一个节点的id;

枚举和常量值

/**
 * 功能描述: 表达式枚举
 *
 * @author: WuChengXing
 * @create: 2021-06-26 13:27
 **/
@Getter
public enum ExpressionEnum {

    EQUAL(1, "等于"),
    GRANT(2, "大于"),
    LESS(3, "小于"),
    LESS_EQUAL(4, "小于等于"),
    GRANT_EQUAL(5, "大于等于"),
    ;

    ExpressionEnum(Integer index, String desc) {
        this.index = index;
        this.desc = desc;
    }

    private final Integer index;

    private final String desc;

    public static ExpressionEnum getByIndex(Integer index) {
        for (ExpressionEnum expressionEnum : values()) {
            if (index.equals(expressionEnum.getIndex())) {
                return expressionEnum;
            }
        }
        return null;
    }
}

----------------------------------------------- 

/**
 * 功能描述: 常量
 *
 * @author: WuChengXing
 * @create: 2021-06-26 13:18
 **/
public final class Constant {
    /**
     * 叶子节点
     */
    public static final Integer NODE_TYPE_LEAF = 1;

    /**
     * 果实节点
     */
    public static final Integer NODE_TYPE_FRUIT = 2;
}

抽象过滤的具体实现 :UserAgeFilter、UserGenderFilter

/**
 * 功能描述: 用户年龄逻辑判断节点
 *
 * @author: WuChengXing
 * @create: 2021-06-26 12:28
 **/
public class UserAgeFilter extends BaseLogicFilter {

    @Override
    public String matterValue(Long treeId, String userId, Map<String, String> decisionMatter) {
        return decisionMatter.get("age");
    }
}

--------------------------------------

/**
 * 功能描述: 性别逻辑判断节点
 *
 * @author: WuChengXing
 * @create: 2021-06-26 12:29
 **/
public class UserGenderFilter extends BaseLogicFilter {

    @Override
    public String matterValue(Long treeId, String userId, Map<String, String> decisionMatter) {
        return decisionMatter.get("gender");
    }
}

决策引擎(IEngine):


/**
 * 功能描述: 决策引擎
 *
 * @author: WuChengXing
 * @create: 2021-06-26 12:31
 **/
public interface IEngine {

    /**
     * 处理决策树,返回特定值
     * @param treeId
     * @param userId
     * @param treeRich
     * @param decisionMatter
     * @return
     */
    EngineResult process(final Long treeId, final String userId, TreeRich treeRich, final Map<String, String> decisionMatter);
}

决策引擎的基础实现(BaseEngine):

/**
 * 功能描述: 基础执行引擎
 *
 * @author: WuChengXing
 * @create: 2021-06-26 12:51
 **/
@Slf4j
public abstract class BaseEngine extends EngineConfig implements IEngine {

    public abstract EngineResult process(Long treeId, String userId, TreeRich treeRich, Map<String, String> decisionMatter);

    public TreeNode engineDecisionMaker(Long treeId, String userId, TreeRich treeRich, Map<String, String> decisionMatter) {
        TreeRoot treeRoot = treeRich.getTreeRoot();
        Map<Long, TreeNode> treeNodeMap = treeRich.getTreeNodeMap();
        // 规则树根Id
        Long treeRootNodeId = treeRoot.getTreeRootNodeId();
        TreeNode treeNode = treeNodeMap.get(treeRootNodeId);
        // treeNodeType: 1=叶子节点,2=果实
        while (treeNode.getTreeNodeType() == 1) {
            // 拿到对应的规则节点对应的key
            String ruleKey = treeNode.getRuleKey();
            // 拿到过滤规则
            LogicFilter logicFilter = logicFilterMap.get(ruleKey);
            // 拿到map对应的需要进行比较的值,即客户端传过来的值
            String matterValue = logicFilter.matterValue(treeId, userId, decisionMatter);
            // 这里去通过“树枝”,过了校验就获取下一个规则
            Long nextNodeId = logicFilter.filter(matterValue, treeNode.getTreeNodeLinks());
            treeNode = treeNodeMap.get(nextNodeId);
            log.info("决策树引擎:===> 返回值:ruleKey: {}, matterValue: {}, nextNodeId: {}", ruleKey, matterValue, nextNodeId);
        }
        return treeNode;
    }

}

决策引擎的实现(EngineHandler):

/**
 * 功能描述: 决策引擎的实现
 *
 * @author: WuChengXing
 * @create: 2021-06-26 13:09
 **/
public class EngineHandler extends BaseEngine {

    @Override
    public EngineResult process(Long treeId, String userId, TreeRich treeRich, Map<String, String> decisionMatter) {
        // 决策流程
        TreeNode treeNode = engineDecisionMaker(treeId, userId, treeRich, decisionMatter);
        return EngineResult.builder().userId(userId).treeId(treeId).treeNodeId(treeNode.getTreeNodeId()).treeNodeValue((String) treeNode.getTreeNodeValue()).build();
    }
}

过滤引擎(EngineConfig )

/**
 * 功能描述: 决策引擎配置
 *
 * @author: WuChengXing
 * @create: 2021-06-26 12:34
 **/
@Data
public class EngineConfig {
    protected static Map<String, LogicFilter> logicFilterMap;

    /**
     * 初始化过滤器
     */
    static {
        logicFilterMap = new HashMap<>(4);
        logicFilterMap.put("userAge", new UserAgeFilter());
        logicFilterMap.put("userGender", new UserGenderFilter());
    }
}

决策节点初始化(TreeNodeInit):

/**
 * 功能描述: 初始化数据
 *
 * @author: WuChengXing
 * @create: 2021-06-26 13:14
 **/
public class TreeNodeInit {
    public TreeRich init() {
        TreeNode treeNode_01 = TreeNode.builder()
                .treeId(10001L)
                .treeNodeId(1L)
                .treeNodeType(Constant.NODE_TYPE_LEAF)
                .treeNodeValue(null)
                .ruleKey("userGender")
                .ruleDesc("用户性别[男/女]")
                .build();

        // 1 --> 11
        TreeNodeLink treeNodeLink_11 = TreeNodeLink.builder()
                .nodeIdFrom(1L)
                .nodeIdTo(11L)
                .ruleLimitType(ExpressionEnum.EQUAL.getIndex())
                .ruleLimitValue("man")
                .build();

        // 1 --> 12
        TreeNodeLink treeNodeLink_12 = TreeNodeLink.builder()
                .nodeIdFrom(1L)
                .nodeIdTo(12L)
                .ruleLimitType(ExpressionEnum.EQUAL.getIndex())
                .ruleLimitValue("woman")
                .build();

        List<TreeNodeLink> treeNodeLinks_1 = new ArrayList<>();
        treeNodeLinks_1.add(treeNodeLink_11);
        treeNodeLinks_1.add(treeNodeLink_12);
        treeNode_01.setTreeNodeLinks(treeNodeLinks_1);

        // ------------------------------------------
        TreeNode treeNode_11 = TreeNode.builder()
                .treeId(10001L)
                .treeNodeId(11L)
                .treeNodeType(Constant.NODE_TYPE_LEAF)
                .treeNodeValue(null)
                .ruleKey("userAge")
                .ruleDesc("用户年龄")
                .build();

        // 11 --> 111
        TreeNodeLink treeNodeLink_111 = TreeNodeLink.builder()
                .nodeIdFrom(11L)
                .nodeIdTo(111L)
                .ruleLimitType(ExpressionEnum.LESS.getIndex())
                .ruleLimitValue("25")
                .build();

        // 11 --> 112
        TreeNodeLink treeNodeLink_112 = TreeNodeLink.builder()
                .nodeIdFrom(11L)
                .nodeIdTo(112L)
                .ruleLimitType(ExpressionEnum.GRANT_EQUAL.getIndex())
                .ruleLimitValue("25")
                .build();

        List<TreeNodeLink> treeNodeLinks_11 = new ArrayList<>();
        treeNodeLinks_11.add(treeNodeLink_111);
        treeNodeLinks_11.add(treeNodeLink_112);
        treeNode_11.setTreeNodeLinks(treeNodeLinks_11);

        // ------------------------------------------
        TreeNode treeNode_12 = TreeNode.builder()
                .treeId(10001L)
                .treeNodeId(12L)
                .treeNodeType(Constant.NODE_TYPE_LEAF)
                .treeNodeValue(null)
                .ruleKey("userAge")
                .ruleDesc("用户年龄")
                .build();

        // 12 --> 121
        TreeNodeLink treeNodeLink_121 = TreeNodeLink.builder()
                .nodeIdFrom(12L)
                .nodeIdTo(121L)
                .ruleLimitType(ExpressionEnum.LESS.getIndex())
                .ruleLimitValue("25")
                .build();

        // 11 --> 122
        TreeNodeLink treeNodeLink_122 = TreeNodeLink.builder()
                .nodeIdFrom(12L)
                .nodeIdTo(122L)
                .ruleLimitType(ExpressionEnum.GRANT_EQUAL.getIndex())
                .ruleLimitValue("25")
                .build();

        List<TreeNodeLink> treeNodeLinks_12 = new ArrayList<>();
        treeNodeLinks_12.add(treeNodeLink_121);
        treeNodeLinks_12.add(treeNodeLink_122);
        treeNode_12.setTreeNodeLinks(treeNodeLinks_12);

        /**
         * 结果
         */

        TreeNode treeNode_111 = TreeNode.builder()
                .treeId(10001L)
                .treeNodeId(111L)
                .treeNodeType(Constant.NODE_TYPE_FRUIT)
                .treeNodeValue("果实A ===> 男,<25")
                .build();
        TreeNode treeNode_112 = TreeNode.builder()
                .treeId(10001L)
                .treeNodeId(112L)
                .treeNodeType(Constant.NODE_TYPE_FRUIT)
                .treeNodeValue("果实B ===> 男,>=25")
                .build();
        TreeNode treeNode_121 = TreeNode.builder()
                .treeId(10001L)
                .treeNodeId(121L)
                .treeNodeType(Constant.NODE_TYPE_FRUIT)
                .treeNodeValue("果实C ===> 女,<25")
                .build();
        TreeNode treeNode_122 = TreeNode.builder()
                .treeId(10001L)
                .treeNodeId(122L)
                .treeNodeType(Constant.NODE_TYPE_FRUIT)
                .treeNodeValue("果实D ===> 女,>=25")
                .build();

        //--------- 树根 ----------------
        TreeRoot treeRoot = new TreeRoot();
        treeRoot.setTreeId(10001L);
        // 这里是记录这整个决策树的第一个规则节点是什么
        treeRoot.setTreeRootNodeId(1L);
        treeRoot.setTreeName("决策树");
        Map<Long, TreeNode> treeNodeMap = new HashMap<>(16);
        treeNodeMap.put(1L, treeNode_01);
        treeNodeMap.put(11L, treeNode_11);
        treeNodeMap.put(12L, treeNode_12);
        treeNodeMap.put(111L, treeNode_111);
        treeNodeMap.put(112L, treeNode_112);
        treeNodeMap.put(121L, treeNode_121);
        treeNodeMap.put(122L, treeNode_122);
        TreeRich treeRich = new TreeRich();
        treeRich.setTreeRoot(treeRoot);
        treeRich.setTreeNodeMap(treeNodeMap);
        return treeRich;
    }
}

这个决策节点初始化操作可以放到数据库中的,通过前端UI去操作也是可以的;类似于树节点配置的功能,然后将组合的新树结构存入数据库就行;这样配置起来比较方便;

测试


/**
 * 功能描述: 组合模式
 *
 * @author: WuChengXing
 * @create: 2021-06-26 11:41
 **/
@Slf4j
public class CombinationModeTest {
    public static void main(String[] args) {
        TreeNodeInit treeNodeInit = new TreeNodeInit();
        TreeRich init = treeNodeInit.init();

        IEngine engine = new EngineHandler();
        Map<String, String> decisionMap = new HashMap<>(2);
        // 具体的参数,用于去跟已经配置好了的规则节点去对比
        decisionMap.put("gender", "woman");
        decisionMap.put("age", "24");
        EngineResult result = engine.process(10001L, "sasassa", init, decisionMap);
        log.info("result ==> {}", JSON.toJSON(result));
    }
}

结果:

21:36:17.610 [main] INFO com.simple.designpatterns.pattern23.structuretype.combination.service.BaseEngine - 决策树引擎:===> 返回值:ruleKey: userGender, matterValue: woman, nextNodeId: 12
21:36:17.614 [main] INFO com.simple.designpatterns.pattern23.structuretype.combination.service.BaseEngine - 决策树引擎:===> 返回值:ruleKey: userAge, matterValue: 24, nextNodeId: 121
21:36:17.699 [main] INFO com.simple.designpatterns.pattern23.structuretype.combination.CombinationModeTest - result ==> {"treeId":10001,"treeNodeId":121,"treeNodeValue":"果实C ===> 女,<25","userId":"sasassa"}