组合模式简单介绍
通过把相似对象或方法组合成一组可被调用的结构树对象的设计思路,成为组合模式。
这种设计方式可以让服务组节点进行自由组合并对外提供服务调用,有些调用方需要使用AB组合,有些调用方需要使用CBA组合,还有一些调用方可能只使用三者中的一个、这时就可以使用组合模式构建服务,对于不同类型的调用方配置不同的组织关系树,而这个树形结构可以配置到数据库中,通过程序图形界面控制树形结构的创建和修改。
简单的场景模拟
如图这是一个简化版的营销规则决策树,根据性别、年龄的不同组合,发展不同类型的优惠券,目的是刺激消费,对精准用户进行促活。
违背设计模式实现
这里举一个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;
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,出了问题难以排查,新加功能的风险较高。
组合模式重构代码
重构代码的改动量相对来说会比较大,为了把不同类型的决策节点和最终的果实组装成一颗可被程序运行的决策树,需要适配设计和工厂方法调用,具体会体现在定义接口和抽象类、初始化配置决策点(性别、年龄)上。
先来看一下我们的工程目录
组合模式代码关系类图
决策树模型
整个类图关系包括了属性结构原子模块实现关系、树形结构执行引擎两部分内容。树形结构原子模块实现关系从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;
@Data @AllArgsConstructor @NoArgsConstructor public class EngineResult { private boolean isSuccess; private String userId; private Long treeId; private Long nodeId; 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;
@Data @AllArgsConstructor @NoArgsConstructor public class TreeRoot { private Long treeId; private Long treeRootNodeId; private String treeName; }
|
TreeNodeLink
树节点链路关系
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;
@Data @AllArgsConstructor @NoArgsConstructor public class TreeNodeLink { private Long nodeIdFrom; private Long nodeIdTo; 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;
@Data @AllArgsConstructor @NoArgsConstructor public class TreeNode { private Long treeId; private Long treeNodeId; private Integer nodeType; private String nodeValue; private String ruleKey; 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;
@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;
public interface LogicFilter {
Long filter(String matterValue, List<TreeNodeLink> treeNodeLinkList);
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;
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;
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;
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;
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;
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;
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(); Long rootNodeId = treeRoot.getTreeRootNodeId(); TreeNode treeNodeInfo = treeNodeMap.get(rootNodeId); 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;
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;
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; 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("用户性别[男/女]");
TreeNodeLink treeNodeLink_11 = new TreeNodeLink(); treeNodeLink_11.setNodeIdFrom(1L); treeNodeLink_11.setNodeIdTo(11L); treeNodeLink_11.setRuleLimitType(1); treeNodeLink_11.setRuleLimitValue("man");
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);
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("用户年龄");
TreeNodeLink treeNodeLink_111 = new TreeNodeLink(); treeNodeLink_111.setNodeIdFrom(11L); treeNodeLink_111.setNodeIdTo(111L); treeNodeLink_111.setRuleLimitType(3); treeNodeLink_111.setRuleLimitValue("25");
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);
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("用户年龄");
TreeNodeLink treeNodeLink_121 = new TreeNodeLink(); treeNodeLink_121.setNodeIdFrom(12L); treeNodeLink_121.setNodeIdTo(121L); treeNodeLink_121.setRuleLimitType(3); treeNodeLink_121.setRuleLimitValue("25");
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);
TreeNode treeNode_111 = new TreeNode(); treeNode_111.setTreeId(10001L); treeNode_111.setTreeNodeId(111L); treeNode_111.setNodeType(2); treeNode_111.setNodeValue("果实A");
TreeNode treeNode_112 = new TreeNode(); treeNode_112.setTreeId(10001L); treeNode_112.setTreeNodeId(112L); treeNode_112.setNodeType(2); treeNode_112.setNodeValue("果实B");
TreeNode treeNode_121 = new TreeNode(); treeNode_121.setTreeId(10001L); treeNode_121.setTreeNodeId(121L); treeNode_121.setNodeType(2); treeNode_121.setNodeValue("果实C");
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;
} }
|
总结
从以上的决策树尝尽来看,组合模式主要解决的是在不同结构的组织下,一系列简单逻辑节点或者扩展的复杂逻辑节点对外部的调用仍然可以非常简单。这种设计模式保证了开闭原则,无需更改模型结构就可以提供新的逻辑节点,并配合组织出新的关系树。当然,如果是一些功能差异非常大的接口,则包装起来也会变得比较困难,但也并非不能很好地处理,只不过需要做一些适配和特定的开发