组合模式简单介绍

通过把相似对象或方法组合成一组可被调用的结构树对象的设计思路,成为组合模式。

这种设计方式可以让服务组节点进行自由组合并对外提供服务调用,有些调用方需要使用AB组合,有些调用方需要使用CBA组合,还有一些调用方可能只使用三者中的一个、这时就可以使用组合模式构建服务,对于不同类型的调用方配置不同的组织关系树,而这个树形结构可以配置到数据库中,通过程序图形界面控制树形结构的创建和修改。

简单的场景模拟

image-20211109220452215

如图这是一个简化版的营销规则决策树,根据性别、年龄的不同组合,发展不同类型的优惠券,目的是刺激消费,对精准用户进行促活。

违背设计模式实现

这里举一个if…else的简单实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.bestrookie.design;
/**
* @author bestrookie
* @date 2021/11/9 9:01 上午
*/
public class EngineController {
public String process(final String userId, final String userSex, final int userAge){
System.out.println("if...else实现方式判断用户结果 userId: "+userId+" userSex: "+userSex+"userAge: "+userAge);
if ("man".equals(userSex)){
if (userAge < 25){
return "果实A";
}
return "果实B";
}
if ("woman".equals(userSex)){
if (userAge < 25){
return "果实C";
}
return "果实D";
}
return null;
}
}

这样看起来是不是十分的快捷,但是如果需要扩展,只能一层一层的加if..else,出了问题难以排查,新加功能的风险较高。

组合模式重构代码

重构代码的改动量相对来说会比较大,为了把不同类型的决策节点和最终的果实组装成一颗可被程序运行的决策树,需要适配设计和工厂方法调用,具体会体现在定义接口和抽象类、初始化配置决策点(性别、年龄)上。

先来看一下我们的工程目录

image-20211109223537165

组合模式代码关系类图

image-20211109223356774

决策树模型

image-20211109223451171

整个类图关系包括了属性结构原子模块实现关系、树形结构执行引擎两部分内容。树形结构原子模块实现关系从LogicFilter开始定义适配的决策过滤器,BaseLogic是对接口的实现,以提供最基本的通用方法。UserAgeFilter和UserGenerFilter是两个具体的实现类,用于判断年龄和性别。树状结构执行引擎是对这棵树可以被组织出来的决策树进行执行的引擎,同样定义了引擎接口和基础的配置,在配置里面设定了需要的模式决策节点。另外在类图中插入了一个树形结构关系模拟树形结构,由树的7个节点1、11、12、111、112、121、122左右串联,组合出一颗二叉关系树。

EngineResult

决策结果;返回对象信息

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
package com.bestrookie.design.domain.model.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author bestrookie
* @date 2021/11/9 2:45 下午
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class EngineResult {
private boolean isSuccess; //执行结果
private String userId; //用户ID
private Long treeId; //规则书ID
private Long nodeId; //果实节点ID
private String nodeValue; //果实节点值

public EngineResult(String userId, Long treeId, Long treeNodeId, String nodeValue) {
this.isSuccess = true;
this.treeId = treeId;
this.userId = userId;
this.nodeId = treeNodeId;
this.nodeValue = nodeValue;
}
}
TreeRoot

树根信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.bestrookie.design.domain.model.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author bestrookie
* @date 2021/11/9 11:23 上午
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TreeRoot {
private Long treeId; //规则树ID
private Long treeRootNodeId; //规则树根ID
private String treeName; //规则树名称
}

树节点链路关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.bestrookie.design.domain.model.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author bestrookie
* @date 2021/11/9 9:56 上午
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TreeNodeLink {
private Long nodeIdFrom; //节点From
private Long nodeIdTo; // 节点To
private Integer ruleLimitType; //限定类型
private String ruleLimitValue; //限定值
}
TreeNode

树节点:包括叶子节点、果实节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.bestrookie.design.domain.model.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* @author bestrookie
* @date 2021/11/9 9:54 上午
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TreeNode {
private Long treeId; //规则数ID;
private Long treeNodeId; //规则树节点ID;
private Integer nodeType; //节点类型;1、子叶 2、果实
private String nodeValue; //节点值【nodeType =2】果实值
private String ruleKey; //规则key
private String ruleDesc; //规则描述
private List<TreeNodeLink> treeNodeLinkList; //节点链路
}
TreeRich

聚合对象:包含组织树信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.bestrookie.design.domain.model.aggregates;
import com.bestrookie.design.domain.model.vo.TreeNode;
import com.bestrookie.design.domain.model.vo.TreeRoot;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
/**
* @author bestrookie
* @date 2021/11/9 2:50 下午
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TreeRich {
private TreeRoot treeRoot;
private Map<Long, TreeNode> treeNodeMap;
}

以上是在model包下的对象,用于描述决策树的各项信息类,包括:聚合对象、决策结果、树节点、树节点链路关系和树根信息。

树节点逻辑过滤器

LogicFilter:

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
package com.bestrookie.design.domain.service.logic;

import com.bestrookie.design.domain.model.vo.TreeNodeLink;

import java.util.List;
import java.util.Map;

/**
* @author bestrookie
* @description 逻辑决策器
* @date 2021/11/9 11:43 上午
*/
public interface LogicFilter {
/**
* 逻辑决策器
* @param matterValue 决策值
* @param treeNodeLinkList 决策节点
* @return 下一个节点
*/
Long filter(String matterValue, List<TreeNodeLink> treeNodeLinkList);

/**
* 获取决策值方法
* @param treeId 决策物料
* @return 决策值
*/
String matterValue(Long treeId, String userId, Map<String, String> decisionMatter);
}

这部分定义了适配的通用接口和相应的方法:逻辑决策器方法、获取决策值方法,让每一个提供决策能力的节点都必须实现此接口,保证统一性。

决策抽象类提供基础服务
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
package com.bestrookie.design.domain.service.logic;
import com.bestrookie.design.domain.model.vo.TreeNodeLink;
import com.bestrookie.design.domain.service.logic.LogicFilter;
import java.util.List;
import java.util.Map;

/**
* @author bestrookie
* @date 2021/11/9 1:51 下午
*/
public abstract class BaseLogic implements LogicFilter {
@Override
public Long filter(String matterValue, List<TreeNodeLink> treeNodeLinkList) {
for (TreeNodeLink nodeLink : treeNodeLinkList) {
if (decisionLogic(matterValue,nodeLink)){
return nodeLink.getNodeIdTo();
}
}
return 0L;
}
@Override
public abstract String matterValue(Long treeId, String userId, Map<String, String> decisionMatter);


private boolean decisionLogic(String matterValue, TreeNodeLink nodeLink){
switch (nodeLink.getRuleLimitType()){
case 1:
return matterValue.equals(nodeLink.getRuleLimitValue());
case 2:
return Double.parseDouble(matterValue) > Double.parseDouble(nodeLink.getRuleLimitValue());
case 3:
return Double.parseDouble(matterValue) < Double.parseDouble(nodeLink.getRuleLimitValue());
case 4:
return Double.parseDouble(matterValue) <= Double.parseDouble(nodeLink.getRuleLimitValue());
case 5:
return Double.parseDouble(matterValue) >= Double.parseDouble(nodeLink.getRuleLimitValue());
default:
return false;
}
}

}

在抽象方法中实现了接口方法,同时定义了基本的决策方法:1、2、3、4、5,等于、小于、大于、小于或等于、大于或等于的判断逻辑。同时定义了抽象方法,让每个实现的接口都必须按照规则提供决策值,这个决策值用于进行逻辑比对。

树节点逻辑实现类

年龄节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.bestrookie.design.domain.service.logic.impl;
import com.bestrookie.design.domain.service.logic.BaseLogic;
import java.util.Map;
/**
* @author bestrookie
* @date 2021/11/9 2:21 下午
*/
public class UserAgeFilter extends BaseLogic {
@Override
public String matterValue(Long treeId, String userId, Map<String, String> decisionMatter) {
return decisionMatter.get("age");
}
}

性别节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.bestrookie.design.domain.service.logic.impl;
import com.bestrookie.design.domain.service.logic.BaseLogic;
import java.util.Map;
/**
* @author bestrookie
* @date 2021/11/9 2:22 下午
*/
public class UserGenderFilter extends BaseLogic {
@Override
public String matterValue(Long treeId, String userId, Map<String, String> decisionMatter) {
return decisionMatter.get("gender");
}
}

以上连个决策逻辑的节点获取值的方式都非常简单,只获取用户入参即可。实际的业务开发可能需要通过数据库、RPC接口和缓存运算等各种方式获取产品需要的信息。

决策引擎接口定义
1
2
3
4
5
6
7
8
9
10
11
12
package com.bestrookie.design.domain.service.engine;
import com.bestrookie.design.domain.model.aggregates.TreeRich;
import com.bestrookie.design.domain.model.vo.EngineResult;
import java.util.Map;

/**
* @author bestrookie
* @date 2021/11/9 2:31 下午
*/
public interface IEngine {
EngineResult process(final Long treeId, final String userId, TreeRich treeRich, final Map<String, String> decisionMatter);
}

对于调用方来说,也同样需要定义统一的接口操作,这样的好处是便于后续扩展出不能类型的决策引擎,也是建造不同的决策工厂。

决策点配置
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
package com.bestrookie.design.domain.service.engine;

import com.bestrookie.design.domain.service.logic.LogicFilter;
import com.bestrookie.design.domain.service.logic.impl.UserAgeFilter;
import com.bestrookie.design.domain.service.logic.impl.UserGenderFilter;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* @author bestrookie
* @date 2021/11/9 2:57 下午
*/
public class EngineConfig {
static Map<String, LogicFilter> logicFilterMap;
static {
logicFilterMap = new ConcurrentHashMap<String,LogicFilter>();
logicFilterMap.put("userAge",new UserAgeFilter());
logicFilterMap.put("userGender",new UserGenderFilter());
}
public Map<String, LogicFilter> getLogicFilterMap(){
return logicFilterMap;
}
public void setLogicFilterMap(Map<String,LogicFilter> logicFilterMap){
EngineConfig.logicFilterMap = logicFilterMap;
}
}

这里可以将服务的决策点配置到Map结构中,这样的Map结构可以抽取到XML或数据库中,就可以方便地在ERP界面中配置任务了。当需要变更时,不用改用代码便于管理。

基础决策引擎功能
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
package com.bestrookie.design.domain.service.engine;
import com.bestrookie.design.domain.model.aggregates.TreeRich;
import com.bestrookie.design.domain.model.vo.EngineResult;
import com.bestrookie.design.domain.model.vo.TreeNode;
import com.bestrookie.design.domain.model.vo.TreeRoot;
import com.bestrookie.design.domain.service.logic.LogicFilter;
import java.util.Map;

/**
* @author bestrookie
* @date 2021/11/9 3:06 下午
*/
public abstract class EngineBase extends EngineConfig implements IEngine{
@Override
public abstract EngineResult process(Long treeId, String userId, TreeRich treeRich, Map<String, String> decisionMatter);
protected TreeNode engineDecisionMaker(TreeRich treeRich,Long treeId,String userId,Map<String, String> decisionMatter){
TreeRoot treeRoot = treeRich.getTreeRoot();
Map<Long,TreeNode> treeNodeMap = treeRich.getTreeNodeMap();
//规则树根id
Long rootNodeId = treeRoot.getTreeRootNodeId();
TreeNode treeNodeInfo = treeNodeMap.get(rootNodeId);
//节点类型【NodeType】 1 叶子 2 果实
while (treeNodeInfo.getNodeType().equals(1)){
String ruleKey = treeNodeInfo.getRuleKey();
LogicFilter logicFilter = logicFilterMap.get(ruleKey);
String matterValue = logicFilter.matterValue(treeId, userId, decisionMatter);
Long nextNode = logicFilter.filter(matterValue, treeNodeInfo.getTreeNodeLinkList());
treeNodeInfo = treeNodeMap.get(nextNode);
System.out.println("决策树引擎 userId:"+userId+" treeId: "+treeId+" treeNode: "+ruleKey+"matterValue: "+matterValue);
}
return treeNodeInfo;
}

}

这里主要提供决策树流程的处理过程,有点像通过链路的关系(性别,年龄)在二叉树中寻找果实节点的过程。同时提供一个抽象方法,执行决策流程的方法,供外部做具体的实现。

决策引擎的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.bestrookie.design.domain.service.engine.impl;
import com.bestrookie.design.domain.model.aggregates.TreeRich;
import com.bestrookie.design.domain.model.vo.EngineResult;
import com.bestrookie.design.domain.model.vo.TreeNode;
import com.bestrookie.design.domain.service.engine.EngineBase;
import java.util.Map;
/**
* @author bestrookie
* @date 2021/11/9 4:09 下午
*/
public class TreeEngineHandle extends EngineBase {
@Override
public EngineResult process(Long treeId, String userId, TreeRich treeRich, Map<String, String> decisionMatter) {
TreeNode treeNode = engineDecisionMaker(treeRich, treeId, userId, decisionMatter);
return new EngineResult(userId,treeId,treeNode.getTreeNodeId(),treeNode.getNodeValue());
}
}

这里决策引擎的实现就非常简单了,通过传递进来的必要信息——-决策树信息、决策物料值,做具体的树形结构决策

测试
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
156
157
158
159
160
161
162
163
164
package com.bestrookie.design;
import com.alibaba.fastjson.JSON;
import com.bestrookie.design.domain.model.aggregates.TreeRich;
import com.bestrookie.design.domain.model.vo.EngineResult;
import com.bestrookie.design.domain.model.vo.TreeNode;
import com.bestrookie.design.domain.model.vo.TreeNodeLink;
import com.bestrookie.design.domain.model.vo.TreeRoot;
import com.bestrookie.design.domain.service.engine.IEngine;
import com.bestrookie.design.domain.service.engine.impl.TreeEngineHandle;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author bestrookie
* @date 2021/11/9 9:36 上午
*/
public class Test {
public static void main(String[] args) {
IEngine treeEngineHandle = new TreeEngineHandle();
TreeRich treeRich = init();
Map<String, String> decisionMatter = new HashMap<String, String>();
decisionMatter.put("gender","man");
decisionMatter.put("age","29");
EngineResult result = treeEngineHandle.process(10001L, "Oli09pLkdjh", treeRich, decisionMatter);
System.out.println(JSON.toJSONString(result));
}
public static TreeRich init() {
TreeRich treeRich;
// 节点:1
TreeNode treeNode_01 = new TreeNode();
treeNode_01.setTreeId(10001L);
treeNode_01.setTreeNodeId(1L);
treeNode_01.setNodeType(1);
treeNode_01.setNodeValue(null);
treeNode_01.setRuleKey("userGender");
treeNode_01.setRuleDesc("用户性别[男/女]");

// 链接:1->11
TreeNodeLink treeNodeLink_11 = new TreeNodeLink();
treeNodeLink_11.setNodeIdFrom(1L);
treeNodeLink_11.setNodeIdTo(11L);
treeNodeLink_11.setRuleLimitType(1);
treeNodeLink_11.setRuleLimitValue("man");

// 链接:1->12
TreeNodeLink treeNodeLink_12 = new TreeNodeLink();
treeNodeLink_12.setNodeIdTo(1L);
treeNodeLink_12.setNodeIdTo(12L);
treeNodeLink_12.setRuleLimitType(1);
treeNodeLink_12.setRuleLimitValue("woman");

List<TreeNodeLink> treeNodeLinkList_1 = new ArrayList<TreeNodeLink>();
treeNodeLinkList_1.add(treeNodeLink_11);
treeNodeLinkList_1.add(treeNodeLink_12);

treeNode_01.setTreeNodeLinkList(treeNodeLinkList_1);

// 节点:11
TreeNode treeNode_11 = new TreeNode();
treeNode_11.setTreeId(10001L);
treeNode_11.setTreeNodeId(11L);
treeNode_11.setNodeType(1);
treeNode_11.setNodeValue(null);
treeNode_11.setRuleKey("userAge");
treeNode_11.setRuleDesc("用户年龄");

// 链接:11->111
TreeNodeLink treeNodeLink_111 = new TreeNodeLink();
treeNodeLink_111.setNodeIdFrom(11L);
treeNodeLink_111.setNodeIdTo(111L);
treeNodeLink_111.setRuleLimitType(3);
treeNodeLink_111.setRuleLimitValue("25");

// 链接:11->112
TreeNodeLink treeNodeLink_112 = new TreeNodeLink();
treeNodeLink_112.setNodeIdFrom(11L);
treeNodeLink_112.setNodeIdTo(112L);
treeNodeLink_112.setRuleLimitType(5);
treeNodeLink_112.setRuleLimitValue("25");

List<TreeNodeLink> treeNodeLinkList_11 = new ArrayList<TreeNodeLink>();
treeNodeLinkList_11.add(treeNodeLink_111);
treeNodeLinkList_11.add(treeNodeLink_112);

treeNode_11.setTreeNodeLinkList(treeNodeLinkList_11);

// 节点:12
TreeNode treeNode_12 = new TreeNode();
treeNode_12.setTreeId(10001L);
treeNode_12.setTreeNodeId(12L);
treeNode_12.setNodeType(1);
treeNode_12.setNodeValue(null);
treeNode_12.setRuleKey("userAge");
treeNode_12.setRuleDesc("用户年龄");

// 链接:12->121
TreeNodeLink treeNodeLink_121 = new TreeNodeLink();
treeNodeLink_121.setNodeIdFrom(12L);
treeNodeLink_121.setNodeIdTo(121L);
treeNodeLink_121.setRuleLimitType(3);
treeNodeLink_121.setRuleLimitValue("25");

// 链接:12->122
TreeNodeLink treeNodeLink_122 = new TreeNodeLink();
treeNodeLink_122.setNodeIdFrom(12L);
treeNodeLink_122.setNodeIdTo(122L);
treeNodeLink_122.setRuleLimitType(5);
treeNodeLink_122.setRuleLimitValue("25");

List<TreeNodeLink> treeNodeLinkList_12 = new ArrayList<TreeNodeLink>();
treeNodeLinkList_12.add(treeNodeLink_121);
treeNodeLinkList_12.add(treeNodeLink_122);

treeNode_12.setTreeNodeLinkList(treeNodeLinkList_12);

// 节点:111
TreeNode treeNode_111 = new TreeNode();
treeNode_111.setTreeId(10001L);
treeNode_111.setTreeNodeId(111L);
treeNode_111.setNodeType(2);
treeNode_111.setNodeValue("果实A");

// 节点:112
TreeNode treeNode_112 = new TreeNode();
treeNode_112.setTreeId(10001L);
treeNode_112.setTreeNodeId(112L);
treeNode_112.setNodeType(2);
treeNode_112.setNodeValue("果实B");

// 节点:121
TreeNode treeNode_121 = new TreeNode();
treeNode_121.setTreeId(10001L);
treeNode_121.setTreeNodeId(121L);
treeNode_121.setNodeType(2);
treeNode_121.setNodeValue("果实C");

// 节点:122
TreeNode treeNode_122 = new TreeNode();
treeNode_122.setTreeId(10001L);
treeNode_122.setTreeNodeId(122L);
treeNode_122.setNodeType(2);
treeNode_122.setNodeValue("果实D");

// 树根
TreeRoot treeRoot = new TreeRoot();
treeRoot.setTreeId(10001L);
treeRoot.setTreeRootNodeId(1L);
treeRoot.setTreeName("规则决策树");

Map<Long, TreeNode> treeNodeMap = new HashMap<Long, TreeNode>();
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 = new TreeRich(treeRoot, treeNodeMap);
return treeRich;

}
}

总结

从以上的决策树尝尽来看,组合模式主要解决的是在不同结构的组织下,一系列简单逻辑节点或者扩展的复杂逻辑节点对外部的调用仍然可以非常简单。这种设计模式保证了开闭原则,无需更改模型结构就可以提供新的逻辑节点,并配合组织出新的关系树。当然,如果是一些功能差异非常大的接口,则包装起来也会变得比较困难,但也并非不能很好地处理,只不过需要做一些适配和特定的开发

评论