设计模式 · 组合模式(Composite Pattern)

文章目录

    • 前言
    • 一、核心定义
    • 二、标准体系结构图
    • 三、场景推演
    • 四、实战案例
      • [4.1 需求分析](#4.1 需求分析)
        • [4.1.1 数据结构设计](#4.1.1 数据结构设计)
        • [4.1.2 相关算法设计](#4.1.2 相关算法设计)
      • [4.2 架构图](#4.2 架构图)
        • [4.2.1 面条代码架构图](#4.2.1 面条代码架构图)
        • [4.2.2 组合模式架构图](#4.2.2 组合模式架构图)
      • [4.3 时序图](#4.3 时序图)
        • [4.3.1 面条代码时序图](#4.3.1 面条代码时序图)
        • [4.3.2 组合模式时序图](#4.3.2 组合模式时序图)
      • [4.4 代码分析](#4.4 代码分析)
        • [4.4.1 面条代码(if-else / 硬编码)](#4.4.1 面条代码(if-else / 硬编码))
        • [4.4.2 组合模式代码](#4.4.2 组合模式代码)
    • 总结

前言

随着业务线的发展,系统中的判断逻辑变得越来越错综复杂。

今天产品要求"按性别发不同的优惠券",明天要求"加上年龄段限制",后天又追加了"新老用户身份"。

如果用传统的编程思维,这段代码很快就成为一座由无限嵌套 if-else 构成的"屎山"。

面对这种易变的规则嵌套,有没有一种优雅的设计能够打破僵局?

组合模式(Composite Pattern) 给出了完美的答案。

它通过将独立的业务逻辑抽象为原子节点,并像搭积木一样组合成一棵"决策树",最精妙的是,它让外部调用方可以无差别地处理单个节点和复杂的树形结构

组合模式在实际业务中的经典场景:不同类型的决策节点(身份证校验、银行卡校验、手机号校验)可以自由组合成一棵服务树,不同调用方使用不同的树结构,而无需修改任何节点代码。

本文代码链接:https://github.com/likerhood/CodeDesignWork/tree/main/codedesign7.0-0 和7.0-1


一、核心定义

组合模式(Composite Pattern)是一种用来组织"树形业务结构"的设计模式。

它的核心思想是:把一个个独立的业务节点,按照树的方式组合起来,并且让外部调用方用同一种方式调用单个节点和整棵树。

放到你的业务场景里,可以这样理解:

身份证校验、银行卡校验、手机号校验,都是一个个独立的"原子节点"。它们各自只负责一件事:

  1. 身份证校验节点:判断身份证是否有效
  2. 银行卡校验节点:判断银行卡是否可用
  3. 手机号校验节点:判断手机号是否符合要求

但是不同调用方需要的校验流程不一样。

比如:

  1. 实名认证场景: 身份证校验 -> 银行卡校验 -> 手机号校验 ;
  2. 快捷注册场景: 手机号校验 -> 身份证校验 ;
  3. 绑卡场景: 身份证校验 -> 银行卡校验。

如果不用组合模式,代码里很容易写成大量 if else:

如果是实名认证,执行 A、B、C 如果是快捷注册,执行 C、A 如果是绑卡,执行 A、B

调用方越多,组合越多,代码就越乱。

组合模式要解决的就是这个问题:
节点本身保持稳定,业务变化通过"组合节点"来完成。

也就是说,身份证校验节点不用关心自己被谁调用,也不用关心自己前面或后面是谁。它只负责身份证校验。至于它和银行卡校验、手机号校验怎么组合,由外部的服务树决定。

组合模式最重要的地方不是"把对象放进集合",而是:
叶子节点和组合节点都实现同一个接口,所以调用方不需要区分当前拿到的是一个单独节点,还是一整棵服务树。

调用方只需要:执行根节点。

至于根节点下面挂了多少层、有哪些校验、按什么顺序执行,调用方都不用关心。

所以,组合模式特别适合这种场景:

业务节点相对稳定,但节点之间的组合方式经常变化。

因此常见案例中,身份证校验、银行卡校验、手机号校验这些节点本身是稳定的;真正经常变化的是不同调用方需要不同的校验链路。

组合模式就是把这些节点搭成不同的树,让业务通过"换树结构"来变化,而不是通过"改节点代码"来变化。


二、标准体系结构图

实现(叶节点)
实现(容器节点)
组合持有子节点
<<interface>>
Component
+operation() : void
Leaf
+operation() : void
Composite
-children: List<Component>
+add(c: Component) : void
+remove(c: Component) : void
+operation() : void

角色 本案例对应 说明
Component(组件接口) LogicFilter 定义统一操作接口,叶节点和容器节点都实现它
Leaf(叶节点) UserAgeFilter / UserGenderFilter 具体决策逻辑,树的末端可执行节点
Composite(容器节点) TreeNode(nodeType=1)+ EngineBase 持有子节点引用,递归向下执行
Client(客户端) TreeEngineHandle + 测试类 只操作根节点,不关心树内部结构

三、场景推演

根据前面提到的身份校验的案例,简单展示代码。

身份证校验、银行卡校验、手机号校验,都是一个个独立的"原子节点"。它们各自只负责一件事:

  1. 身份证校验节点:判断身份证是否有效
  2. 银行卡校验节点:判断银行卡是否可用
  3. 手机号校验节点:判断手机号是否符合要求

不同调用方需要的校验流程:

  1. 实名认证场景: 身份证校验 -> 银行卡校验 -> 手机号校验 ;
  2. 快捷注册场景: 手机号校验 -> 身份证校验 ;
  3. 绑卡场景: 身份证校验 -> 银行卡校验。

接下来是代码案例:

  1. 定义统一的"接口"(树枝和树叶的共同标准)

不管是单独的"身份证校验",还是包含一堆校验的"整棵服务树",对外必须长得一样。

Java 复制代码
// 统一的组件接口(Component)
public interface ValidationNode {
    // 所有的节点都必须提供一个统一的校验方法
    boolean validate(String userId); 
}
  1. 编写原子节点(叶子节点 / Leaf)

这些就是你说的"稳定的业务节点",它们只管干好自己的事,完全不知道自己会被放在哪里。

Java 复制代码
// 1. 身份证校验节点
public class IdCardValidationNode implements ValidationNode {
    @Override
    public boolean validate(String userId) {
        System.out.println("-> 正在执行:身份证有效性校验...");
        // 模拟调用公安接口等逻辑
        return true; 
    }
}

// 2. 银行卡校验节点
public class BankCardValidationNode implements ValidationNode {
    @Override
    public boolean validate(String userId) {
        System.out.println("-> 正在执行:银行卡状态校验...");
        // 模拟调用银联接口等逻辑
        return true; 
    }
}

// 3. 手机号校验节点
public class PhoneValidationNode implements ValidationNode {
    @Override
    public boolean validate(String userId) {
        System.out.println("-> 正在执行:手机号风控校验...");
        // 模拟调用运营商接口等逻辑
        return true; 
    }
}

3. 编写组合节点(树枝节点 / Composite)

这是组合模式的灵魂! 这个类既是 ValidationNode(实现了接口),内部又包含了一个 List<ValidationNode>。它负责把叶子节点串起来。

Java 复制代码
import java.util.ArrayList;
import java.util.List;

// 校验流程树(组合节点)
public class ValidationFlow implements ValidationNode {
    
    // 内部持有一组节点,这些节点可以是叶子,也可以是另一棵子树!
    private List<ValidationNode> nodes = new ArrayList<>();

    // 提供添加节点的方法(搭积木)
    public void addNode(ValidationNode node) {
        nodes.add(node);
    }

    // 重写 validate 方法:它的逻辑就是按顺序执行内部所有的节点
    @Override
    public boolean validate(String userId) {
        for (ValidationNode node : nodes) {
            // 如果某个节点校验失败,直接返回 false,阻断流程
            if (!node.validate(userId)) {
                System.out.println("X 校验不通过,流程终止!");
                return false;
            }
        }
        return true;
    }
}
  1. 外部调用方
Java 复制代码
public class Client {
    public static void main(String[] args) {
        String userId = "USER_10086";

        System.out.println("====== 场景一:实名认证 (身份证 -> 银行卡 -> 手机号) ======");
        ValidationFlow realNameFlow = new ValidationFlow();
        realNameFlow.addNode(new IdCardValidationNode());
        realNameFlow.addNode(new BankCardValidationNode());
        realNameFlow.addNode(new PhoneValidationNode());
        
        // 调用方根本不在乎里面有几个步骤,直接调 validate
        realNameFlow.validate(userId);


        System.out.println("\n====== 场景二:快捷注册 (手机号 -> 身份证) ======");
        ValidationFlow quickRegFlow = new ValidationFlow();
        quickRegFlow.addNode(new PhoneValidationNode());
        quickRegFlow.addNode(new IdCardValidationNode());
        
        // 同样,直接调 validate
        quickRegFlow.validate(userId);


        System.out.println("\n====== 场景三:极致套娃 (实名认证树 + 人脸识别) ======");
        // 假设出了个新场景,要求在之前的"实名认证"基础上,再加一个人脸识别。
        // 因为 ValidationFlow 本身也是一个 ValidationNode,我们可以把"树"挂在"树"上!
        
        ValidationFlow superSafeFlow = new ValidationFlow();
        // 把整棵场景一的树塞进去
        superSafeFlow.addNode(realNameFlow); 
        // 假设我们有个新节点叫 FaceRecognitionNode
        // superSafeFlow.addNode(new FaceRecognitionNode()); 
        
        superSafeFlow.validate(userId);
    }
}

组合模式是的优势:

  1. 解耦IdCardValidationNode 里的代码永远不需要修改,即使业务上多出 100 种排列组合。
  2. 多态 :在 ValidationFlowfor (ValidationNode node : nodes) 循环里,引擎根本不知道正在执行的是具体的手机号校验,还是另一个嵌套的子流程,它只知道"这是一个可以 validate() 的东西"。
  3. 彻底消灭 if-else :原本需要写在代码里的流程判断(if 场景==实名...),现在变成了单纯的"组装对象"。在真实的框架中,这种对象的组装通常可以通过读取数据库配置或 XML/YAML 文件来动态完成,从而实现真正的"不发版改逻辑"。

四、实战案例

4.1 需求分析

营销系统需要根据用户属性(性别、年龄)进行差异化发券:

  • 男性 + 年龄 < 25 → 果实A(年轻男性优惠券)
  • 男性 + 年龄 ≥ 25 → 果实B(成熟男性优惠券)
  • 女性 + 年龄 < 25 → 果实C(年轻女性优惠券)
  • 女性 + 年龄 ≥ 25 → 果实D(成熟女性优惠券)

业务会持续迭代:今天按性别,明天加年龄段,后天加婚育状况......每次迭代就往同一个方法里追加 if-else,最终演变成无法维护的"千行大法"。

组合模式将每个判断条件抽象为独立的决策节点 ,通过配置树结构来描述业务规则,新增判断维度只需新增节点类并挂载到树上,原有代码无需改动。

4.1.1 数据结构设计

树的形状与整体架构(视觉直观图)

复制代码
TreeRoot
  treeId = 10001
  treeRootNodeId = 1  ← 入口
 
                    ┌─────────────────────────┐
                    │      节点 1             │
                    │  ruleKey = "userGender" │
                    │  nodeType = 1 (叶子)    │
                    └────────────┬────────────┘
                                 │
              ┌──────────────────┴──────────────────┐
         == "man"                                == "woman"
    (Link: 1→11)                              (Link: 1→12)
              │                                      │
  ┌───────────▼───────────┐          ┌───────────────▼───────────┐
  │      节点 11          │          │          节点 12          │
  │  ruleKey = "userAge"  │          │   ruleKey = "userAge"     │
  │  nodeType = 1 (叶子)  │          │   nodeType = 1 (叶子)     │
  └───────────┬───────────┘          └───────────────┬───────────┘
              │                                      │
       ┌──────┴──────┐                       ┌──────┴──────┐
    < 25           >= 25                  < 25           >= 25
 (Link:11→111)  (Link:11→112)         (Link:12→121)  (Link:12→122)
       │               │                   │               │
  ┌────▼────┐     ┌────▼────┐         ┌────▼────┐     ┌────▼────┐
  │ 节点111 │     │ 节点112 │         │ 节点121 │     │ 节点122 │
  │ 果实A   │     │ 果实B   │         │ 果实C   │     │ 果实D   │
  │type=2   │     │type=2   │         │type=2   │     │type=2   │
  └─────────┘     └─────────┘         └─────────┘     └─────────┘

树并不是像链表一样用对象指针连起来的,而是用一个 Map 拍平存放,节点之间的父子关系靠 TreeNodeLink 的 ID 引用来表达:

复制代码
treeNodeMap (HashMap)
├── key=1   → TreeNode { ruleKey="userGender", treeNodeLinkList=[Link(1→11), Link(1→12)] }
├── key=11  → TreeNode { ruleKey="userAge",    treeNodeLinkList=[Link(11→111), Link(11→112)] }
├── key=12  → TreeNode { ruleKey="userAge",    treeNodeLinkList=[Link(12→121), Link(12→122)] }
├── key=111 → TreeNode { nodeType=2, nodeValue="果实A",  treeNodeLinkList=null }
├── key=112 → TreeNode { nodeType=2, nodeValue="果实B",  treeNodeLinkList=null }
├── key=121 → TreeNode { nodeType=2, nodeValue="果实C",  treeNodeLinkList=null }
└── key=122 → TreeNode { nodeType=2, nodeValue="果实D",  treeNodeLinkList=null }

引擎遍历时,上述两部分通过 TreeRich(整棵树) 对象组合在一起,缺一不可(没有 TreeRoot 就不知道从哪个节点起步;没有 Map 知道下一个ID也找不到对象):

复制代码
TreeRich(整棵树)
│
├── TreeRoot(树的身份 + 入口)
│     treeId         = 10001
│     treeRootNodeId = 1       ← 指向下面 Map 中 key=1 的节点
│     treeName       = "规则决策树"
│
└── Map<Long, TreeNode>(所有节点的查找表)
      │
      ├── key=1  →  TreeNode(判断节点)
      │               nodeType  = 1
      │               ruleKey   = "userGender"
      │               treeNodeLinkList:
      │                 ├── TreeNodeLink { 1→11, type=1, value="man"   }
      │                 └── TreeNodeLink { 1→12, type=1, value="woman" }
      │
      ├── key=11 →  TreeNode(判断节点)
      │               nodeType  = 1
      │               ruleKey   = "userAge"
      │               treeNodeLinkList:
      │                 ├── TreeNodeLink { 11→111, type=3, value="25" }  (< 25)
      │                 └── TreeNodeLink { 11→112, type=5, value="25" }  (>= 25)
      │
      ├── key=12 →  TreeNode(判断节点)
      │               nodeType  = 1
      │               ruleKey   = "userAge"
      │               treeNodeLinkList:
      │                 ├── TreeNodeLink { 12→121, type=3, value="25" }  (< 25)
      │                 └── TreeNodeLink { 12→122, type=5, value="25" }  (>= 25)
      │
      ├── key=111 → TreeNode(结果节点)
      │               nodeType  = 2
      │               nodeValue = "果实A"
      │
      ├── key=112 → TreeNode(结果节点)
      │               nodeType  = 2
      │               nodeValue = "果实B"
      │
      ├── key=121 → TreeNode(结果节点)
      │               nodeType  = 2
      │               nodeValue = "果实C"
      │
      └── key=122 → TreeNode(结果节点)
                      nodeType  = 2
                      nodeValue = "果实D"

TreeRoot --- 树的入口

Java 复制代码
public class TreeRoot {
    private Long treeId;         // 这棵树的ID
    private Long treeRootNodeId; // 入口节点的ID
    private String treeName;     // 树的名字
}

它解决一个核心问题:引擎从哪里开始走?

Map 里面存了所有的节点,如果没有 TreeRoot 指定 treeRootNodeId = 1,引擎就不知道哪个是根节点。同时 treeIdtreeName 是这棵树的"身份证",用于日志记录和业务标识。


TreeNode --- 节点内部结构

Plaintext 复制代码
TreeNode 的所有字段:

┌─────────────────────────────────────────────────────┐
│  treeId          = 10001     ← 属于哪棵树             │
│  treeNodeId      = 11        ← 自己的编号             │
│  nodeType        = 1         ← 类型:1=判断节点       │
│                                      2=结果节点       │
│  ruleKey         = "userAge" ← 判断什么(只有type=1有)│
│  ruleDesc        = "用户年龄" ← 描述(给人看的)      │
│  nodeValue       = null      ← 结果值(只有type=2有) │
│  treeNodeLinkList = [...]    ← 出口列表(只有type=1有)│
└─────────────────────────────────────────────────────┘

一个叶子节点(判断节点)内部的结构实例:

Plaintext 复制代码
TreeNode (节点11)
├── treeNodeId = 11
├── nodeType = 1         ← "我是中间节点,还有子节点"
├── ruleKey = "userAge"  ← "引擎通过这个key找到 UserAgeFilter"
└── treeNodeLinkList
      ├── TreeNodeLink
      │     ├── nodeIdFrom     = 11
      │     ├── nodeIdTo       = 111
      │     ├── ruleLimitType  = 3   ← "<"
      │     └── ruleLimitValue = "25"
      └── TreeNodeLink
            ├── nodeIdFrom     = 11
            ├── nodeIdTo       = 112
            ├── ruleLimitType  = 5   ← ">="
            └── ruleLimitValue = "25"

关键规律:nodeType 决定哪些字段有意义

复制代码
nodeType = 1 (判断节点)          nodeType = 2 (结果节点)
─────────────────────────        ─────────────────────────
ruleKey        ✓ 有值            ruleKey        ✗ 无意义
nodeValue      ✗ null            nodeValue      ✓ "果实A"
treeNodeLinkList ✓ 有子链路      treeNodeLinkList ✗ null / 空

两种节点对比:

复制代码
节点11(判断节点)                 节点111(结果节点)
treeNodeId  = 11                   treeNodeId  = 111
nodeType    = 1                    nodeType    = 2
ruleKey     = "userAge"  ← 有      ruleKey     = null      ← 无
nodeValue   = null       ← 无      nodeValue   = "果实A"   ← 有
links       = [两条链路] ← 有      links       = []        ← 无

TreeNodeLink --- 节点之间的连线

一条 TreeNodeLink 就是树上的一根箭头,它本质上是把一个 if 条件拆成了三个字段来存,回答三个问题:

复制代码
TreeNodeLink {
    nodeIdFrom     = 11    ← 从哪出发
    nodeIdTo       = 111   ← 到哪去
    ruleLimitType  = 3     ← 什么条件下走这条路?(用数字表示运算符)
    ruleLimitValue = "25"  ← 条件的比较值
}

ruleLimitType 对照表:

复制代码
1  →  ==   等于        matterValue == "25"
2  →  >    大于        matterValue >  25
3  →  <    小于        matterValue <  25
4  →  <=   小于等于    matterValue <= 25
5  →  >=   大于等于    matterValue >= 25

逻辑映射与举例:

复制代码
if ( age   <    25  )
      ↑     ↑    ↑
  matterValue  ruleLimitType  ruleLimitValue

节点11(判断年龄)的两条出路对应 if-else:

复制代码
Link A: nodeIdFrom=11, nodeIdTo=111, ruleLimitType=3, ruleLimitValue="25"
        含义:age < 25  → 走向节点111(果实A)

Link B: nodeIdFrom=11, nodeIdTo=112, ruleLimitType=5, ruleLimitValue="25"
        含义:age >= 25 → 走向节点112(果实B)

翻译成 Java 代码逻辑即:

Java 复制代码
if (age < 25)  → 节点111
if (age >= 25) → 节点112
4.1.2 相关算法设计

logicFilterMap
<<interface>>
IEngine
+process(treeId, userId, treeRich, decisionMatter) : EngineResult
<<interface>>
LogicFilter
+filter(matterValue, treeNodeLinkList) : Long
+matterValue(treeId, userId, decisionMatter) : String
EngineConfig
+Map<String,LogicFilter> logicFilterMap
<<abstract>>
EngineBase
+process(treeId, userId, treeRich, decisionMatter) : EngineResult
#engineDecisionMaker(treeRich, treeId, userId, decisionMatter) : TreeNode
<<abstract>>
BaseLogic
+filter(matterValue, treeNodeLinkList) : Long
-decisionLogic(matterValue, nodeLink) : boolean
+matterValue(treeId, userId, decisionMatter) : String
TreeEngineHandler
+process(treeId, userId, treeRich, decisionMatter) : EngineResult
UserGenderFilter
+matterValue(treeId, userId, decisionMatter) : String
UserAgeFilter
+matterValue(treeId, userId, decisionMatter) : String

核心思路:把 if-else 翻译成数据

原始代码里的每一个 if-else 分支,在这里都被拆成了三个对象

复制代码
原来的 if-else:

if (gender == "man") {
    if (age < 25) {
        return "果实A";         这3层判断
    } else {                    被翻译成
        return "果实B";               ↓
    }                          3种数据对象
}

翻译之后:

TreeNode(节点)      → 代表"在这里做一个判断"
TreeNodeLink(连线)  → 代表"满足什么条件,走哪条路"
TreeRoot(树根)      → 代表"从哪里开始判断"

为什么要翻译成数据?

复制代码
if-else 的问题:                 数据化之后:
─────────────────────────────  ──────────────────────────────
逻辑写死在代码里                 逻辑存在数据里(可以存数据库)
改规则 = 改代码 + 重新部署        改规则 = 改一条数据库记录
加新判断层 = 改代码              加新判断层 = 插入新节点数据
不同业务不同规则 = 复制代码      不同业务 = 传不同的 TreeRich

数据结构的精妙之处:用 Map 拍平存储

复制代码
不用对象引用连接(链表思维):
  节点1 → { 子节点引用: [节点11对象, 节点12对象] }
            ↑ 对象直接持有对象,无法持久化到数据库

用 Map + ID 引用(数据库思维):
  Map {
    1   → 节点1对象  { links: [1→11, 1→12] }   ← 存 ID,不存对象
    11  → 节点11对象 { links: [11→111, 11→112] }
    111 → 节点111对象 { nodeValue: "果实A" }
  }
  ↑ 完美对应数据库表结构,加载进来直接能用

四个类的职责一句话总结:

复制代码
TreeNode     → 节点本身(我是谁,我判断什么,我有哪些出口)
TreeNodeLink → 一条带条件的出口(满足什么条件,去哪个节点)
TreeRoot     → 树的身份证(整棵树叫什么,从哪个节点进入)
TreeRich     → 打包盒(把 TreeRoot 和所有节点装在一起传递)

组合模式引擎设计与核心思路:把"变化的部分"和"不变的部分"分离

复制代码
不变的部分(所有引擎都一样):     变化的部分(每种规则不同):
────────────────────────────────   ────────────────────────────────
遍历树的 while 循环算法             取哪个字段(gender? age? level?)
按 ruleKey 查找 Filter             
按 nodeType 判断是否到终点         

继承体系如何实现这个分离:

复制代码
                IEngine(接口)
                    │
                    │ "我保证对外提供 process() 能力"
                    │
            EngineBase(抽象类)
            ├── extends EngineConfig  → 获得 logicFilterMap 注册表
            ├── process() abstract    → 强制子类实现结果包装
            └── engineDecisionMaker() → 遍历算法,所有子类共用
                    │
                    │ extends
                    │
          TreeEngineHandler(具体类)
          └── process()  → 调用遍历算法 + 包装 EngineResult

LogicFilter 体系如何实现"取值"的扩展:

复制代码
            LogicFilter(接口)
            ├── filter()      → "我能比较"
            └── matterValue() → "我能取值"
                    │
            BaseLogic(抽象类)
            ├── filter() ✅ 实现  → 比较逻辑对所有节点通用,写一次
            └── matterValue() ❌  → 每种节点取不同字段,留给子类
                    │
            ┌───────┴────────┐
  UserGenderFilter    UserAgeFilter
  matterValue()✅      matterValue()✅
  取 "gender"          取 "age"

组合模式的价值体现在扩展时:

复制代码
现在(2个判断维度):        将来(加第3个维度,比如城市):
────────────────────────     ────────────────────────────────
UserGenderFilter             UserGenderFilter   ← 不动
UserAgeFilter                UserAgeFilter      ← 不动
                             UserCityFilter     ← 新增1个文件
                             EngineConfig 加1行  ← 只改1行

所有已有代码:零修改

树的遍历检索算法本质:带条件跳转的 while 循环

这里没有用递归,而是用 while 循环,原因是规则树是单向的(只会从根走向叶子,不会回头),用循环比递归更直观、更安全(不会有栈溢出风险)。

复制代码
算法流程:

START
  │
  ▼
从 treeRootNodeId 取得入口节点
  │
  ▼
┌─────────────────────────────┐
│  nodeType == 1 ? (判断节点) │──── NO ──→ 返回当前节点(果实)→ END
└──────────┬──────────────────┘
           │ YES
           ▼
   ruleKey → logicFilterMap → 找到 Filter
           │
           ▼
   Filter.matterValue() → 取出用户属性值(如 "man")
           │
           ▼
   Filter.filter() → 遍历所有 Link,找满足条件的那条
           │
           ▼
   treeNodeMap.get(nextNodeId) → 跳到下一节点
           │
           └──────────────────────→ 回到循环顶部

算法中最关键的一行:

Java 复制代码
LogicFilter logicFilter = logicFilterMap.get(ruleKey);

这一行完成了从"数据"到"行为"的桥接

复制代码
树节点上存的是字符串 "userAge"
                    ↓
          logicFilterMap.get("userAge")
                    ↓
          拿到 UserAgeFilter 对象
                    ↓
          调用它的 matterValue() 和 filter()

没有任何 if-else 判断是哪种节点,Map 查找直接解决了分支问题。

filter() 内部的匹配算法:

复制代码
遍历当前节点的所有出口链路:

Link1: (< 25)   ──→ decisionLogic("29", Link1) → 29 < 25 → false → 跳过
Link2: (>= 25)  ──→ decisionLogic("29", Link2) → 29 >= 25 → true → 立刻返回 Link2.nodeIdTo

找到第一条满足条件的就返回,后面的不再检查
(因为所有条件是互斥的,找到一条就够了)

EngineConfig 配置设计核心思路:集中注册,统一查找

Java 复制代码
static Map<String, LogicFilter> logicFilterMap;

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

三个技术细节:

复制代码
static 字段:
  属于类本身,不属于任何一个对象实例
  所有继承 EngineConfig 的子类共享同一份 Map
  整个程序只有这一份注册表 ✓

static 代码块:
  JVM 加载这个类时执行一次,且只执行一次
  Filter 对象只被 new 一次,不会重复创建 ✓

ConcurrentHashMap:
  线程安全,多个请求同时进来不会出问题
  生产环境必须考虑并发,普通 HashMap 会出 bug ✓

配置类的位置为什么在 EngineBase 和具体 Filter 之间:

复制代码
Filter 层(知道怎么取值、比较)
      ↑ 被注册进去
EngineConfig(注册中心,Map 存放所有 Filter)
      ↑ 被继承
EngineBase(遍历算法,直接用 logicFilterMap)
      ↑ 被继承
TreeEngineHandler(具体引擎,包装结果)

这个顺序保证了:引擎能直接用注册表,注册表知道所有 Filter,但引擎不需要直接依赖每一个具体 Filter 类

三个设计的整体关系

复制代码
数据结构设计          引擎设计                算法设计
──────────────        ──────────────          ──────────────
TreeRich 装载         IEngine 对外暴露        while 循环遍历
  规则树数据    →     process() 接口    →     按 ruleKey 查 Filter
                                             按 Link 条件跳转
TreeNode 描述         EngineBase 实现          
  节点和出口    →     遍历算法           →    matterValue 取值
                                             filter 比较跳转
TreeNodeLink 描述     LogicFilter 体系
  条件和目标    →     取值 + 比较        →    找到果实节点返回

三者缺一不可:
没有数据结构 → 无法描述规则
没有引擎设计 → 无法扩展新规则
没有遍历算法 → 无法执行判断

核心组件理解确认

IEngine --- 所有规则树引擎都要实现这个接口,不管是用户信息树、学历树还是其他什么树,对外都暴露同一个 process() 方法,调用方不需要关心内部是什么树。

EngineConfig --- 基本正确,但不是严格的单例模式。它用 static 保证 logicFilterMap 只有一份,让所有继承它的子类共用同一个注册表。严格单例是控制对象实例只有一个,这里控制的是 Map 数据只有一份,思路相同但写法略有区别。

EngineBase --- 完全正确。抽象类可以继承普通类,也可以实现接口,同时把接口方法继续声明为 abstract 强制子类实现。自己写的 engineDecisionMaker 是所有子类共用的遍历算法,子类直接调用,不重复写。

新加一棵规则树的扩展方式

假设现在要加一棵学历规则树,判断逻辑是:

复制代码
节点1:判断学历 (ruleKey = "userEducation")
  ├── == "bachelor" → 节点11
  └── == "master"   → 节点12

节点11:判断工作年限 (ruleKey = "userWorkYear")
  ├── < 3  → 果实X(初级岗)
  └── >= 3 → 果实Y(中级岗)

节点12:直接是果实Z(高级岗)

只需要做两件事:

新增 Filter 类

Java 复制代码
// 新增:UserEducationFilter.java
public class UserEducationFilter extends BaseLogic {
    @Override
    public String matterValue(Long treeId, String userId, Map<String, String> decisionMatter) {
        return decisionMatter.get("education"); // 只加这一个文件
    }
}

// 新增:UserWorkYearFilter.java
public class UserWorkYearFilter extends BaseLogic {
    @Override
    public String matterValue(Long treeId, String userId, Map<String, String> decisionMatter) {
        return decisionMatter.get("workYear");
    }
}

EngineConfig 注册

Java 复制代码
static {
    logicFilterMap = new ConcurrentHashMap<>();
    logicFilterMap.put("userAge",       new UserAgeFilter());
    logicFilterMap.put("userGender",    new UserGenderFilter());
    logicFilterMap.put("userEducation", new UserEducationFilter()); // ← 加这一行
    logicFilterMap.put("userWorkYear",  new UserWorkYearFilter());  // ← 加这一行
}

其他所有代码零修改 。引擎遍历时遇到 ruleKey="userEducation" 自动就能找到对应的 Filter。

在原有规则树上新增判断维度的扩展方式

复制代码
                    原始 if-else 设计
────────────────────────────────────────────────────
加新判断维度:   进 EngineController 改代码 ✗
加新规则树:     复制粘贴一个新的 Controller ✗
多人协作:       改同一个文件,代码冲突 ✗
单元测试:       整个方法耦合在一起,难以测试 ✗

                    现在的设计
────────────────────────────────────────────────────
加新判断维度:   新增一个 Filter 文件 + 注册一行 ✓
加新规则树:     新增一个 EngineHandler 文件 ✓
多人协作:       每人负责自己的 Filter 文件,互不干扰 ✓
单元测试:       每个 Filter 独立测试,引擎独立测试 ✓

核心原则是"开闭原则"

复制代码
对扩展开放  → 可以随时加新的 Filter、新的 Engine
对修改关闭  → 加新东西不需要改已有代码

4.2 架构图

4.2.1 面条代码架构图
4.2.2 组合模式架构图

4.3 时序图

4.3.1 面条代码时序图

EngineController 客户端 EngineController 客户端 if ("man".equals(userSex)) if (userAge >= 25) → return "果实B" 规则逻辑全部内联,调用方需传入具体字段值 修改规则 = 修改 process 方法本身 process("Oli09pLkdjh", "man", 29) "果实B"


4.3.2 组合模式时序图

TreeRich/TreeNode UserAgeFilter UserGenderFilter EngineBase TreeEngineHandle 客户端 TreeRich/TreeNode UserAgeFilter UserGenderFilter EngineBase TreeEngineHandle 客户端 从根节点1开始,nodeType=1(子叶),进入循环 "man".equals("man") → true,返回 nodeIdTo=11 nodeType=1,继续循环 29 >= 25 → case5 成立,返回 nodeIdTo=112 process(10001L, "Oli09pLkdjh", treeRich, {gender:man, age:29}) engineDecisionMaker(treeRich, treeId, userId, decisionMatter) 取节点1,ruleKey="userGender" matterValue(10001, "Oli09pLkdjh", {gender:man, age:29}) "man" filter("man", links[→11(man), →12(woman)]) 11L 取节点11,ruleKey="userAge" matterValue(10001, "Oli09pLkdjh", {gender:man, age:29}) "29" filter("29", links[→111(<25), →112(>=25)]) 112L 取节点112,nodeType=2(果实),退出循环 TreeNode(nodeId=112, nodeValue="果实B") EngineResult(userId, treeId=10001, nodeId=112, nodeValue="果实B")


4.4 代码分析

4.4.1 面条代码(if-else / 硬编码)
java 复制代码
// tutorials-11.0-1 EngineController.java
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";   // 硬编码:性别man AND 年龄<25
        }
        if (userAge >= 25) {
            return "果实B";   // 硬编码:性别man AND 年龄>=25
        }
    }

    if ("woman".equals(userSex)) {
        if (userAge < 25) {
            return "果实C";   // 硬编码:性别woman AND 年龄<25
        }
        if (userAge >= 25) {
            return "果实D";   // 硬编码:性别woman AND 年龄>=25
        }
    }

    return null;
}

问题清单(真实业务演进过程):

迭代 产品需求 代码影响
第1次 按性别发不同优惠券 加2个 if 分支
第2次 再按年龄细分 嵌套 if,分支数×2
第3次 加婚育状况 继续嵌套,分支数再×3
第4次 调整年龄阈值 全文搜索魔法数字,极易改错
第5次 值粘错位置 线上 bug,客诉

4.4.2 组合模式代码

(1)数据模型层:树的数据结构

java 复制代码
// TreeNode.java - 节点有两种类型
// nodeType=1: 子叶节点(带判断逻辑,有子链路)
// nodeType=2: 果实节点(无子链路,有果实值)
TreeNode {
    Long treeNodeId;
    Integer nodeType;           // 1=子叶, 2=果实
    String nodeValue;           // 仅果实节点有值
    String ruleKey;             // 对应 EngineConfig 中的 key(如 "userGender")
    List<TreeNodeLink> treeNodeLinkList;  // 出边列表
}

// TreeNodeLink.java - 连接两个节点的有向边
TreeNodeLink {
    Long nodeIdFrom;            // 起始节点
    Long nodeIdTo;              // 目标节点
    Integer ruleLimitType;      // 比较类型:1=等于, 2=>,3=<, 4=<=, 5=>=
    String ruleLimitValue;      // 比较阈值
}

(2)决策节点接口 LogicFilter(组合模式的 Component)

java 复制代码
public interface LogicFilter {
    // 过滤器:传入"当前节点的决策值" + "所有出边",返回"命中的下一节点ID"
    Long filter(String matterValue, List<TreeNodeLink> treeNodeLineInfoList);

    // 取值器:从决策物料中提取当前节点需要的值(如 age、gender)
    String matterValue(Long treeId, String userId, Map<String, String> decisionMatter);
}

(3)抽象类 BaseLogic:实现通用比较逻辑(模板方法)

java 复制代码
public abstract class BaseLogic implements LogicFilter {

    @Override
    public Long filter(String matterValue, List<TreeNodeLink> treeNodeLinkList) {
        // 遍历所有出边,找到第一个满足条件的,返回其目标节点ID
        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);

    // 通用比较逻辑(equals / > / < / <= / >=)
    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;
        }
    }
}

(4)具体节点实现(Leaf,只覆盖取值逻辑)

java 复制代码
// 年龄节点:从物料Map中取 "age"
public class UserAgeFilter extends BaseLogic {
    @Override
    public String matterValue(Long treeId, String userId, Map<String, String> decisionMatter) {
        return decisionMatter.get("age");
    }
}

// 性别节点:从物料Map中取 "gender"
public class UserGenderFilter extends BaseLogic {
    @Override
    public String matterValue(Long treeId, String userId, Map<String, String> decisionMatter) {
        return decisionMatter.get("gender");
    }
}

新增一种判断维度(如"婚育状态")只需新建一个 UserMaritalFilter extends BaseLogic,在 EngineConfig 中注册,然后在树结构数据中挂载新节点。原有任何代码无需修改。

(5)节点注册配置 EngineConfig

java 复制代码
public class EngineConfig {
    static Map<String, LogicFilter> logicFilterMap;

    static {
        logicFilterMap = new ConcurrentHashMap<>();
        logicFilterMap.put("userAge", new UserAgeFilter());       // ruleKey → 节点实例
        logicFilterMap.put("userGender", new UserGenderFilter()); // 可从数据库/配置中心加载
    }
}

(6)引擎核心:树遍历算法 EngineBase.engineDecisionMaker()

java 复制代码
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();           // 取节点的过滤器key
        LogicFilter logicFilter = logicFilterMap.get(ruleKey); // 找到对应过滤器
        String matterValue = logicFilter.matterValue(treeId, userId, decisionMatter); // 取值
        Long nextNode = logicFilter.filter(matterValue, treeNodeInfo.getTreeNodeLinkList()); // 过滤,找下一节点
        treeNodeInfo = treeNodeMap.get(nextNode);             // 移动到下一节点
    }

    return treeNodeInfo;  // 返回果实节点(nodeType=2)
}

(7)引擎实现与调用

java 复制代码
// TreeEngineHandle:最终实现,只是薄薄的一层
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());
    }
}

// 调用方
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);
// result.getNodeValue() = "果实B"
// result.getNodeId()    = 112

总结

回顾整个重构路径:原始代码用 if-else 把所有判断逻辑硬编码在一个方法里,每次业务迭代都必须进入同一个方法修改,随着条件组合数指数级增长,代码迅速腐化。重构后的方案将每一个判断条件抽象为独立的 LogicFilter节点,将判断规则的结构数据化为 TreeNode + TreeNodeLink,通过 EngineBasewhile 循环统一遍历,彻底消灭了业务逻辑里的 if-else

组合模式在这里真正发挥价值的地方,不是经典教材里那句"把对象放进集合",而是:

  • 同一个 TreeNode 类既是判断节点(Composite)又是结果节点(Leaf),引擎统一处理,无需区分类型,树结构可以任意层数扩展而引擎代码不动;
  • 节点(Filter)只负责自己的取值逻辑,和其他节点完全解耦,新增维度只加文件不改存量;
  • 业务规则的变化通过"换树结构"来承接,而不是"改节点代码",真正实现了对扩展开放、对修改关闭。

组合模式最适合的场景可以用一句话来判断:业务节点相对稳定,但节点之间的组合方式经常变化。 只要你的业务满足这个特征,组合模式就值得引入。

相关推荐
多加点辣也没关系1 小时前
设计模式-迭代器模式
设计模式·迭代器模式
江米小枣tonylua16 小时前
从红绿灯到方向盘:TDD 在 AI 时代的新角色
前端·设计模式·ai编程
nnsix16 小时前
设计模式 - 工厂模式 笔记
笔记·设计模式
洛水水21 小时前
结构性设计模式详解
c++·设计模式
多加点辣也没关系1 天前
设计模式-策略模式
java·设计模式·策略模式
雪度娃娃1 天前
结构型设计模式——代理模式
java·c++·设计模式·系统安全·代理模式
蜡笔小马1 天前
06.C++设计模式-装饰模式
c++·设计模式·装饰器模式
悟05151 天前
设计模式-策略模式
设计模式·策略模式