0%

初学 Java 设计模式(九):实战组合模式 「决策树实现精准化运营」

一、组合模式介绍

1. 解决的问题

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

2. 定义

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

3. 应用场景

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

二、组合模式优缺点

1. 优点

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

2. 缺点

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

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

1. 实例场景

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

在这里插入图片描述

2. 组合模式实现

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

基础概念

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

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

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

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

决策树遍历步骤

  1. 当发现节点为非叶子节点时,根据策略名获取当前策略。
  2. 根据策略数据遍历节点策略列表,判断满足哪种节点策略。
  3. 根据满足的节点策略获取下一个节点。
  4. 当发现节点为叶子节点,返回节点数据,遍历结束。
2.1 工程结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 树相关模型

决策树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 决策树
*/
@Getter
@Setter
public class Tree {
/**
* id
*/
private Long id;

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

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

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

决策树节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* 决策树节点
*/
@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;
}

决策树节点决策

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 决策树节点决策
*/
@Getter
@Setter
public class TreeNodeDecision {
/**
* 决策名称
*/
private String name;

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

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

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

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

逻辑决策器接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 逻辑决策器接口
*/
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);
}

逻辑基础决策器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* 逻辑基础决策器
*/
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;
}
}
}

用户性别决策器

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

用户年龄决策器

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

决策结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 决策结果
*/
@Getter
@Setter
@AllArgsConstructor
public class DecisionResult {
/**
* 用户id
*/
private String userId;

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

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

决策树服务接口

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

决策树基础服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* 决策树基础服务
*/
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;
}
}

用户推送决策树服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 用户推送决策树服务
*/
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 测试验证类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
/**
* 决策树测试类
*/
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 测试结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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