头几年只要群里一问我该学哪个开发语言,哪个语言最好。群里肯定聊的特别火热,有人支持PHP、有 人喊号Java、也有C++和C#。但这几年开始好像大家并不会真的刀枪棍棒、斧钺钩叉 般讨论了,大多数 时候都是开玩笑的闹一闹。于此同时在整体的互联网开发中很多时候是一些开发语言公用的,共同打造 整体的生态圈。而大家选择的方式也是更偏向于不同领域下选择适合的架构,而不是一味地追求某个语 言。这可以给很多初学编程的新人一些提议,不要刻意的觉得某个语言好,某个语言不好,只是在适合 的场景下选择最需要的。而你要选择的那个语言可以参考招聘网站的需求量和薪资水平决定。
总会有人喜欢在整体的项目开发中用上点新特性,把自己新学的知识实践试试。不能说这样就是不好, 甚至可以说这是一部分很热爱学习的人,喜欢创新,喜欢实践。但编程除了用上新特性外,还需要考虑 整体的扩展性、可读性、可维护、易扩展等方面的考虑。就像你家里雇佣了一伙装修师傅,有那么一个 小工喜欢炫技搞花活,在家的淋浴下安装了马桶 。
往往很多大需求都是通过增删改查堆出来的,今天要一个需求 if 一下,明天加个内容 else 扩展一下。 日积月累需求也就越来越大,扩展和维护的成本也就越来越高。往往大部分研发是不具备产品思维和整 体业务需求导向的,总以为写好代码完成功能即可。但这样的不考虑扩展性的实现,很难让后续的需求 都快速迭代,久而久之就会被陷入恶性循环,每天都有bug要改。

一、开发环境

  1. JDK 1.8
    2. Idea + Maven

    二、组合模式介绍

    image.png
    从上图可以看到这有点像螺丝 ( 和螺母,通过一堆的链接组织出一棵结构树。而这种通过把相似对象 ( 也可以称作是方法 )组合成一组可被调用的结构树对象的设计思路叫做组合模式。
    这种设计方式可以让你的服务组节点进行自由组合对外提供服务,例如你有三个原子校验功能( A:身份 证 、 B:银行卡 、 C:手机号 )服务并对外提供调用使用。有些调用方需要使用AB组合,有些调用方需要 使用到CBA组合,还有一些可能只使用三者中的一个。那么这个时候你就可以使用组合模式进行构建服 务,对于不同类型的调用方配置不同的组织关系树,而这个树结构你可以配置到数据库中也可以不断的 通过图形界面来控制树结构。
    所以不同的设计模式用在恰当好处的场景可以让代码逻辑非常清晰并易于扩展,同时也可以减少团队新 增人员对项目的学习成本。

    三、案例场景模拟

    image.png
    以上是一个非常简化版的营销规则决策树 ,根据性别 、年龄来发放不同类型的优惠券,来刺激消费起 到精准用户促活的目的。
    虽然一部分小伙伴可能并没有开发过营销场景,但你可能时时刻刻的被营销着。比如你去经常浏览男性 喜欢的机械键盘、笔记本电脑、汽车装饰等等,那么就给你推荐此类的优惠券刺激你消费。那么如果你 购物不多,或者钱不在自己手里。那么你是否打过车,有一段时间经常有小伙伴喊,为什么同样的距离 他就10元,我就15元呢?其实这些都是被营销的案例,一般对于不常使用软件的小伙伴,经常会进行稍 微大力度的促活,增加用户粘性。
    那么在这里我们就模拟一个类似的决策场景,体现出组合模式在其中起到的重要性。另外,组合模式不 只是可以运用于规则决策树,还可以做服务包装将不同的接口进行组合配置,对外提供服务能力,减少 开发成本。

    四、用一坨坨代码实现

    这里我们举一个关于 ifelse 诞生的例子,介绍小姐姐与程序员之间的故事导致的事故
    image.png

    1. 工程结构

    image.png
    公司里要都是这样的程序员绝对省下不少成本,根本不要搭建微服务,一个工程搞定所有业务!
    但千万不要这么干! 酒肉穿肠过,佛祖心中留。世人若学我,如同进魔道。

    2. 代码实现

    ```java public class EngineController {

    private Logger logger = LoggerFactory.getLogger(EngineController.class);

    public String process(final String userId, final String userSex, final int userAge) {

    1. logger.info("ifelse实现方式判断用户结果。userId:{} userSex:{} userAge:{}", userId, userSex, userAge);
    2. if ("man".equals(userSex)) {
    3. if (userAge < 25) {
    4. return "果实A";
    5. }
    6. if (userAge >= 25) {
    7. return "果实B";
    8. }
    9. }
    10. if ("woman".equals(userSex)) {
    11. if (userAge < 25) {
    12. return "果实C";
    13. }
    14. if (userAge >= 25) {
    15. return "果实D";
    16. }
    17. }
    18. return null;

    }

}

  1. 除了我们说的扩展性和每次的维护以外,这样的代码实现起来是最快的。而且从样子来看也很适合
  2. 新人理解。
  3. <br />但是我劝你别写 ,写这样代码不是被扣绩效就是被开除。
  4. <a name="furM8"></a>
  5. ## 3. 测试验证
  6. <a name="lc9YF"></a>
  7. ### 3.1 编写测试类
  8. ```java
  9. public class ApiTest {
  10. private Logger logger = LoggerFactory.getLogger(ApiTest.class);
  11. @Test
  12. public void test_EngineController() {
  13. EngineController engineController = new EngineController();
  14. String process = engineController.process("Oli09pLkdjh", "man", 29);
  15. logger.info("测试结果:{}", process);
  16. }
  17. }

这里我们模拟了一个用户ID,并传输性别:man、年龄:29,我们的预期结果是:果实B。实际对 应业务就是给 头秃的程序员发一张枸杞优惠券 。

3.2 测试结果

image.png
从测试结果上看我们的程序运行正常并且符合预期,只不过实现上并不是我们推荐的。接下来我们 会采用组合模式来优化这部分代码。

五、组合模式重构代码

接下来使用组合模式来进行代码优化,也算是一次很小的重构。
接下来的重构部分代码改动量相对来说会比较大一些,为了让我们可以把不同类型的决策节点和最终的 果实组装成一棵可被运行的决策树,需要做适配设计和工厂方法调用,具体会体现在定义接口以及抽象 类和初始化配置决策节点( 性别 、年龄 )上。建议这部分代码多阅读几次,最好实践下。

1. 工程结构

image.png
组合模式模型结构
image.png
首先可以看下黑色框的模拟指导树结构; 1 、 11 、 12 、 111 、 112 、 121 、 122 ,这是一组 树结构的ID,并由节点串联组合出一棵关系树。
接下来是类图部分,左侧是从 LogicFilter 开始定义适配的决策过滤器,BaseLogic 是对接口的 实现,提供最基本的通用方法。UserAgeFilter 、UserGenerFilter ,是两个具体的实现类用 于判断年龄和性别 。
最后则是对这颗可以被组织出来的决策树,进行执行的引擎。同样定义了引擎接口和基础的配置, 在配置里面设定了需要的模式决策节点。

2. 代码实现

2.1 基础对象

image.png

2.2 树节点逻辑过滤器接口

  1. public interface LogicFilter {
  2. /**
  3. * 逻辑决策器
  4. *
  5. * @param matterValue 决策值
  6. * @param treeNodeLineInfoList 决策节点
  7. * @return 下一个节点Id
  8. */
  9. Long filter(String matterValue, List<TreeNodeLink> treeNodeLineInfoList);
  10. /**
  11. * 获取决策值
  12. *
  13. * @param decisionMatter 决策物料
  14. * @return 决策值
  15. */
  16. String matterValue(Long treeId, String userId, Map<String, String> decisionMatter);
  17. }

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

2.3 决策抽象类提供基础服务

  1. public abstract class BaseLogic implements LogicFilter {
  2. @Override
  3. public Long filter(String matterValue, List<TreeNodeLink> treeNodeLinkList) {
  4. for (TreeNodeLink nodeLine : treeNodeLinkList) {
  5. if (decisionLogic(matterValue, nodeLine)) return nodeLine.getNodeIdTo();
  6. }
  7. return 0L;
  8. }
  9. @Override
  10. public abstract String matterValue(Long treeId, String userId, Map<String, String> decisionMatter);
  11. private boolean decisionLogic(String matterValue, TreeNodeLink nodeLink) {
  12. switch (nodeLink.getRuleLimitType()) {
  13. case 1:
  14. return matterValue.equals(nodeLink.getRuleLimitValue());
  15. case 2:
  16. return Double.parseDouble(matterValue) > Double.parseDouble(nodeLink.getRuleLimitValue());
  17. case 3:
  18. return Double.parseDouble(matterValue) < Double.parseDouble(nodeLink.getRuleLimitValue());
  19. case 4:
  20. return Double.parseDouble(matterValue) >= Double.parseDouble(nodeLink.getRuleLimitValue());
  21. case 5:
  22. return Double.parseDouble(matterValue) <= Double.parseDouble(nodeLink.getRuleLimitValue());
  23. default:
  24. return false;
  25. }
  26. }
  27. }

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

2.4 树节点逻辑实现类

年龄节点

  1. public class UserAgeFilter extends BaseLogic {
  2. @Override
  3. public String matterValue(Long treeId, String userId, Map<String, String> decisionMatter) {
  4. return decisionMatter.get("age");
  5. }
  6. }

性别节点

  1. public class UserGenderFilter extends BaseLogic {
  2. @Override
  3. public String matterValue(Long treeId, String userId, Map<String, String> decisionMatter) {
  4. return decisionMatter.get("gender");
  5. }
  6. }

以上两个决策逻辑的节点获取值的方式都非常简单,只是获取用户的入参即可。实际的业务开发可 以从数据库、RPC接口、缓存运算等各种方式获取。

2.5 决策引擎接口定义

  1. public interface IEngine {
  2. EngineResult process(final Long treeId, final String userId, TreeRich treeRich, final Map<String, String> decisionMatter);
  3. }

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

2.6 决策节点配置

  1. public class EngineConfig {
  2. static Map<String, LogicFilter> logicFilterMap;
  3. static {
  4. logicFilterMap = new ConcurrentHashMap<>();
  5. logicFilterMap.put("userAge", new UserAgeFilter());
  6. logicFilterMap.put("userGender", new UserGenderFilter());
  7. }
  8. public Map<String, LogicFilter> getLogicFilterMap() {
  9. return logicFilterMap;
  10. }
  11. public void setLogicFilterMap(Map<String, LogicFilter> logicFilterMap) {
  12. this.logicFilterMap = logicFilterMap;
  13. }
  14. }

在这里将可提供服务的决策节点配置到map结构中,对于这样的map结构可以抽取到数据库中, 那么就可以非常方便的管理。

2.7 基础决策引擎功能

  1. public abstract class EngineBase extends EngineConfig implements IEngine {
  2. private Logger logger = LoggerFactory.getLogger(EngineBase.class);
  3. @Override
  4. public abstract EngineResult process(Long treeId, String userId, TreeRich treeRich, Map<String, String> decisionMatter);
  5. protected TreeNode engineDecisionMaker(TreeRich treeRich, Long treeId, String userId, Map<String, String> decisionMatter) {
  6. TreeRoot treeRoot = treeRich.getTreeRoot();
  7. Map<Long, TreeNode> treeNodeMap = treeRich.getTreeNodeMap();
  8. // 规则树根ID
  9. Long rootNodeId = treeRoot.getTreeRootNodeId();
  10. TreeNode treeNodeInfo = treeNodeMap.get(rootNodeId);
  11. //节点类型[NodeType];1子叶、2果实
  12. while (treeNodeInfo.getNodeType().equals(1)) {
  13. String ruleKey = treeNodeInfo.getRuleKey();
  14. LogicFilter logicFilter = logicFilterMap.get(ruleKey);
  15. String matterValue = logicFilter.matterValue(treeId, userId, decisionMatter);
  16. Long nextNode = logicFilter.filter(matterValue, treeNodeInfo.getTreeNodeLinkList());
  17. treeNodeInfo = treeNodeMap.get(nextNode);
  18. logger.info("决策树引擎=>{} userId:{} treeId:{} treeNode:{} ruleKey:{} matterValue:{}", treeRoot.getTreeName(), userId, treeId, treeNodeInfo.getTreeNodeId(), ruleKey, matterValue);
  19. }
  20. return treeNodeInfo;
  21. }
  22. }

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

2.8 决策引擎的实现

  1. public class TreeEngineHandle extends EngineBase {
  2. @Override
  3. public EngineResult process(Long treeId, String userId, TreeRich treeRich, Map<String, String> decisionMatter) {
  4. // 决策流程
  5. TreeNode treeNode = engineDecisionMaker(treeRich, treeId, userId, decisionMatter);
  6. // 决策结果
  7. return new EngineResult(userId, treeId, treeNode.getTreeNodeId(), treeNode.getNodeValue());
  8. }
  9. }

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

3. 测试验证

3.1 组装树关系

  1. @Before
  2. public void init() {
  3. // 节点:1
  4. TreeNode treeNode_01 = new TreeNode();
  5. treeNode_01.setTreeId(10001L);
  6. treeNode_01.setTreeNodeId(1L);
  7. treeNode_01.setNodeType(1);
  8. treeNode_01.setNodeValue(null);
  9. treeNode_01.setRuleKey("userGender");
  10. treeNode_01.setRuleDesc("用户性别[男/女]");
  11. // 链接:1->11
  12. TreeNodeLink treeNodeLink_11 = new TreeNodeLink();
  13. treeNodeLink_11.setNodeIdFrom(1L);
  14. treeNodeLink_11.setNodeIdTo(11L);
  15. treeNodeLink_11.setRuleLimitType(1);
  16. treeNodeLink_11.setRuleLimitValue("man");
  17. // 链接:1->12
  18. TreeNodeLink treeNodeLink_12 = new TreeNodeLink();
  19. treeNodeLink_12.setNodeIdFrom(1L);
  20. treeNodeLink_12.setNodeIdTo(12L);
  21. treeNodeLink_12.setRuleLimitType(1);
  22. treeNodeLink_12.setRuleLimitValue("woman");
  23. List<TreeNodeLink> treeNodeLinkList_1 = new ArrayList<>();
  24. treeNodeLinkList_1.add(treeNodeLink_11);
  25. treeNodeLinkList_1.add(treeNodeLink_12);
  26. treeNode_01.setTreeNodeLinkList(treeNodeLinkList_1);
  27. // 节点:11
  28. TreeNode treeNode_11 = new TreeNode();
  29. treeNode_11.setTreeId(10001L);
  30. treeNode_11.setTreeNodeId(11L);
  31. treeNode_11.setNodeType(1);
  32. treeNode_11.setNodeValue(null);
  33. treeNode_11.setRuleKey("userAge");
  34. treeNode_11.setRuleDesc("用户年龄");
  35. // 链接:11->111
  36. TreeNodeLink treeNodeLink_111 = new TreeNodeLink();
  37. treeNodeLink_111.setNodeIdFrom(11L);
  38. treeNodeLink_111.setNodeIdTo(111L);
  39. treeNodeLink_111.setRuleLimitType(3);
  40. treeNodeLink_111.setRuleLimitValue("25");
  41. // 链接:11->112
  42. TreeNodeLink treeNodeLink_112 = new TreeNodeLink();
  43. treeNodeLink_112.setNodeIdFrom(11L);
  44. treeNodeLink_112.setNodeIdTo(112L);
  45. treeNodeLink_112.setRuleLimitType(4);
  46. treeNodeLink_112.setRuleLimitValue("25");
  47. List<TreeNodeLink> treeNodeLinkList_11 = new ArrayList<>();
  48. treeNodeLinkList_11.add(treeNodeLink_111);
  49. treeNodeLinkList_11.add(treeNodeLink_112);
  50. treeNode_11.setTreeNodeLinkList(treeNodeLinkList_11);
  51. // 节点:12
  52. TreeNode treeNode_12 = new TreeNode();
  53. treeNode_12.setTreeId(10001L);
  54. treeNode_12.setTreeNodeId(12L);
  55. treeNode_12.setNodeType(1);
  56. treeNode_12.setNodeValue(null);
  57. treeNode_12.setRuleKey("userAge");
  58. treeNode_12.setRuleDesc("用户年龄");
  59. // 链接:12->121
  60. TreeNodeLink treeNodeLink_121 = new TreeNodeLink();
  61. treeNodeLink_121.setNodeIdFrom(12L);
  62. treeNodeLink_121.setNodeIdTo(121L);
  63. treeNodeLink_121.setRuleLimitType(3);
  64. treeNodeLink_121.setRuleLimitValue("25");
  65. // 链接:12->122
  66. TreeNodeLink treeNodeLink_122 = new TreeNodeLink();
  67. treeNodeLink_122.setNodeIdFrom(12L);
  68. treeNodeLink_122.setNodeIdTo(122L);
  69. treeNodeLink_122.setRuleLimitType(4);
  70. treeNodeLink_122.setRuleLimitValue("25");
  71. List<TreeNodeLink> treeNodeLinkList_12 = new ArrayList<>();
  72. treeNodeLinkList_12.add(treeNodeLink_121);
  73. treeNodeLinkList_12.add(treeNodeLink_122);
  74. treeNode_12.setTreeNodeLinkList(treeNodeLinkList_12);
  75. // 节点:111
  76. TreeNode treeNode_111 = new TreeNode();
  77. treeNode_111.setTreeId(10001L);
  78. treeNode_111.setTreeNodeId(111L);
  79. treeNode_111.setNodeType(2);
  80. treeNode_111.setNodeValue("果实A");
  81. // 节点:112
  82. TreeNode treeNode_112 = new TreeNode();
  83. treeNode_112.setTreeId(10001L);
  84. treeNode_112.setTreeNodeId(112L);
  85. treeNode_112.setNodeType(2);
  86. treeNode_112.setNodeValue("果实B");
  87. // 节点:121
  88. TreeNode treeNode_121 = new TreeNode();
  89. treeNode_121.setTreeId(10001L);
  90. treeNode_121.setTreeNodeId(121L);
  91. treeNode_121.setNodeType(2);
  92. treeNode_121.setNodeValue("果实C");
  93. // 节点:122
  94. TreeNode treeNode_122 = new TreeNode();
  95. treeNode_122.setTreeId(10001L);
  96. treeNode_122.setTreeNodeId(122L);
  97. treeNode_122.setNodeType(2);
  98. treeNode_122.setNodeValue("果实D");
  99. // 树根
  100. TreeRoot treeRoot = new TreeRoot();
  101. treeRoot.setTreeId(10001L);
  102. treeRoot.setTreeRootNodeId(1L);
  103. treeRoot.setTreeName("规则决策树");
  104. Map<Long, TreeNode> treeNodeMap = new HashMap<>();
  105. treeNodeMap.put(1L, treeNode_01);
  106. treeNodeMap.put(11L, treeNode_11);
  107. treeNodeMap.put(12L, treeNode_12);
  108. treeNodeMap.put(111L, treeNode_111);
  109. treeNodeMap.put(112L, treeNode_112);
  110. treeNodeMap.put(121L, treeNode_121);
  111. treeNodeMap.put(122L, treeNode_122);
  112. treeRich = new TreeRich(treeRoot, treeNodeMap);
  113. }

image.png
重要,这一部分是组合模式非常重要的使用,在我们已经建造好的决策树关系下,可以创建出树的 各个节点,以及对节点间使用链路进行串联。
以及后续你需要做任何业务的扩展都可以在里面添加相应的节点,并做动态化的配置。
关于这部分手动组合的方式可以提取到数据库中,那么也就可以扩展到图形界面的进行配置操作。

3.2 编写测试类

  1. @Test
  2. public void test_tree() {
  3. logger.info("决策树组合结构信息:\r\n" + JSON.toJSONString(treeRich));
  4. IEngine treeEngineHandle = new TreeEngineHandle();
  5. Map<String, String> decisionMatter = new HashMap<>();
  6. decisionMatter.put("gender", "man");
  7. decisionMatter.put("age", "29");
  8. EngineResult result = treeEngineHandle.process(10001L, "Oli09pLkdjh", treeRich, decisionMatter);
  9. logger.info("测试结果:{}", JSON.toJSONString(result));
  10. }

在这里提供了调用的通过组织模式创建出来的流程决策树,调用的时候传入了决策树的ID,那么如 果是业务开发中就可以方便的解耦决策树与业务的绑定关系,按需传入决策树ID即可。
此外入参我们还提供了需要处理; 男 (man)、 年年龄 (29岁),的参数信息。

3.3 测试结果

image.png
从测试结果上看这与我们使用 ifelse 是一样的,但是目前这与的组合模式设计下,就非常方便后 续的拓拓展和改。

六、总结

从以上的决策树场景来看,组合模式的主要解决的是一系列简单逻辑节点或者扩展的复杂逻辑节点 在不同结构的组织下,对于外部的调用是仍然可以非常简单的。
这部分设计模式保证了开闭原则,无需更改模型结构你就可以提供新的逻辑节点的使用并配合组织 出新的关系树。但如果是一些功能差异化非常大的接口进行包装就会变得比较困难,但也不是不能 很好的处理,只不过需要做一些适配和特定化的开发。
很多时候因为你的极致追求和稍有倔强的工匠精神,即使在面对同样的业务需求,你能完成出最好 的代码结构和最易于扩展的技术架构。 不要被远不能给你指导提升能力的影响到放弃自己的追求!