业务开发常用设计模式:状态、策略、工厂、适配器、模板方法

前置说明:本文只讲 Java 后端业务代码里高频出现的 5 个模式------模板方法、策略、工厂、状态、适配器。不涉及框架/中间件开发,不追求 GOF 23 个全覆盖。


前言

为什么是这 5 个?

因为 90% 的业务代码,本质上都在重复做 5 件事:

业务里反复出现的问题 对应模式
流程固定,但某些步骤不一样 模板方法
同一件事,多种算法 策略
根据类型 new 不同对象 工厂
对象行为随状态变化 状态
对接多个外部接口,内部想统一调用 适配器

学这 5 个,是为了在写代码时能识别出:

这段代码里,哪部分是不变的,哪部分是变化的?

把变化的部分抽出来,用接口隔离,用工厂选择,用状态管理,用适配器包裹。

这就是所有设计模式的底层心法。


贯穿全文的业务案例

大促领券活动:

  • 接口 receiveCoupon 领券(写)
  • 接口 queryCoupon 券查询(读)
  • 券有生命周期状态:待使用、已使用、已过期、已冻结
  • 活动类型:满减、折扣、新人立减、限时秒杀
  • 下游依赖:自营券库存 RPC、美团券 API、抖音券 API

这 5 个模式,刚好各自对应上面一个变化点。


一、模板方法 Template Method

1.1 一句话定义

父类定义流程骨架,把可变步骤留给子类实现。

1.2 痛点场景

领券流程:

复制代码
参数校验 → 风控 → 查活动 → 扣库存 → 发券 → 发消息

满减活动、折扣活动、新人活动的流程完全一样,只是「查活动」「扣库存」「发券」的实现不同。

如果用 if-else 写:

java 复制代码
if (type == FULL_REDUCE) {
    validate(); riskCheck(); loadFullReduceActivity(); deductStock(); issueCoupon(); ...
} else if (type == DISCOUNT) {
    validate(); riskCheck(); loadDiscountActivity(); deductStock(); issueCoupon(); ...
}

流程被复制了 N 遍,改一处要改 N 处。

1.3 抽象心法

流程是稳定的,步骤是多变的。

把稳定的流程写死在父类(final),把多变的步骤开放给子类(abstractprotected 钩子)。

判断标准:你在用"步骤顺序"思考业务时,就该想到模板方法。

1.4 结构图

#mermaid-svg-QuciUfh8lzFjmyJc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QuciUfh8lzFjmyJc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QuciUfh8lzFjmyJc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QuciUfh8lzFjmyJc .error-icon{fill:#552222;}#mermaid-svg-QuciUfh8lzFjmyJc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QuciUfh8lzFjmyJc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QuciUfh8lzFjmyJc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QuciUfh8lzFjmyJc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QuciUfh8lzFjmyJc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QuciUfh8lzFjmyJc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QuciUfh8lzFjmyJc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QuciUfh8lzFjmyJc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QuciUfh8lzFjmyJc .marker.cross{stroke:#333333;}#mermaid-svg-QuciUfh8lzFjmyJc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QuciUfh8lzFjmyJc p{margin:0;}#mermaid-svg-QuciUfh8lzFjmyJc .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-QuciUfh8lzFjmyJc .cluster-label text{fill:#333;}#mermaid-svg-QuciUfh8lzFjmyJc .cluster-label span{color:#333;}#mermaid-svg-QuciUfh8lzFjmyJc .cluster-label span p{background-color:transparent;}#mermaid-svg-QuciUfh8lzFjmyJc .label text,#mermaid-svg-QuciUfh8lzFjmyJc span{fill:#333;color:#333;}#mermaid-svg-QuciUfh8lzFjmyJc .node rect,#mermaid-svg-QuciUfh8lzFjmyJc .node circle,#mermaid-svg-QuciUfh8lzFjmyJc .node ellipse,#mermaid-svg-QuciUfh8lzFjmyJc .node polygon,#mermaid-svg-QuciUfh8lzFjmyJc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-QuciUfh8lzFjmyJc .rough-node .label text,#mermaid-svg-QuciUfh8lzFjmyJc .node .label text,#mermaid-svg-QuciUfh8lzFjmyJc .image-shape .label,#mermaid-svg-QuciUfh8lzFjmyJc .icon-shape .label{text-anchor:middle;}#mermaid-svg-QuciUfh8lzFjmyJc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-QuciUfh8lzFjmyJc .rough-node .label,#mermaid-svg-QuciUfh8lzFjmyJc .node .label,#mermaid-svg-QuciUfh8lzFjmyJc .image-shape .label,#mermaid-svg-QuciUfh8lzFjmyJc .icon-shape .label{text-align:center;}#mermaid-svg-QuciUfh8lzFjmyJc .node.clickable{cursor:pointer;}#mermaid-svg-QuciUfh8lzFjmyJc .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-QuciUfh8lzFjmyJc .arrowheadPath{fill:#333333;}#mermaid-svg-QuciUfh8lzFjmyJc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-QuciUfh8lzFjmyJc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-QuciUfh8lzFjmyJc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QuciUfh8lzFjmyJc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-QuciUfh8lzFjmyJc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QuciUfh8lzFjmyJc .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-QuciUfh8lzFjmyJc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-QuciUfh8lzFjmyJc .cluster text{fill:#333;}#mermaid-svg-QuciUfh8lzFjmyJc .cluster span{color:#333;}#mermaid-svg-QuciUfh8lzFjmyJc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-QuciUfh8lzFjmyJc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-QuciUfh8lzFjmyJc rect.text{fill:none;stroke-width:0;}#mermaid-svg-QuciUfh8lzFjmyJc .icon-shape,#mermaid-svg-QuciUfh8lzFjmyJc .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QuciUfh8lzFjmyJc .icon-shape p,#mermaid-svg-QuciUfh8lzFjmyJc .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-QuciUfh8lzFjmyJc .icon-shape .label rect,#mermaid-svg-QuciUfh8lzFjmyJc .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QuciUfh8lzFjmyJc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-QuciUfh8lzFjmyJc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-QuciUfh8lzFjmyJc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开始 receive
validate 参数校验

固定
riskCheck 风控

固定
loadActivity 加载活动

可变
deductStock 扣库存

可变
issueCoupon 发券

可变
afterIssue 后置钩子

可选
返回结果

蓝色 = 固定步骤,橙色 = 子类必须实现,浅蓝 = 可选钩子。

1.5 代码示例

java 复制代码
public abstract class AbstractCouponReceiveTemplate {

    // 模板方法:final 防子类改流程
    public final ReceiveResult receive(ReceiveContext ctx) {
        validate(ctx);
        riskCheck(ctx);
        Activity activity = loadActivity(ctx);
        DeductResult deduct = deductStock(activity, ctx);
        if (!deduct.isSuccess()) {
            return ReceiveResult.fail("库存不足");
        }
        try {
            Coupon coupon = issueCoupon(activity, ctx);
            afterIssue(activity, ctx, coupon);
            return ReceiveResult.success(coupon);
        } catch (Exception e) {
            rollbackStock(activity, ctx);
            throw e;
        }
    }

    // 固定步骤:private,子类不可见
    private void validate(ReceiveContext ctx) { /* ... */ }
    private void riskCheck(ReceiveContext ctx) { /* ... */ }

    // 可变步骤:子类必须实现
    protected abstract Activity loadActivity(ReceiveContext ctx);
    protected abstract DeductResult deductStock(Activity a, ReceiveContext ctx);
    protected abstract Coupon issueCoupon(Activity a, ReceiveContext ctx);

    // 钩子:默认空实现,子类按需覆盖
    protected void afterIssue(Activity a, ReceiveContext ctx, Coupon c) {}
    protected void rollbackStock(Activity a, ReceiveContext ctx) {}
}

1.6 什么时候用 / 不用

  • ✅ 流程步骤 ≥ 3 步,且多种业务共享同一流程
  • ✅ 希望流程本身被"锁死",不允许子类乱改
  • ❌ 只有 2 个分支、流程短,直接 if-else 更清晰
  • ❌ 步骤之间差异太大,硬抽父类会导致子类"必须实现一堆用不上的钩子"

1.7 坑点

  1. 不要为了继承而继承:模板方法的本质是"流程复用",不是为了共用几个工具方法。
  2. 钩子数量要克制:超过 3 个钩子,说明抽象过度,考虑拆分。
  3. 父类方法要 final:否则子类重写流程,模板方法就形同虚设。
  4. 异常处理要统一:回滚、日志、埋点应放在模板方法里,不要散落到子类。

二、策略 Strategy

2.1 一句话定义

同一件事有多种做法,把"做法"抽成接口,运行时挑一个。

2.2 痛点场景

计算券的优惠金额:

  • 满减:满 100 减 20
  • 折扣:8 折
  • 新人立减:直接减 10 元
  • 秒杀:固定价 9.9

用 if-else:

java 复制代码
if (type == FULL_REDUCE) { ... }
else if (type == DISCOUNT) { ... }
else if (type == NEW_USER) { ... }
else if (type == SECKILL) { ... }

每加一种活动类型,就要改这段代码,违反开闭原则。

2.3 抽象心法

算法是可替换的,调用方只关心接口,不关心实现。

判断标准:多个 if-else 分支在做"同一类事",只是算法不同 → 策略模式。

2.4 结构图

#mermaid-svg-v7BSUTs6LRMegAYZ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-v7BSUTs6LRMegAYZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-v7BSUTs6LRMegAYZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-v7BSUTs6LRMegAYZ .error-icon{fill:#552222;}#mermaid-svg-v7BSUTs6LRMegAYZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-v7BSUTs6LRMegAYZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-v7BSUTs6LRMegAYZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-v7BSUTs6LRMegAYZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-v7BSUTs6LRMegAYZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-v7BSUTs6LRMegAYZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-v7BSUTs6LRMegAYZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-v7BSUTs6LRMegAYZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-v7BSUTs6LRMegAYZ .marker.cross{stroke:#333333;}#mermaid-svg-v7BSUTs6LRMegAYZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-v7BSUTs6LRMegAYZ p{margin:0;}#mermaid-svg-v7BSUTs6LRMegAYZ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-v7BSUTs6LRMegAYZ .cluster-label text{fill:#333;}#mermaid-svg-v7BSUTs6LRMegAYZ .cluster-label span{color:#333;}#mermaid-svg-v7BSUTs6LRMegAYZ .cluster-label span p{background-color:transparent;}#mermaid-svg-v7BSUTs6LRMegAYZ .label text,#mermaid-svg-v7BSUTs6LRMegAYZ span{fill:#333;color:#333;}#mermaid-svg-v7BSUTs6LRMegAYZ .node rect,#mermaid-svg-v7BSUTs6LRMegAYZ .node circle,#mermaid-svg-v7BSUTs6LRMegAYZ .node ellipse,#mermaid-svg-v7BSUTs6LRMegAYZ .node polygon,#mermaid-svg-v7BSUTs6LRMegAYZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-v7BSUTs6LRMegAYZ .rough-node .label text,#mermaid-svg-v7BSUTs6LRMegAYZ .node .label text,#mermaid-svg-v7BSUTs6LRMegAYZ .image-shape .label,#mermaid-svg-v7BSUTs6LRMegAYZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-v7BSUTs6LRMegAYZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-v7BSUTs6LRMegAYZ .rough-node .label,#mermaid-svg-v7BSUTs6LRMegAYZ .node .label,#mermaid-svg-v7BSUTs6LRMegAYZ .image-shape .label,#mermaid-svg-v7BSUTs6LRMegAYZ .icon-shape .label{text-align:center;}#mermaid-svg-v7BSUTs6LRMegAYZ .node.clickable{cursor:pointer;}#mermaid-svg-v7BSUTs6LRMegAYZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-v7BSUTs6LRMegAYZ .arrowheadPath{fill:#333333;}#mermaid-svg-v7BSUTs6LRMegAYZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-v7BSUTs6LRMegAYZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-v7BSUTs6LRMegAYZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-v7BSUTs6LRMegAYZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-v7BSUTs6LRMegAYZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-v7BSUTs6LRMegAYZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-v7BSUTs6LRMegAYZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-v7BSUTs6LRMegAYZ .cluster text{fill:#333;}#mermaid-svg-v7BSUTs6LRMegAYZ .cluster span{color:#333;}#mermaid-svg-v7BSUTs6LRMegAYZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-v7BSUTs6LRMegAYZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-v7BSUTs6LRMegAYZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-v7BSUTs6LRMegAYZ .icon-shape,#mermaid-svg-v7BSUTs6LRMegAYZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-v7BSUTs6LRMegAYZ .icon-shape p,#mermaid-svg-v7BSUTs6LRMegAYZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-v7BSUTs6LRMegAYZ .icon-shape .label rect,#mermaid-svg-v7BSUTs6LRMegAYZ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-v7BSUTs6LRMegAYZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-v7BSUTs6LRMegAYZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-v7BSUTs6LRMegAYZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 面向接口调用
实现
实现
实现
实现
业务调用方
DiscountStrategy
满减策略
折扣策略
新人立减策略
秒杀策略

2.5 代码示例

java 复制代码
public interface DiscountStrategy {
    DiscountType type();
    BigDecimal calculate(BigDecimal originalPrice, Activity activity);
}

@Component
public class FullReduceStrategy implements DiscountStrategy {
    public DiscountType type() { return DiscountType.FULL_REDUCE; }

    public BigDecimal calculate(BigDecimal price, Activity activity) {
        // 满减逻辑
        return price.compareTo(activity.getThreshold()) >= 0
                ? price.subtract(activity.getReduceAmount())
                : price;
    }
}

@Component
public class DiscountStrategy implements DiscountStrategy {
    public DiscountType type() { return DiscountType.DISCOUNT; }

    public BigDecimal calculate(BigDecimal price, Activity activity) {
        return price.multiply(activity.getDiscountRate());
    }
}

调用方:

java 复制代码
BigDecimal finalPrice = strategy.calculate(price, activity);

调用方完全不关心是哪种算法。

2.6 什么时候用 / 不用

  • ✅ 分支 ≥ 3 个,每个分支是"同一类算法"
  • ✅ 未来可能不断新增类型(开闭原则)
  • ❌ 只有 2 个稳定分支,if-else 更直观
  • ❌ 分支内部逻辑差异极大,抽出来的接口很"假"

2.7 坑点

  1. 策略类不要变成"空壳" :如果实现只有一行 return xxx,可能不值得抽象。
  2. 策略的粒度要对:一个策略负责一件事,不要一个策略类里又塞多个分支。
  3. 策略的输入输出要统一:接口签名定好,否则子类会各自为政。
  4. 策略是无状态的:有状态的话,跟状态模式就混了(见下文对比)。

三、工厂 Factory

3.1 一句话定义

把"选哪个实现"的逻辑收口到一个地方。

3.2 痛点场景

策略写好了,但调用方还是要知道"用哪个策略":

java 复制代码
DiscountStrategy strategy;
if (type == FULL_REDUCE) strategy = new FullReduceStrategy();
else if (type == DISCOUNT) strategy = new DiscountStrategy();
...

这段选择逻辑散落在业务代码里,还是要改。

3.3 抽象心法

调用方只关心"拿到能用的东西",不关心"怎么选出来的"。

判断标准:代码里出现"根据 type 去 new 不同对象" → 工厂模式。

工厂的本质不是"创建对象",而是把选择逻辑和业务逻辑解耦

3.4 结构图

#mermaid-svg-U2ekiep15QED8Hto{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-U2ekiep15QED8Hto .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-U2ekiep15QED8Hto .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-U2ekiep15QED8Hto .error-icon{fill:#552222;}#mermaid-svg-U2ekiep15QED8Hto .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-U2ekiep15QED8Hto .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-U2ekiep15QED8Hto .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-U2ekiep15QED8Hto .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-U2ekiep15QED8Hto .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-U2ekiep15QED8Hto .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-U2ekiep15QED8Hto .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-U2ekiep15QED8Hto .marker{fill:#333333;stroke:#333333;}#mermaid-svg-U2ekiep15QED8Hto .marker.cross{stroke:#333333;}#mermaid-svg-U2ekiep15QED8Hto svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-U2ekiep15QED8Hto p{margin:0;}#mermaid-svg-U2ekiep15QED8Hto .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-U2ekiep15QED8Hto .cluster-label text{fill:#333;}#mermaid-svg-U2ekiep15QED8Hto .cluster-label span{color:#333;}#mermaid-svg-U2ekiep15QED8Hto .cluster-label span p{background-color:transparent;}#mermaid-svg-U2ekiep15QED8Hto .label text,#mermaid-svg-U2ekiep15QED8Hto span{fill:#333;color:#333;}#mermaid-svg-U2ekiep15QED8Hto .node rect,#mermaid-svg-U2ekiep15QED8Hto .node circle,#mermaid-svg-U2ekiep15QED8Hto .node ellipse,#mermaid-svg-U2ekiep15QED8Hto .node polygon,#mermaid-svg-U2ekiep15QED8Hto .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-U2ekiep15QED8Hto .rough-node .label text,#mermaid-svg-U2ekiep15QED8Hto .node .label text,#mermaid-svg-U2ekiep15QED8Hto .image-shape .label,#mermaid-svg-U2ekiep15QED8Hto .icon-shape .label{text-anchor:middle;}#mermaid-svg-U2ekiep15QED8Hto .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-U2ekiep15QED8Hto .rough-node .label,#mermaid-svg-U2ekiep15QED8Hto .node .label,#mermaid-svg-U2ekiep15QED8Hto .image-shape .label,#mermaid-svg-U2ekiep15QED8Hto .icon-shape .label{text-align:center;}#mermaid-svg-U2ekiep15QED8Hto .node.clickable{cursor:pointer;}#mermaid-svg-U2ekiep15QED8Hto .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-U2ekiep15QED8Hto .arrowheadPath{fill:#333333;}#mermaid-svg-U2ekiep15QED8Hto .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-U2ekiep15QED8Hto .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-U2ekiep15QED8Hto .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-U2ekiep15QED8Hto .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-U2ekiep15QED8Hto .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-U2ekiep15QED8Hto .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-U2ekiep15QED8Hto .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-U2ekiep15QED8Hto .cluster text{fill:#333;}#mermaid-svg-U2ekiep15QED8Hto .cluster span{color:#333;}#mermaid-svg-U2ekiep15QED8Hto div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-U2ekiep15QED8Hto .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-U2ekiep15QED8Hto rect.text{fill:none;stroke-width:0;}#mermaid-svg-U2ekiep15QED8Hto .icon-shape,#mermaid-svg-U2ekiep15QED8Hto .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-U2ekiep15QED8Hto .icon-shape p,#mermaid-svg-U2ekiep15QED8Hto .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-U2ekiep15QED8Hto .icon-shape .label rect,#mermaid-svg-U2ekiep15QED8Hto .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-U2ekiep15QED8Hto .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-U2ekiep15QED8Hto .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-U2ekiep15QED8Hto :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 传入 type
查询注册表
返回对应实例
业务调用方
DiscountStrategyFactory
Map: type -> strategy
满减策略
折扣策略
新人策略

3.5 代码示例(Spring 版本)

java 复制代码
@Component
public class DiscountStrategyFactory {

    private final Map<DiscountType, DiscountStrategy> registry = new HashMap<>();

    // Spring 自动注入所有 DiscountStrategy 实现
    public DiscountStrategyFactory(List<DiscountStrategy> strategies) {
        for (DiscountStrategy s : strategies) {
            registry.put(s.type(), s);
        }
    }

    public DiscountStrategy get(DiscountType type) {
        DiscountStrategy s = registry.get(type);
        if (s == null) {
            throw new IllegalArgumentException("不支持的优惠类型: " + type);
        }
        return s;
    }
}

调用方:

java 复制代码
DiscountStrategy strategy = factory.get(activity.getType());
BigDecimal finalPrice = strategy.calculate(price, activity);

新增一种活动类型,只需新增一个 @Component 策略类,工厂和调用方一行都不用改

3.6 什么时候用 / 不用

  • ✅ 有多个实现,需要按类型选择
  • ✅ 希望新增实现时不影响调用方
  • ✅ 配合 Spring 的 List<T> 自动注入,几乎零成本
  • ❌ 只有 1~2 个实现,直接 new 或注入即可
  • ❌ 对象创建逻辑极复杂,但只创建一次(用 Spring 配置类即可)

3.7 坑点

  1. 工厂不要变上帝类:如果工厂里塞满了各种类型的创建逻辑,说明抽象层次错了。
  2. Spring 环境下优先用容器@Autowired List<T> 比手写工厂更简洁。
  3. 工厂的异常要明确:type 找不到时抛什么异常、怎么降级,要定好。
  4. 工厂 + 策略是一对搭档:策略负责算法,工厂负责选择。两者经常一起出现。

四、状态 State

4.1 一句话定义

对象的行为取决于它的状态,状态变了,行为跟着变。

4.2 痛点场景

券有生命周期状态:待使用、已使用、已过期、已冻结。

不同状态下允许的操作不同:

  • 待使用:可核销、可冻结、可过期
  • 已使用:不可再核销
  • 已过期:不可核销
  • 已冻结:可解冻,不可核销

用 if-else:

java 复制代码
public void use(Coupon c) {
    if (c.status == UNUSED) { ... }
    else if (c.status == FROZEN) { throw new IllegalStateException("已冻结"); }
    else if (c.status == USED) { throw new IllegalStateException("已使用"); }
    else if (c.status == EXPIRED) { throw new IllegalStateException("已过期"); }
}

这段 if-else 会在每个操作里重复出现。

4.3 抽象心法

对象的行为是"状态"的函数。

判断标准:代码里大量出现 if (status == X) { 允许 A,禁止 B } → 状态模式。

和策略的区别:

维度 策略 状态
谁决定用哪个 调用方显式选择 对象自己(内部流转)
实现之间是否知道彼此 不知道 通常知道下一个状态
关注点 算法可替换 行为合法性 + 状态流转
典型问句 "这次用哪种算法?" "当前处于什么状态?"

4.4 结构图

#mermaid-svg-hHqe0VIFBYTfOv5R{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hHqe0VIFBYTfOv5R .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hHqe0VIFBYTfOv5R .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hHqe0VIFBYTfOv5R .error-icon{fill:#552222;}#mermaid-svg-hHqe0VIFBYTfOv5R .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hHqe0VIFBYTfOv5R .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hHqe0VIFBYTfOv5R .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hHqe0VIFBYTfOv5R .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hHqe0VIFBYTfOv5R .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hHqe0VIFBYTfOv5R .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hHqe0VIFBYTfOv5R .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hHqe0VIFBYTfOv5R .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hHqe0VIFBYTfOv5R .marker.cross{stroke:#333333;}#mermaid-svg-hHqe0VIFBYTfOv5R svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hHqe0VIFBYTfOv5R p{margin:0;}#mermaid-svg-hHqe0VIFBYTfOv5R defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-hHqe0VIFBYTfOv5R g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-hHqe0VIFBYTfOv5R g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-hHqe0VIFBYTfOv5R g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-hHqe0VIFBYTfOv5R g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-hHqe0VIFBYTfOv5R g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-hHqe0VIFBYTfOv5R .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-hHqe0VIFBYTfOv5R .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-hHqe0VIFBYTfOv5R .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-hHqe0VIFBYTfOv5R .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-hHqe0VIFBYTfOv5R .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-hHqe0VIFBYTfOv5R .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-hHqe0VIFBYTfOv5R .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-hHqe0VIFBYTfOv5R .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hHqe0VIFBYTfOv5R .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-hHqe0VIFBYTfOv5R .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hHqe0VIFBYTfOv5R .edgeLabel .label text{fill:#333;}#mermaid-svg-hHqe0VIFBYTfOv5R .label div .edgeLabel{color:#333;}#mermaid-svg-hHqe0VIFBYTfOv5R .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-hHqe0VIFBYTfOv5R .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-hHqe0VIFBYTfOv5R .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-hHqe0VIFBYTfOv5R .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-hHqe0VIFBYTfOv5R .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-hHqe0VIFBYTfOv5R .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hHqe0VIFBYTfOv5R .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hHqe0VIFBYTfOv5R #statediagram-barbEnd{fill:#333333;}#mermaid-svg-hHqe0VIFBYTfOv5R .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hHqe0VIFBYTfOv5R .cluster-label,#mermaid-svg-hHqe0VIFBYTfOv5R .nodeLabel{color:#131300;}#mermaid-svg-hHqe0VIFBYTfOv5R .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-hHqe0VIFBYTfOv5R .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-hHqe0VIFBYTfOv5R .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-hHqe0VIFBYTfOv5R .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-hHqe0VIFBYTfOv5R .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-hHqe0VIFBYTfOv5R .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-hHqe0VIFBYTfOv5R .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-hHqe0VIFBYTfOv5R .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-hHqe0VIFBYTfOv5R .note-edge{stroke-dasharray:5;}#mermaid-svg-hHqe0VIFBYTfOv5R .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-hHqe0VIFBYTfOv5R .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-hHqe0VIFBYTfOv5R .statediagram-note text{fill:black;}#mermaid-svg-hHqe0VIFBYTfOv5R .statediagram-note .nodeLabel{color:black;}#mermaid-svg-hHqe0VIFBYTfOv5R .statediagram .edgeLabel{color:red;}#mermaid-svg-hHqe0VIFBYTfOv5R #dependencyStart,#mermaid-svg-hHqe0VIFBYTfOv5R #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-hHqe0VIFBYTfOv5R .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-hHqe0VIFBYTfOv5R :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 发放成功
核销
过期
冻结
解冻
过期
UNUSED
USED
EXPIRED
FROZEN

4.5 代码示例

java 复制代码
public interface CouponState {
    CouponStatus status();
    void use(CouponContext ctx);
    void freeze(CouponContext ctx);
    void expire(CouponContext ctx);
}

@Component
public class UnusedState implements CouponState {
    public CouponStatus status() { return CouponStatus.UNUSED; }

    public void use(CouponContext ctx) {
        // 允许核销,并流转到 USED
        ctx.getCoupon().setStatus(CouponStatus.USED);
    }

    public void freeze(CouponContext ctx) {
        ctx.getCoupon().setStatus(CouponStatus.FROZEN);
    }

    public void expire(CouponContext ctx) {
        ctx.getCoupon().setStatus(CouponStatus.EXPIRED);
    }
}

@Component
public class UsedState implements CouponState {
    public CouponStatus status() { return CouponStatus.USED; }

    public void use(CouponContext ctx) {
        throw new IllegalStateException("券已使用,不能重复核销");
    }

    public void freeze(CouponContext ctx) {
        throw new IllegalStateException("券已使用,不能冻结");
    }

    public void expire(CouponContext ctx) {
        throw new IllegalStateException("券已使用,不能过期");
    }
}

业务实体:

java 复制代码
public class Coupon {
    private CouponStatus status;
    private CouponStateMachine stateMachine;

    public void use() {
        stateMachine.current(status).use(new CouponContext(this));
    }
}

4.6 什么时候用 / 不用

  • ✅ 状态 ≥ 3 个,且状态决定行为
  • ✅ 状态之间有明确的流转规则
  • ✅ 状态相关的 if-else 散落在多个方法里
  • ❌ 状态少、行为简单,枚举 + switch 更轻
  • ❌ 状态流转很复杂,建议直接用状态机框架(如 Spring StateMachine、Cola StateMachine)

4.7 坑点

  1. 状态类数量容易爆炸:N 个状态 × M 个操作 = N×M 个方法,要提前评估。
  2. 状态流转要集中管理:不要在每个状态类里散落流转逻辑,容易乱。
  3. 枚举 + switch 也是一种选择:状态少、行为简单时,不必强上状态模式。
  4. 状态模式 vs 状态机框架:状态模式适合简单状态流转,复杂流程建议上框架。

五、适配器 Adapter

5.1 一句话定义

外部接口改不了,内部接口不想动,中间加一层翻译。

5.2 痛点场景

对接多个券供应商:

  • 自营券:RPC 接口 SelfCouponService.query(code)
  • 美团券:HTTP 接口 GET /meituan/coupon/detail?code=xxx
  • 抖音券:HTTP 接口 POST /douyin/coupon/query,参数是 JSON

内部业务代码只想调一个统一接口:

java 复制代码
VendorCoupon coupon = vendorClient.query(code);

5.3 抽象心法

外部世界不可控,内部世界不想动,中间加一层翻译。

判断标准:多个外部系统提供"类似但不同"的接口,内部希望统一调用 → 适配器模式。

适配器的本质是隔离变化:把外部差异挡在边界之外。

5.4 结构图

#mermaid-svg-Gq1m2cYs5CgRQErc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Gq1m2cYs5CgRQErc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Gq1m2cYs5CgRQErc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Gq1m2cYs5CgRQErc .error-icon{fill:#552222;}#mermaid-svg-Gq1m2cYs5CgRQErc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Gq1m2cYs5CgRQErc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Gq1m2cYs5CgRQErc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Gq1m2cYs5CgRQErc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Gq1m2cYs5CgRQErc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Gq1m2cYs5CgRQErc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Gq1m2cYs5CgRQErc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Gq1m2cYs5CgRQErc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Gq1m2cYs5CgRQErc .marker.cross{stroke:#333333;}#mermaid-svg-Gq1m2cYs5CgRQErc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Gq1m2cYs5CgRQErc p{margin:0;}#mermaid-svg-Gq1m2cYs5CgRQErc .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Gq1m2cYs5CgRQErc .cluster-label text{fill:#333;}#mermaid-svg-Gq1m2cYs5CgRQErc .cluster-label span{color:#333;}#mermaid-svg-Gq1m2cYs5CgRQErc .cluster-label span p{background-color:transparent;}#mermaid-svg-Gq1m2cYs5CgRQErc .label text,#mermaid-svg-Gq1m2cYs5CgRQErc span{fill:#333;color:#333;}#mermaid-svg-Gq1m2cYs5CgRQErc .node rect,#mermaid-svg-Gq1m2cYs5CgRQErc .node circle,#mermaid-svg-Gq1m2cYs5CgRQErc .node ellipse,#mermaid-svg-Gq1m2cYs5CgRQErc .node polygon,#mermaid-svg-Gq1m2cYs5CgRQErc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Gq1m2cYs5CgRQErc .rough-node .label text,#mermaid-svg-Gq1m2cYs5CgRQErc .node .label text,#mermaid-svg-Gq1m2cYs5CgRQErc .image-shape .label,#mermaid-svg-Gq1m2cYs5CgRQErc .icon-shape .label{text-anchor:middle;}#mermaid-svg-Gq1m2cYs5CgRQErc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Gq1m2cYs5CgRQErc .rough-node .label,#mermaid-svg-Gq1m2cYs5CgRQErc .node .label,#mermaid-svg-Gq1m2cYs5CgRQErc .image-shape .label,#mermaid-svg-Gq1m2cYs5CgRQErc .icon-shape .label{text-align:center;}#mermaid-svg-Gq1m2cYs5CgRQErc .node.clickable{cursor:pointer;}#mermaid-svg-Gq1m2cYs5CgRQErc .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Gq1m2cYs5CgRQErc .arrowheadPath{fill:#333333;}#mermaid-svg-Gq1m2cYs5CgRQErc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Gq1m2cYs5CgRQErc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Gq1m2cYs5CgRQErc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Gq1m2cYs5CgRQErc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Gq1m2cYs5CgRQErc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Gq1m2cYs5CgRQErc .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Gq1m2cYs5CgRQErc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Gq1m2cYs5CgRQErc .cluster text{fill:#333;}#mermaid-svg-Gq1m2cYs5CgRQErc .cluster span{color:#333;}#mermaid-svg-Gq1m2cYs5CgRQErc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Gq1m2cYs5CgRQErc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Gq1m2cYs5CgRQErc rect.text{fill:none;stroke-width:0;}#mermaid-svg-Gq1m2cYs5CgRQErc .icon-shape,#mermaid-svg-Gq1m2cYs5CgRQErc .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Gq1m2cYs5CgRQErc .icon-shape p,#mermaid-svg-Gq1m2cYs5CgRQErc .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Gq1m2cYs5CgRQErc .icon-shape .label rect,#mermaid-svg-Gq1m2cYs5CgRQErc .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Gq1m2cYs5CgRQErc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Gq1m2cYs5CgRQErc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Gq1m2cYs5CgRQErc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 依赖
实现
实现
实现
HTTP
HTTP
RPC
内部业务代码
CouponVendorClient

内部统一接口
美团适配器
抖音适配器
自营适配器
美团 API
抖音 API
自营服务

5.5 代码示例

内部统一接口:

java 复制代码
public interface CouponVendorClient {
    VendorCoupon query(String couponCode);
    boolean lock(String couponCode, String orderNo);
}

美团适配器:

java 复制代码
@Component
public class MeituanCouponAdapter implements CouponVendorClient {

    private final MeituanHttpClient httpClient;

    public VendorCoupon query(String couponCode) {
        MeituanResponse resp = httpClient.get("/meituan/coupon/detail", couponCode);
        return convert(resp);
    }

    public boolean lock(String couponCode, String orderNo) {
        MeituanResponse resp = httpClient.post("/meituan/coupon/lock", couponCode, orderNo);
        return resp.isSuccess();
    }

    private VendorCoupon convert(MeituanResponse resp) {
        // 字段映射、异常处理、超时处理
        return VendorCoupon.builder()
                .code(resp.getCouponId())
                .amount(resp.getAmount())
                .status(mapStatus(resp.getStatus()))
                .build();
    }
}

业务代码只依赖 CouponVendorClient,完全不知道底下是美团还是抖音。

5.6 什么时候用 / 不用

  • ✅ 对接多个外部系统,接口语义相似但格式不同
  • ✅ 希望内部代码不感知外部实现
  • ✅ 需要统一异常、超时、日志、埋点
  • ❌ 只有一个外部系统,且接口稳定,直接调用即可
  • ❌ 外部接口差异太大,硬适配会导致适配器成为"万能转换器"

5.7 坑点

  1. 适配器不是简单转发:字段映射、异常转换、超时处理、幂等都要在适配器里做。
  2. 适配器要处理外部异常:外部抛的异常不能直接透传到业务层。
  3. 适配器要设置超时:不要依赖外部系统自己超时。
  4. 适配器要幂等:外部重试、网络抖动时,适配器要保证幂等。
  5. 适配器 + 工厂 + 策略:多种供应商时,通常用工厂选适配器,用策略处理业务。

六、把它们串起来

回到大促领券场景,这 5 个模式是如何协作的:
#mermaid-svg-1oDCWN2z3EMzRoNk{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1oDCWN2z3EMzRoNk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1oDCWN2z3EMzRoNk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1oDCWN2z3EMzRoNk .error-icon{fill:#552222;}#mermaid-svg-1oDCWN2z3EMzRoNk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1oDCWN2z3EMzRoNk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1oDCWN2z3EMzRoNk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1oDCWN2z3EMzRoNk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1oDCWN2z3EMzRoNk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1oDCWN2z3EMzRoNk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1oDCWN2z3EMzRoNk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1oDCWN2z3EMzRoNk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1oDCWN2z3EMzRoNk .marker.cross{stroke:#333333;}#mermaid-svg-1oDCWN2z3EMzRoNk svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1oDCWN2z3EMzRoNk p{margin:0;}#mermaid-svg-1oDCWN2z3EMzRoNk .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1oDCWN2z3EMzRoNk .cluster-label text{fill:#333;}#mermaid-svg-1oDCWN2z3EMzRoNk .cluster-label span{color:#333;}#mermaid-svg-1oDCWN2z3EMzRoNk .cluster-label span p{background-color:transparent;}#mermaid-svg-1oDCWN2z3EMzRoNk .label text,#mermaid-svg-1oDCWN2z3EMzRoNk span{fill:#333;color:#333;}#mermaid-svg-1oDCWN2z3EMzRoNk .node rect,#mermaid-svg-1oDCWN2z3EMzRoNk .node circle,#mermaid-svg-1oDCWN2z3EMzRoNk .node ellipse,#mermaid-svg-1oDCWN2z3EMzRoNk .node polygon,#mermaid-svg-1oDCWN2z3EMzRoNk .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1oDCWN2z3EMzRoNk .rough-node .label text,#mermaid-svg-1oDCWN2z3EMzRoNk .node .label text,#mermaid-svg-1oDCWN2z3EMzRoNk .image-shape .label,#mermaid-svg-1oDCWN2z3EMzRoNk .icon-shape .label{text-anchor:middle;}#mermaid-svg-1oDCWN2z3EMzRoNk .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1oDCWN2z3EMzRoNk .rough-node .label,#mermaid-svg-1oDCWN2z3EMzRoNk .node .label,#mermaid-svg-1oDCWN2z3EMzRoNk .image-shape .label,#mermaid-svg-1oDCWN2z3EMzRoNk .icon-shape .label{text-align:center;}#mermaid-svg-1oDCWN2z3EMzRoNk .node.clickable{cursor:pointer;}#mermaid-svg-1oDCWN2z3EMzRoNk .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1oDCWN2z3EMzRoNk .arrowheadPath{fill:#333333;}#mermaid-svg-1oDCWN2z3EMzRoNk .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1oDCWN2z3EMzRoNk .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1oDCWN2z3EMzRoNk .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1oDCWN2z3EMzRoNk .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1oDCWN2z3EMzRoNk .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1oDCWN2z3EMzRoNk .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1oDCWN2z3EMzRoNk .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1oDCWN2z3EMzRoNk .cluster text{fill:#333;}#mermaid-svg-1oDCWN2z3EMzRoNk .cluster span{color:#333;}#mermaid-svg-1oDCWN2z3EMzRoNk div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1oDCWN2z3EMzRoNk .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1oDCWN2z3EMzRoNk rect.text{fill:none;stroke-width:0;}#mermaid-svg-1oDCWN2z3EMzRoNk .icon-shape,#mermaid-svg-1oDCWN2z3EMzRoNk .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1oDCWN2z3EMzRoNk .icon-shape p,#mermaid-svg-1oDCWN2z3EMzRoNk .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1oDCWN2z3EMzRoNk .icon-shape .label rect,#mermaid-svg-1oDCWN2z3EMzRoNk .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1oDCWN2z3EMzRoNk .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1oDCWN2z3EMzRoNk .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1oDCWN2z3EMzRoNk :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 领券请求 receiveCoupon
模板方法

固定流程骨架
工厂

根据活动类型选策略
策略

计算优惠金额
状态

券的生命周期管理
适配器

统一外部供应商接口
美团 API
抖音 API
自营 RPC

  • 模板方法:定义领券的固定流程
  • 策略:负责优惠计算的可变算法
  • 工厂:根据活动类型选择策略
  • 状态:管理券的生命周期
  • 适配器:隔离外部供应商差异

这 5 个模式不是孤立的,而是各管一段变化点


七、决策表

你遇到的场景 该用的模式
流程固定,步骤有差异 模板方法
同一件事,多种算法 策略
根据类型 new 不同对象 工厂
对象行为随状态变化 状态
对接多个外部接口,内部想统一 适配器
多个 if-else 分支做"同一类事" 策略 + 工厂
状态相关 if-else 散落多处 状态
流程 + 步骤差异 + 类型选择 模板方法 + 策略 + 工厂

八、坑点汇总

  1. 不要为了模式而模式:两个分支的 if-else 比策略模式更清晰。
  2. 策略和工厂是一对:策略负责算法,工厂负责选择,分开写更清晰。
  3. 状态和策略容易混:策略是调用方选,状态是对象自己流转。
  4. 模板方法不要过度分层:钩子超过 3 个,考虑拆分。
  5. 适配器要处理异常和超时:不是简单转发,要统一边界。
  6. 优先用 Spring 容器List<T> 注入 + @Component 已经能替代大部分工厂。
  7. 模式是手段,不是目的:真正的目标是可读、可测、可维护。

九、总结

  1. 模板方法:父类定流程,子类填步骤。判断标准:流程固定、步骤可变。
  2. 策略:同一件事多种算法。判断标准:多个 if-else 做同一类事。
  3. 工厂:把选择逻辑收口。判断标准:根据 type new 不同对象。
  4. 状态:行为随状态变化。判断标准:状态相关 if-else 散落多处。
  5. 适配器:隔离外部差异。判断标准:多个外部系统,内部想统一调用。

一句话:

设计模式的本质,是识别"不变"和"变化",把变化隔离出去。


十、课后思考题

  1. 模板方法和策略都涉及"可变步骤",它们分别适合什么场景?能不能一起用?
  2. 策略模式和状态模式的核心区别是什么?为什么状态模式通常需要状态之间互相知道?
  3. 工厂模式在 Spring 环境下,为什么用 @Autowired List<T> 比手写工厂更简单?
  4. 适配器模式除了字段映射,还需要处理哪些"边界问题"?为什么这些不能放到业务代码里?
  5. 如果一个业务既有"流程可变"又有"算法可变"又有"状态流转",你会怎么组合这 5 个模式?
  6. 什么情况下,用枚举 + switch 比状态模式更合适?两者的取舍标准是什么?

相关推荐
程序员老赵1 小时前
Docker 部署 XXL-JOB:轻松搭建分布式任务调度平台
后端·docker·开源
用户233376852182 小时前
Docker镜像找不到排障实录-CRLF跨平台暗雷
后端
程序员cxuan2 小时前
从 0 到 1 速通 WorkBuddy
人工智能·后端·程序员
薛定谔的算法2 小时前
M01:环境搭建与第一个 Java 程序
后端
盗_心i2 小时前
OCPP 1.6-J 配置管理手册:GetConfiguration / ChangeConfiguration 与标准配置键
后端
传奇开心果编程2 小时前
【Go入门练中学】第1课:从零开始
后端·学习·golang
isfox2 小时前
Python 的 @classmethod 和 @staticmethod:什么时候用哪个,一次说清楚
后端
薛定谔的算法2 小时前
前端视角秒懂:Java 和 Spring Boot 的关系,就像 JS 和 React
后端
梁辰兴3 小时前
软件工程:逆向工程
软件工程·逆向工程·应用场景·梁辰兴·抽象层次·工程过程·方法工具