一、组合模式介绍

1. 解决的问题

在树形结构的问题中,模糊了简单元素和复杂元素的概念,客户端可以像处理简单元素一样来处理复杂元素,从而使客户端和复杂元素的内部元素解耦。

2. 定义

组合模式是一种结构型设计模式,可以使用它将对象组合成树状结构,并能像使用独立对象一样使用它们。

3. 应用场景

  • 需要实现树状对象结构,可以使用组合模式。
  • 希望客户端代码以相同方式处理简单和复杂元素,可以使用组合模式。

二、组合模式优缺点

1. 优点

  • 可以利用多态和递归机制更方便地使用复杂树结构。
  • 开闭原则:无需更改现有代码,就可以在应用中添加新元素,使其成为对象树的一部分。

2. 缺点

  • 对于功能差异较大的类,提供公共接口会有困难。在特定情况下,需要过渡一般化组件接口,使其变得令人难以理解。

三、组合模式应用实例:决策树实现精准化运营

1. 实例场景

精准化运营在我们的业务开发中越来越重要,产品往往会根据性别、年龄、查看偏好等条件来决定给不同范围的用户推送不同的消息,目前就以该场景来实现组合场景。

组合模式-实例场景-精准化运营

2. 组合模式实现

本次组合模式的实现思想是依托决策树的形式。

基础概念

决策树记录了当前根节点和全部节点列表。

非叶子节点包含策略名、节点策略列表。

节点策略包含下一个字节点、当前决策类型(大于、等于等条件)、当前决策类型。

叶子节点定义了最终的策略,如推送二次元文章、推送财经类文章等。

决策树遍历步骤

  1. 当发现节点为非叶子节点时,根据策略名获取当前策略。
  2. 根据策略数据遍历节点策略列表,判断满足哪种节点策略。
  3. 根据满足的节点策略获取下一个节点。
  4. 当发现节点为叶子节点,返回节点数据,遍历结束。
2.1 工程结构
composite-pattern
└─ src
    ├─ main
    │    └─ java
    │    └─ org.design.pattern.composite
    │       ├─ model
    │       │  ├─ tree
    │       │  │  ├─ Tree.java
    │       │  │  ├─ TreeNode.java
    │       │  │  └─ TreeNodeDecision.java
    │       │  └─ decision
    │       │     ├─ DecisionResult.java
    │       │     ├─ LogicDecision.java
    │       │     ├─ LogicBaseDecision.java
    │       │     └─ impl
    │       │        ├─ UserGenderDecision.java
    │       │        └─ UserAgeDecision.java
    │       └─ service
    │          ├─ DecisionTreeService.java
    │          ├─ DecisionTreeBaseService.java
    │          └─ impl
    │              └─ UserPushDecisionTreeService.java
    └─ test
        └─ java
            └─ org.design.pattern.composite.test
                  └─ TreeTest.java
2.2 代码实现
2.2.1 树相关模型

决策树

/**
 * 决策树
 */
@Getter
@Setter
public class Tree {
    /**
     * id
     */
    private Long id;

    /**
     * 名称
     */
    private String name;

    /**
     * 根节点
     */
    private TreeNode treeRootNode;

    /**
     * 节点列表
     */
    private Map<Long, TreeNode> treeNodeMap;
}

决策树节点

/**
 * 决策树节点
 */
@Setter
@Getter
public class TreeNode {
    /**
     * 节点id
     */
    private Long nodeId;

    /**
     * 节点类型
     */
    private String NodeType;

    /**
     * 节点值
     */
    private Object NodeValue;

    /**
     * 来源节点id
     */
    private Long fromNodeId;

    /**
     * 去向节点id
     */
    private Long toNodeId;

    /**
     * 决策名
     */
    private String decisionName;

    /**
     * 决策规则列表
     */
    private List<TreeNodeDecision> treeNodeDecisionList;
}

决策树节点决策

/**
 * 决策树节点决策
 */
@Getter
@Setter
public class TreeNodeDecision {
    /**
     * 决策名称
     */
    private String name;

    /**
     * 来源节点
     */
    private Long fromNode;

    /**
     * 去向节点
     */
    private Long toNode;

    /**
     * 决策类型
     */
    private String decisionType;

    /**
     * 决策值
     */
    private String decisionValue;
}
2.2.2 决策相关模型

逻辑决策器接口

/**
 * 逻辑决策器接口
 */
public interface LogicDecision {
    /**
     * 获取决策值
     * @param treeId 决策树id
     * @param userId 用户id
     * @param userData 用户数据
     * @return String
     */
    String getDecisionValue(Long treeId, String userId, Map<String, String> userData);

    /**
     * 过滤出节点决策
     * @param decisionValue 决策值
     * @param treeNodeDecisionList 节点决策列表
     * @return
     */
    Long filterDecisionNode(String decisionValue, List<TreeNodeDecision> treeNodeDecisionList);
}

逻辑基础决策器

/**
 * 逻辑基础决策器
 */
public abstract class LogicBaseDecision implements LogicDecision {
    @Override
    public abstract String getDecisionValue(Long treeId, String userId, Map<String, String> userData);;

    @Override
    public Long filterDecisionNode(String decisionValue, List<TreeNodeDecision> treeNodeDecisionList) {
        for (TreeNodeDecision treeNodeDecision : treeNodeDecisionList) {
            if (filterDecisionByType(decisionValue, treeNodeDecision)) {
                return treeNodeDecision.getToNode();
            }
        }
        return 0L;
    }

    /**
     * 根据类型过滤决策
     * @param decisionValue 决策值
     * @param treeNodeDecision 节点决策
     * @return boolean
     */
    private boolean filterDecisionByType(String decisionValue, TreeNodeDecision treeNodeDecision) {
        switch (treeNodeDecision.getDecisionType()) {
            case "eq":
                return decisionValue.equals(treeNodeDecision.getDecisionValue());
            case "gt":
                return Double.parseDouble(decisionValue) > Double.parseDouble(treeNodeDecision.getDecisionValue());
            case "gte":
                return Double.parseDouble(decisionValue) >= Double.parseDouble(treeNodeDecision.getDecisionValue());
            case "lt":
                return Double.parseDouble(decisionValue) < Double.parseDouble(treeNodeDecision.getDecisionValue());
            case "lte":
                return Double.parseDouble(decisionValue) <= Double.parseDouble(treeNodeDecision.getDecisionValue());
            default:
                return false;
        }
    }
}

用户性别决策器

/**
 * 用户性别决策器
 */
public class UserGenderDecision extends LogicBaseDecision {
    @Override
    public String getDecisionValue(Long treeId, String userId, Map<String, String> userData) {
        return userData.get("gender");
    }
}

用户年龄决策器

/**
 * 用户年龄决策器
 */
public class UserAgeDecision extends LogicBaseDecision {
    @Override
    public String getDecisionValue(Long treeId, String userId, Map<String, String> userData) {
        return userData.get("age");
    }
}

决策结果

/**
 * 决策结果
 */
@Getter
@Setter
@AllArgsConstructor
public class DecisionResult {
    /**
     * 用户id
     */
    private String userId;

    /**
     * 决策树id
     */
    private Long treeId;

    /**
     * 结果节点值
     */
    private Object resultNodeValue;
}
2.2.3 决策树服务

决策树服务接口

/**
 * 决策树服务接口
 */
public interface DecisionTreeService {
    DecisionResult process(final Tree treeId, final String userId, final Map<String, String> userData);
}

决策树基础服务

/**
 * 决策树基础服务
 */
public abstract class DecisionTreeBaseService implements DecisionTreeService {
    private final Logger log = LoggerFactory.getLogger(DecisionTreeBaseService.class);

    /**
     * 决策map
     */
    protected Map<String, LogicDecision> logicDecisionMap = new ConcurrentHashMap<>();

    @Override
    public abstract DecisionResult process(Tree tree, String userId, Map<String, String> userData);

    /**
     * 寻找叶子节点
     * @param tree 决策树
     * @param userId 用户id
     * @param userData 用户数据
     * @return
     */
    protected TreeNode findLeafNode(Tree tree, String userId, Map<String, String> userData) {
        TreeNode treeNode = tree.getTreeRootNode();
        while (treeNode.getNodeType().equals("branch")) {
            //获取节点决策
            LogicDecision logicDecision = logicDecisionMap.get(treeNode.getDecisionName());
            //获取需要决策的值
            String decisionValue = logicDecision.getDecisionValue(tree.getId(), userId, userData);
            //根据节点决策列表,获取下一节点id
            Long nextNodeId = logicDecision.filterDecisionNode(decisionValue, treeNode.getTreeNodeDecisionList());
            log.info(
                    "决策树:{},用户id:{},当前节点id:{},下一节点id:{},决策名:{},决策值:{}",
                    tree.getId(),
                    userId,
                    treeNode.getNodeId(),
                    nextNodeId,
                    treeNode.getDecisionName(),
                    decisionValue
            );
            treeNode = tree.getTreeNodeMap().get(nextNodeId);
        }
        return treeNode;
    }
}

用户推送决策树服务

/**
 * 用户推送决策树服务
 */
public class UserPushDecisionTreeService extends DecisionTreeBaseService {
    public UserPushDecisionTreeService() {
        this.logicDecisionMap.put("gender", new UserGenderDecision());
        this.logicDecisionMap.put("age", new UserAgeDecision());
    }

    @Override
    public DecisionResult process(Tree tree, String userId, Map<String, String> userData) {
        //寻找最终决策节点
        TreeNode resultNode = findLeafNode(tree, userId, userData);
        //决策结果
        return new DecisionResult(userId, tree.getId(), resultNode.getNodeValue());
    }
}
2.3 测试验证
2.3.1 测试验证类
/**
 * 决策树测试类
 */
public class TreeTest {
    private final Logger log = LoggerFactory.getLogger(TreeTest.class);

    private Tree tree;

    @Before
    public void initTree() {
        //根节点(用户性别)
        TreeNode rootNode = new TreeNode();
        rootNode.setNodeId(1L);
        rootNode.setNodeType("branch");

        //根节点的子节点
        //子节点1
        TreeNode rootChildOne = new TreeNode();
        rootChildOne.setNodeId(11L);
        rootChildOne.setNodeType("branch");
        rootChildOne.setFromNodeId(rootNode.getNodeId());
        //子节点2
        TreeNode rootChildTwo = new TreeNode();
        rootChildTwo.setNodeId(12L);
        rootChildTwo.setNodeType("branch");
        rootChildTwo.setFromNodeId(rootNode.getNodeId());

        //孙节点
        //孙节点1
        TreeNode grandChildrenOne = new TreeNode();
        grandChildrenOne.setNodeId(111L);
        grandChildrenOne.setNodeType("leaf");
        grandChildrenOne.setNodeValue("推送二次元类文章");
        grandChildrenOne.setFromNodeId(rootChildOne.getFromNodeId());
        //孙节点2
        TreeNode grandChildrenTwo = new TreeNode();
        grandChildrenTwo.setNodeId(112L);
        grandChildrenTwo.setNodeType("leaf");
        grandChildrenTwo.setNodeValue("推送财经类文章");
        grandChildrenTwo.setFromNodeId(rootChildOne.getFromNodeId());
        //孙节点3
        TreeNode grandChildrenThree = new TreeNode();
        grandChildrenThree.setNodeId(121L);
        grandChildrenThree.setNodeType("leaf");
        grandChildrenThree.setNodeValue("推送A类文章");
        grandChildrenThree.setFromNodeId(rootChildTwo.getFromNodeId());
        //孙节点4
        TreeNode grandChildrenFour = new TreeNode();
        grandChildrenFour.setNodeId(122L);
        grandChildrenFour.setNodeType("leaf");
        grandChildrenFour.setNodeValue("推送B类文章");
        grandChildrenFour.setFromNodeId(rootChildOne.getFromNodeId());

        //根节点决策
        String rootDecisionName = "gender";
        rootNode.setDecisionName(rootDecisionName);
        //性别男
        TreeNodeDecision manDecision = new TreeNodeDecision();
        manDecision.setDecisionType("eq");
        manDecision.setDecisionValue("man");
        manDecision.setFromNode(rootNode.getNodeId());
        manDecision.setToNode(rootChildOne.getNodeId());
        //性别女
        TreeNodeDecision womanDecision = new TreeNodeDecision();
        womanDecision.setDecisionType("eq");
        womanDecision.setDecisionValue("woman");
        womanDecision.setFromNode(rootNode.getNodeId());
        womanDecision.setToNode(rootChildTwo.getNodeId());
        //设置根节点决策
        List<TreeNodeDecision> rootNodeDecisionList = new ArrayList<>(2);
        rootNodeDecisionList.add(manDecision);
        rootNodeDecisionList.add(womanDecision);
        rootNode.setTreeNodeDecisionList(rootNodeDecisionList);

        //子节点决策
        String childDecisionName = "age";
        rootChildOne.setDecisionName(childDecisionName);
        rootChildTwo.setDecisionName(childDecisionName);
        //子节点1决策
        //男,年龄≤24
        TreeNodeDecision ageDecisionOne = new TreeNodeDecision();
        ageDecisionOne.setDecisionType("lte");
        ageDecisionOne.setDecisionValue("24");
        ageDecisionOne.setFromNode(rootChildOne.getNodeId());
        ageDecisionOne.setToNode(grandChildrenOne.getNodeId());
        //男,年龄>24
        TreeNodeDecision ageDecisionTwo = new TreeNodeDecision();
        ageDecisionTwo.setDecisionType("gt");
        ageDecisionTwo.setDecisionValue("24");
        ageDecisionTwo.setFromNode(rootChildOne.getNodeId());
        ageDecisionTwo.setToNode(grandChildrenTwo.getNodeId());
        //设置子节点1决策
        List<TreeNodeDecision> rootChildOneDecisionList = new ArrayList<>(2);
        rootChildOneDecisionList.add(ageDecisionOne);
        rootChildOneDecisionList.add(ageDecisionTwo);
        rootChildOne.setTreeNodeDecisionList(rootChildOneDecisionList);

        //子节点2决策
        //女,年龄≤35
        TreeNodeDecision ageDecisionThree = new TreeNodeDecision();
        ageDecisionThree.setDecisionType("lte");
        ageDecisionThree.setDecisionValue("35");
        ageDecisionThree.setFromNode(rootChildTwo.getNodeId());
        ageDecisionThree.setToNode(grandChildrenThree.getNodeId());
        //女,年龄>35
        TreeNodeDecision ageDecisionFour = new TreeNodeDecision();
        ageDecisionFour.setDecisionType("gt");
        ageDecisionFour.setDecisionValue("35");
        ageDecisionFour.setFromNode(rootChildTwo.getNodeId());
        ageDecisionFour.setToNode(grandChildrenFour.getNodeId());
        //设置子节点2决策
        List<TreeNodeDecision> rootChildTwoDecisionList = new ArrayList<>(2);
        rootChildTwoDecisionList.add(ageDecisionThree);
        rootChildTwoDecisionList.add(ageDecisionFour);
        rootChildTwo.setTreeNodeDecisionList(rootChildTwoDecisionList);

        //设置决策树
        tree = new Tree();
        tree.setId(1L);
        tree.setName("精准化运营消息推送决策树");
        //决策树根节点
        tree.setTreeRootNode(rootNode);
        //决策树节点
        Map<Long, TreeNode> treeNodeMap = new TreeMap<>();
        treeNodeMap.put(rootNode.getNodeId(), rootNode);
        treeNodeMap.put(rootChildOne.getNodeId(), rootChildOne);
        treeNodeMap.put(rootChildTwo.getNodeId(), rootChildTwo);
        treeNodeMap.put(grandChildrenOne.getNodeId(), grandChildrenOne);
        treeNodeMap.put(grandChildrenTwo.getNodeId(), grandChildrenTwo);
        treeNodeMap.put(grandChildrenThree.getNodeId(), grandChildrenThree);
        treeNodeMap.put(grandChildrenFour.getNodeId(), grandChildrenFour);
        tree.setTreeNodeMap(treeNodeMap);
    }

    @Test
    public void testUserPushDecisionTree() {
        UserPushDecisionTreeService userPushDecisionTreeService = new UserPushDecisionTreeService();
        //用户数据
        Map<String, String> userData = new HashMap<>();
        userData.put("gender", "man");
        userData.put("age", "29");
        DecisionResult result = userPushDecisionTreeService.process(tree, "yiyufxst", userData);
        log.info("性别:{},年龄:{}的用户推送{}", userData.get("gender"), userData.get("age"), result.getResultNodeValue());
        userData.put("age", "24");
        result = userPushDecisionTreeService.process(tree, "yiyufxst", userData);
        log.info("性别:{},年龄:{}的用户推送{}", userData.get("gender"), userData.get("age"), result.getResultNodeValue());
        userData.put("gender", "woman");
        userData.put("age", "35");
        result = userPushDecisionTreeService.process(tree, "马冬梅", userData);
        log.info("性别:{},年龄:{}的用户推送{}", userData.get("gender"), userData.get("age"), result.getResultNodeValue());
        userData.put("age", "40");
        result = userPushDecisionTreeService.process(tree, "马冬梅", userData);
        log.info("性别:{},年龄:{}的用户推送{}", userData.get("gender"), userData.get("age"), result.getResultNodeValue());
    }
}
2.3.2 测试结果
18:19:50.198 [main] INFO  o.d.p.c.s.DecisionTreeBaseService - 决策树:1,用户id:yiyufxst,当前节点id:1,下一节点id:11,决策名:gender,决策值:man
18:19:50.201 [main] INFO  o.d.p.c.s.DecisionTreeBaseService - 决策树:1,用户id:yiyufxst,当前节点id:11,下一节点id:112,决策名:age,决策值:29
18:19:50.201 [main] INFO  o.d.pattern.composite.test.TreeTest - 性别:man,年龄:29的用户推送推送财经类文章
18:19:50.201 [main] INFO  o.d.p.c.s.DecisionTreeBaseService - 决策树:1,用户id:yiyufxst,当前节点id:1,下一节点id:11,决策名:gender,决策值:man
18:19:50.201 [main] INFO  o.d.p.c.s.DecisionTreeBaseService - 决策树:1,用户id:yiyufxst,当前节点id:11,下一节点id:111,决策名:age,决策值:24
18:19:50.201 [main] INFO  o.d.pattern.composite.test.TreeTest - 性别:man,年龄:24的用户推送推送二次元类文章
18:19:50.202 [main] INFO  o.d.p.c.s.DecisionTreeBaseService - 决策树:1,用户id:马冬梅,当前节点id:1,下一节点id:12,决策名:gender,决策值:woman
18:19:50.202 [main] INFO  o.d.p.c.s.DecisionTreeBaseService - 决策树:1,用户id:马冬梅,当前节点id:12,下一节点id:121,决策名:age,决策值:35
18:19:50.202 [main] INFO  o.d.pattern.composite.test.TreeTest - 性别:woman,年龄:35的用户推送推送A类文章
18:19:50.202 [main] INFO  o.d.p.c.s.DecisionTreeBaseService - 决策树:1,用户id:马冬梅,当前节点id:1,下一节点id:12,决策名:gender,决策值:woman
18:19:50.202 [main] INFO  o.d.p.c.s.DecisionTreeBaseService - 决策树:1,用户id:马冬梅,当前节点id:12,下一节点id:122,决策名:age,决策值:40
18:19:50.202 [main] INFO  o.d.pattern.composite.test.TreeTest - 性别:woman,年龄:40的用户推送推送B类文章

Process finished with exit code 0

四、组合模式结构

组合模式-模式结构图

  1. 组件(Component)接口描述了树中简单项目和复杂项目所共有的操作。
  2. 叶节点(Leaf)是树的基础结构,它不包含子项目。

    一般情况下,叶节点最终会完成大部分的实际工作,因为它们无法把工作委派给其他部分。

  3. 容器(Container)——又名“组合(Composite)”——是包含叶节点或其他容器等子项目的单位。容器不知道其子项目所属的具体类,它只通过通用的组件接口与其子项目交互。

    容器接收到请求会将工作分配给自己的子项目,处理中间结果,然后将最终结果返回给客户端。

  4. 客户端(Client)通过组件接口与所有项目交互。因此,客户端能以相同方式与树状结构中的简单或复杂项目交互。

设计模式并不难学,其本身就是多年经验提炼出的开发指导思想,关键在于多加练习,带着使用设计模式的思想去优化代码,就能构建出更合理的代码。

源码地址:https://github.com/yiyufxst/design-pattern-java

参考资料:
小博哥重学设计模式:https://github.com/fuzhengwei/itstack-demo-design
深入设计模式:https://refactoringguru.cn/design-patterns/catalog


易羽fxst
158 声望5 粉丝