13

重学 Java 设计模式:实战组合模式(营销差异化人群发券,决策树引擎搭建场景)

 3 years ago
source link: https://segmentfault.com/a/1190000022866607
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

ryqmUfi.png!web

作者:小傅哥

博客: https://bugstack.cn

沉淀、分享、成长,让自己和他人都能有所收获!:smile:

一、前言

小朋友才做选择题,成年人我都要

头几年只要群里一问我该学哪个开发语言,哪个语言最好。群里肯定聊的特别火热,有人支持PHP、有人喊号Java、也有C++和C#。但这几年开始好像大家并不会真的 刀枪棍棒、斧钺钩叉 般讨论了,大多数时候都是开玩笑的闹一闹。于此同时在整体的互联网开发中很多时候是一些开发语言公用的,共同打造整体的生态圈。而大家选择的方式也是更偏向于不同领域下选择适合的架构,而不是一味地追求某个语言。这可以给很多初学编程的新人一些提议,不要刻意的觉得某个语言好,某个语言不好,只是在适合的场景下选择最需要的。而你要选择的那个语言可以参考招聘网站的需求量和薪资水平决定。

编程开发不是炫技

总会有人喜欢在整体的项目开发中用上点新特性,把自己新学的知识实践试试。不能说这样就是不好,甚至可以说这是一部分很热爱学习的人,喜欢创新,喜欢实践。但编程除了用上新特性外,还需要考虑整体的扩展性、可读性、可维护、易扩展等方面的考虑。就像你家里雇佣了一伙装修师傅,有那么一个小工喜欢炫技搞花活,在家的淋浴下:shower:安装了马桶:toilet:。

即使是写CRUD也应该有设计模式

往往很多大需求都是通过增删改查堆出来的,今天要一个需求 if 一下,明天加个内容 else 扩展一下。日积月累需求也就越来越大,扩展和维护的成本也就越来越高。往往大部分研发是不具备产品思维和整体业务需求导向的,总以为写好代码完成功能即可。但这样的不考虑扩展性的实现,很难让后续的需求都快速迭代,久而久之就会被陷入恶性循环,每天都有bug要改。

二、开发环境

  1. JDK 1.8
  2. Idea + Maven
  3. 涉及工程三个,可以通过关注 公众号 bugstack虫洞栈 ,回复 源码下载 获取(打开获取的链接,找到序号18)
工程 描述 itstack-demo-design-8-01 使用一坨代码实现业务需求 itstack-demo-design-8-02 通过设计模式优化改造代码,产生对比性从而学习

三、组合模式介绍

MJRBNjb.png!web

从上图可以看到这有点像螺丝:nut_and_bolt:和螺母,通过一堆的链接组织出一棵结构树。而这种通过把相似对象( 也可以称作是方法 )组合成一组可被调用的结构树对象的设计思路叫做组合模式。

这种设计方式可以让你的服务组节点进行自由组合对外提供服务,例如你有三个原子校验功能( A:身份证B:银行卡C:手机号 )服务并对外提供调用使用。有些调用方需要使用AB组合,有些调用方需要使用到CBA组合,还有一些可能只使用三者中的一个。那么这个时候你就可以使用组合模式进行构建服务,对于不同类型的调用方配置不同的组织关系树,而这个树结构你可以配置到数据库中也可以不断的通过图形界面来控制树结构。

所以不同的设计模式用在恰当好处的场景可以让代码逻辑非常清晰并易于扩展,同时也可以减少团队新增人员对项目的学习成本。

四、案例场景模拟

NNfYzaa.png!web

以上是一个非常简化版的营销规则 决策树 ,根据 性别年龄 来发放不同类型的优惠券,来刺激消费起到精准用户促活的目的。

虽然一部分小伙伴可能并没有开发过营销场景,但你可能时时刻刻的被营销着。比如你去经常浏览男性喜欢的机械键盘、笔记本电脑、汽车装饰等等,那么久给你推荐此类的优惠券刺激你消费。那么如果你购物不多,或者钱不在自己手里。那么你是否打过车,有一段时间经常有小伙伴喊,为什么同样的距离他就10元,我就15元呢?其实这些都是被营销的案例,一般对于不常使用软件的小伙伴,经常会进行稍微大力度的促活,增加用户粘性。

那么在这里我们就模拟一个类似的决策场景,体现出组合模式在其中起到的重要性。另外,组合模式不只是可以运用于规则决策树,还可以做服务包装将不同的接口进行组合配置,对外提供服务能力,减少开发成本。

五、用一坨坨代码实现

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

日期 需求 紧急程度 程序员(话外音) 星期一.早上 猿哥哥,老板说要搞一下营销拉拉量,给男生女生发不同的优惠券,促活消费。 很紧急,下班就要 行吧,也不难,加下判断就上线 星期二.下午 小哥哥,咱们上线后非常好。要让咱们按照年轻、中年、成年,不同年龄加下判断,准确刺激消费。 超紧急,明天就要 也不难,加就加吧 星期三.晚上 喂,小哥哥!睡了吗!老板说咱们这次活动很成功,可以不可以在细分下,把单身、结婚、有娃的都加上不同判断。这样更能刺激用户消费。 贼紧急,最快上线。 已经意识到 ifelse 越来越多了 星期四.凌晨 哇!小哥哥你们太棒了,上的真快。嘻嘻!有个小请求,需要调整下年龄段,因为现在学生处对象的都比较早,有对象的更容易买某某某东西。要改下值!辛苦辛苦! 老板,在等着呢! 一大片的值要修改,哎!这么多 ifelse 了 星期五.半夜 歪歪喂!巴巴,坏了,怎么发的优惠券不对了,有客诉了,很多女生都来投诉。你快看看。老板,他... (一头汗),哎,值粘错位置了! 终究还是一个人扛下了所有

1. 工程结构

itstack-demo-design-8-01
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                └── EngineController.java
酒肉穿肠过,佛祖心中留。世人若学我,如同进魔道。

2. 代码实现

public class EngineController {

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

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

        logger.info("ifelse实现方式判断用户结果。userId:{} userSex:{} userAge:{}", userId, userSex, userAge);

        if ("man".equals(userSex)) {
            if (userAge < 25) {
                return "果实A";
            }

            if (userAge >= 25) {
                return "果实B";
            }
        }

        if ("woman".equals(userSex)) {
            if (userAge < 25) {
                return "果实C";
            }

            if (userAge >= 25) {
                return "果实D";
            }
        }

        return null;

    }

}
我劝你别写

3. 测试验证

3.1 编写测试类

@Test
public void test_EngineController() {
    EngineController engineController = new EngineController();
    String process = engineController.process("Oli09pLkdjh", "man", 29);
    logger.info("测试结果:{}", process);
}
  • 这里我们模拟了一个用户ID,并传输性别:man、年龄:29,我们的预期结果是:果实B。实际对应业务就是给 头秃的程序员发一张枸杞优惠券

3.2 测试结果

22:10:12.891 [main] INFO  o.i.demo.design.EngineController - ifelse实现方式判断用户结果。userId:Oli09pLkdjh userSex:man userAge:29
22:10:12.898 [main] INFO  org.itstack.demo.design.test.ApiTest - 测试结果:果实B

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

六、组合模式重构代码

接下来使用组合模式来进行代码优化,也算是一次很小的重构。

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

1. 工程结构

itstack-demo-design-8-02
└── src
    ├── main
    │   └── java
    │      └── org.itstack.demo.design.domain
    │          ├── model
    │          │   ├── aggregates
    │          │   │   └── TreeRich.java
    │          │   └── vo
    │          │       ├── EngineResult.java
    │          │       ├── TreeNode.java
    │          │       ├── TreeNodeLink.java    
    │          │       └── TreeRoot.java    
    │          └── service
    │              ├── engine
    │              │   ├── impl    
    │              │   │   └── TreeEngineHandle.java       
    │              │   ├── EngineBase.java 
    │              │   ├── EngineConfig.java       
    │              │   └── IEngine.java    
    │              └── logic
    │                  ├── impl    
    │                  │   ├── LogicFilter.java     
    │                  │   └── LogicFilter.java        
    │                  └── LogicFilter.java    
    └── test
         └── java
             └── org.itstack.demo.design.test
                 └── ApiTest.java

组合模式模型结构

aq2IVbJ.png!web

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

    • static {
           logicFilterMap = new ConcurrentHashMap<>();
           logicFilterMap.put("userAge", new UserAgeFilter());
           logicFilterMap.put("userGender", new UserGenderFilter());
      }
  • 接下来会对每一个类进行细致的讲解,如果感觉没有读懂一定是我作者的表述不够清晰,可以添加我的微信(fustack)与我交流。

2. 代码实现

2.1 基础对象

包路径 类 介绍 model.aggregates TreeRich 聚合对象,包含组织树信息 model.vo EngineResult 决策返回对象信息 model.vo TreeNode 树节点;子叶节点、果实节点 model.vo TreeNodeLink 树节点链接链路 model.vo TreeRoot 树根信息
  • 以上这部分简单介绍,不包含逻辑只是各项必要属性的 get/set ,整个源代码可以通过关注微信公众号: bugstack虫洞栈 ,回复源码下载打开链接获取。

2.2 树节点逻辑过滤器接口

public interface LogicFilter {

    /**
     * 逻辑决策器
     *
     * @param matterValue          决策值
     * @param treeNodeLineInfoList 决策节点
     * @return 下一个节点Id
     */
    Long filter(String matterValue, List<TreeNodeLink> treeNodeLineInfoList);

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

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

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

public abstract class BaseLogic implements LogicFilter {

    @Override
    public Long filter(String matterValue, List<TreeNodeLink> treeNodeLinkList) {
        for (TreeNodeLink nodeLine : treeNodeLinkList) {
            if (decisionLogic(matterValue, nodeLine)) return nodeLine.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等于、小于、大于、小于等于、大于等于 的判断逻辑。
  • 同时定义了抽象方法,让每一个实现接口的类都必须按照规则提供 决策值 ,这个决策值用于做逻辑比对。

2.4 树节点逻辑实现类

年龄节点

public class UserAgeFilter extends BaseLogic {

    @Override
    public String matterValue(Long treeId, String userId, Map<String, String> decisionMatter) {
        return decisionMatter.get("age");
    }

}

性别节点

public class UserGenderFilter extends BaseLogic {

    @Override
    public String matterValue(Long treeId, String userId, Map<String, String> decisionMatter) {
        return decisionMatter.get("gender");
    }

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

2.5 决策引擎接口定义

public interface IEngine {

    EngineResult process(final Long treeId, final String userId, TreeRich treeRich, final Map<String, String> decisionMatter);

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

2.6 决策节点配置

public class EngineConfig {

    static Map<String, LogicFilter> logicFilterMap;

    static {
        logicFilterMap = new ConcurrentHashMap<>();
        logicFilterMap.put("userAge", new UserAgeFilter());
        logicFilterMap.put("userGender", new UserGenderFilter());
    }

    public Map<String, LogicFilter> getLogicFilterMap() {
        return logicFilterMap;
    }

    public void setLogicFilterMap(Map<String, LogicFilter> logicFilterMap) {
        this.logicFilterMap = logicFilterMap;
    }

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

2.7 基础决策引擎功能

public abstract class EngineBase extends EngineConfig implements IEngine {

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

    @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);
            logger.info("决策树引擎=>{} userId:{} treeId:{} treeNode:{} ruleKey:{} matterValue:{}", treeRoot.getTreeName(), userId, treeId, treeNodeInfo.getTreeNodeId(), ruleKey, matterValue);
        }
        return treeNodeInfo;
    }

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

2.8 决策引擎的实现

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());
    }

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

3. 测试验证

3.1 组装树关系

@Before
public void init() {
    // 节点: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<>();
    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<>();
    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<>();
    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<>();
    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);
}

yqYf6zZ.png!web

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

3.2 编写测试类

@Test
public void test_tree() {
    logger.info("决策树组合结构信息:\r\n" + JSON.toJSONString(treeRich));
    
    IEngine treeEngineHandle = new TreeEngineHandle();
    Map<String, String> decisionMatter = new HashMap<>();
    decisionMatter.put("gender", "man");
    decisionMatter.put("age", "29");
    
    EngineResult result = treeEngineHandle.process(10001L, "Oli09pLkdjh", treeRich, decisionMatter);
    
    logger.info("测试结果:{}", JSON.toJSONString(result));
}
  • 在这里提供了调用的通过组织模式创建出来的流程决策树,调用的时候传入了决策树的ID,那么如果是业务开发中就可以方便的解耦决策树与业务的绑定关系,按需传入决策树ID即可。
  • 此外入参我们还提供了需要处理; (man)、 年龄 (29岁),的参数信息。

3.3 测试结果

23:35:05.711 [main] INFO  o.i.d.d.d.service.engine.EngineBase - 决策树引擎=>规则决策树 userId:Oli09pLkdjh treeId:10001 treeNode:11 ruleKey:userGender matterValue:man
23:35:05.712 [main] INFO  o.i.d.d.d.service.engine.EngineBase - 决策树引擎=>规则决策树 userId:Oli09pLkdjh treeId:10001 treeNode:112 ruleKey:userAge matterValue:29
23:35:05.715 [main] INFO  org.itstack.demo.design.test.ApiTest - 测试结果:{"nodeId":112,"nodeValue":"果实B","success":true,"treeId":10001,"userId":"Oli09pLkdjh"}

Process finished with exit code 0
ifelse

七、总结

不要被远不能给你指导提升能力的影响到放弃自己的追求!

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK