一、宏观:为什么要写这篇

1. 一个高频需求

  做计费系统时,我遇到最多的一类需求,长这样:

"在现有计费逻辑上,加一个××规则。"

  加折扣、加阶梯、加渠道特惠……每次来需求,本质都是同一件事:给已有的"算钱"流程,动态地加一步加工。

  功能不是永远固定的,是按需装配的。这听起来不复杂,但做着做着会发现——"动态加功能"这件事,比想象的要僵硬得多。

2. 走过的弯路

  最早用条件判断:在现有的逻辑里加判断,渠道A走XX逻辑,渠道B走YY逻辑的时候。后来需求越来越多,加判断加的越来越多,有时甚至还改公共方法。哎,开发改的七上八下,测试测的小心翼翼,领导听了胆战心惊。

  后来学了开闭原则,我打算改用继承——每个渠道一个类,不动旧代码,这样只用测试新逻辑,原逻辑完全不受影响。但很快发现:每个子类都是"核心+装饰"的混合体,想动态加减功能只能再建子类。

3. 顿悟:装饰模式,把功能搬移出去

  直到看到装饰模式,原文里一句话击中了我:

"把类中的装饰功能从类中搬移去除,这样可以简化原有的类。"

  核心思想就一句:装饰模式把"动态添加功能"从核心类里剥离了出来。

  基础类:只管核心职责,干净纯粹。

  装饰器:每个单独成类,只做一件事——接收前序结果、加工、返回。

  调用方:像穿衣服一样,按需包装。想加功能就包一层,想减就少包一层

// 渠道A:全功能
BillingStep a = new DiscountMode1Decorator(
                    new TieredDecorator(new BasicBilling()));

// 渠道C:无阶梯,少包一层
BillingStep c = new DiscountFixedDecorator(new BasicBilling());

零个新类。核心类始终没被污染,装饰功能彼此独立、可插拔。

  那一刻我真正理解了那句话——"西装想穿就穿,想脱就脱,马甲想套就套,想摘就摘。"装饰模式不是关于顺序的,是关于"核心职责与装饰逻辑的彻底分离"。

4. 这篇博客要做什么

  从计费场景出发,先展示没有装饰模式时的两种写法,再展示装饰模式怎么解决它,最后深入到关键代码和设计细节。


二、中观:从痛点走到装饰模式

5. 场景与朴素写法

  三个渠道的需求:

渠道

流程

渠道A

基础费用 → 阶梯定价 → 折扣模式一(打9折)

渠道B

基础费用 → 折扣模式二(满100减20)→ 阶梯定价

渠道C

基础费用 → 折扣写死(减5块,无阶梯)

  注意渠道B的顺序和其他两个不同——先打折再阶梯。这种"顺序差异"在真实业务中很常见,也是继承写法最头疼的地方。

  朴素写法——条件判断:

public BigDecimal calculate(Usage usage, String channel) {
    BigDecimal cost = basicBilling(usage);
    
    if ("CHANNEL_A".equals(channel)) {
        cost = tieredPricing(usage, cost);
        cost = discountMode1(cost);
    } else if ("CHANNEL_B".equals(channel)) {
        cost = discountMode2(cost);
        cost = tieredPricing(usage, cost);
    } else if ("CHANNEL_C".equals(channel)) {
        cost = discountFixed(cost);
    }
    
    return cost;
}

问题:每加一个渠道改这个方法,三种关注点搅在一起,方法越滚越大。而且渠道B的顺序不同,分支里的调用顺序也得跟着调——顺序被硬编码在方法逻辑里。

6. 继承写法

基础类:

class BasicBilling {
    public BigDecimal calculate(Usage usage) {
        return usage.getBasePrice()
                     .multiply(BigDecimal.valueOf(usage.getHours()));
    }
}

渠道A:

class ChannelABilling extends BasicBilling {
    @Override
    public BigDecimal calculate(Usage usage) {
        BigDecimal cost = super.calculate(usage);
        cost = tieredPricing(usage, cost);
        cost = discountMode1(cost);
        return cost;
    }
}

渠道B(顺序不同):

class ChannelBBilling extends BasicBilling {
    @Override
    public BigDecimal calculate(Usage usage) {
        BigDecimal cost = super.calculate(usage);
        cost = discountMode2(cost);
        cost = tieredPricing(usage, cost);
        return cost;
    }
}

渠道C(无阶梯):

class ChannelCBilling extends BasicBilling {
    @Override
    public BigDecimal calculate(Usage usage) {
        BigDecimal cost = super.calculate(usage);
        cost = discountFixed(cost);
        return cost;
    }
}

比条件判断好——新增渠道不改旧类。但问题仍在:

  • 阶梯逻辑在 ChannelA 和 ChannelB 里重复
  • 渠道B顺序不同,没法复用 ChannelA 的"先阶梯再折扣"逻辑,得重写一套
  • 每个子类都是"核心+装饰"混合体

  一句话:继承守住了开闭,但把装饰逻辑焊进了类结构里,顺序也被固化了。

7. 装饰模式

统一接口:

interface BillingStep {
    BigDecimal calculate(Usage usage);
}

基础类(无装饰):

class BasicBilling implements BillingStep {
    @Override
    public BigDecimal calculate(Usage usage) {
        return usage.getBasePrice()
                     .multiply(BigDecimal.valueOf(usage.getHours()));
    }
}

抽象装饰器:

abstract class BillingDecorator implements BillingStep {
    protected BillingStep step;
    
    public BillingDecorator(BillingStep step) {
        this.step = step;
    }
}

具体装饰器(各自独立):

class TieredDecorator extends BillingDecorator {
    public TieredDecorator(BillingStep step) { super(step); }
    
    @Override
    public BigDecimal calculate(Usage usage) {
        BigDecimal cost = step.calculate(usage);
        if (usage.getHours() > 100) {
            cost = cost.multiply(new BigDecimal("0.8"));
        }
        return cost;
    }
}

class DiscountMode1Decorator extends BillingDecorator {
    public DiscountMode1Decorator(BillingStep step) { super(step); }
    
    @Override
    public BigDecimal calculate(Usage usage) {
        BigDecimal cost = step.calculate(usage);
        return cost.multiply(new BigDecimal("0.9"));
    }
}

class DiscountMode2Decorator extends BillingDecorator {
    public DiscountMode2Decorator(BillingStep step) { super(step); }
    
    @Override
    public BigDecimal calculate(Usage usage) {
        BigDecimal cost = step.calculate(usage);
        if (cost.compareTo(new BigDecimal("100")) >= 0) {
            return cost.subtract(new BigDecimal("20"));
        }
        return cost;
    }
}

class DiscountFixedDecorator extends BillingDecorator {
    public DiscountFixedDecorator(BillingStep step) { super(step); }
    
    @Override
    public BigDecimal calculate(Usage usage) {
        BigDecimal cost = step.calculate(usage);
        return cost.subtract(new BigDecimal("5"));
    }
}

组装(按需包装,顺序由 new 的嵌套决定):

// 渠道A:基础 → 阶梯 → 折扣
BillingStep channelA = new DiscountMode1Decorator(
                           new TieredDecorator(new BasicBilling()));

// 渠道B:基础 → 折扣 → 阶梯
BillingStep channelB = new TieredDecorator(
                           new DiscountMode2Decorator(new BasicBilling()));

// 渠道C:基础 → 折扣(无阶梯)
BillingStep channelC = new DiscountFixedDecorator(new BasicBilling());

内层(先执行)

外层(后执行)

渠道A

阶梯

折扣

渠道B

折扣

阶梯

同样的装饰器,只是 new 的嵌套顺序不同,结果就不同。顺序不在类里,在组装里。

  类图(静态结构):

时序图(运行时调用链,以渠道A为例):

8. 三种写法对比

维度

条件判断

继承

装饰模式

新增渠道

改旧方法,加分支

新建类,不改旧类

新建装饰器或仅换组装

装饰逻辑复用

靠方法调用

靠 super + 重复代码

每个装饰器独立,天然复用

动态加减功能

改 if 分支

新建子类

换一行 new,不改任何类

核心类干净度

被污染

没被改,但子类被污染

始终干净,装饰逻辑全在外围

排列组合场景

方法爆炸

类爆炸

N 个装饰器任意拼

三、微观:关键代码与设计细节

9. 完整可运行示例

import java.math.BigDecimal;
import java.math.RoundingMode;

class Usage {
    private BigDecimal basePrice;
    private long hours;

    public Usage(BigDecimal basePrice, long hours) {
        this.basePrice = basePrice;
        this.hours = hours;
    }

    public BigDecimal getBasePrice() { return basePrice; }
    public long getHours() { return hours; }
}

interface BillingStep {
    BigDecimal calculate(Usage usage);
}

class BasicBilling implements BillingStep {
    @Override
    public BigDecimal calculate(Usage usage) {
        BigDecimal cost = usage.getBasePrice()
                .multiply(BigDecimal.valueOf(usage.getHours()));
        System.out.println("  [BasicBilling] 基础费用 = " + cost);
        return cost;
    }
}

abstract class BillingDecorator implements BillingStep {
    protected BillingStep step;

    public BillingDecorator(BillingStep step) {
        this.step = step;
    }
}

class TieredDecorator extends BillingDecorator {
    public TieredDecorator(BillingStep step) { super(step); }

    @Override
    public BigDecimal calculate(Usage usage) {
        BigDecimal cost = step.calculate(usage);
        if (usage.getHours() > 100) {
            BigDecimal extraHours = BigDecimal.valueOf(usage.getHours() - 100);
            BigDecimal extraCost = usage.getBasePrice()
                    .multiply(extraHours)
                    .multiply(new BigDecimal("0.8"));
            BigDecimal basePart = usage.getBasePrice()
                    .multiply(BigDecimal.valueOf(100));
            cost = basePart.add(extraCost);
            System.out.println("  [TieredDecorator] 阶梯定价后 = " + cost);
        } else {
            System.out.println("  [TieredDecorator] 未达阶梯阈值,不调整 = " + cost);
        }
        return cost.setScale(2, RoundingMode.HALF_UP);
    }
}

class DiscountMode1Decorator extends BillingDecorator {
    public DiscountMode1Decorator(BillingStep step) { super(step); }

    @Override
    public BigDecimal calculate(Usage usage) {
        BigDecimal cost = step.calculate(usage);
        BigDecimal result = cost.multiply(new BigDecimal("0.9"));
        System.out.println("  [DiscountMode1] 打9折后 = " + result);
        return result.setScale(2, RoundingMode.HALF_UP);
    }
}

class DiscountMode2Decorator extends BillingDecorator {
    public DiscountMode2Decorator(BillingStep step) { super(step); }

    @Override
    public BigDecimal calculate(Usage usage) {
        BigDecimal cost = step.calculate(usage);
        BigDecimal result;
        if (cost.compareTo(new BigDecimal("100")) >= 0) {
            result = cost.subtract(new BigDecimal("20"));
            System.out.println("  [DiscountMode2] 满100减20后 = " + result);
        } else {
            result = cost;
            System.out.println("  [DiscountMode2] 未满100,不减 = " + result);
        }
        return result.setScale(2, RoundingMode.HALF_UP);
    }
}

class DiscountFixedDecorator extends BillingDecorator {
    public DiscountFixedDecorator(BillingStep step) { super(step); }

    @Override
    public BigDecimal calculate(Usage usage) {
        BigDecimal cost = step.calculate(usage);
        BigDecimal result = cost.subtract(new BigDecimal("5"));
        System.out.println("  [DiscountFixed] 固定减5后 = " + result);
        return result.setScale(2, RoundingMode.HALF_UP);
    }
}

public class Main {
    public static void main(String[] args) {
        Usage usage = new Usage(new BigDecimal("10"), 120);

        System.out.println("===== 渠道A:基础 + 阶梯 + 折扣模式一 =====");
        BillingStep channelA = new DiscountMode1Decorator(
                new TieredDecorator(new BasicBilling()));
        System.out.println("最终结果: " + channelA.calculate(usage));
        System.out.println();

        System.out.println("===== 渠道B:基础 + 折扣模式二 + 阶梯(先折扣再阶梯)=====");
        BillingStep channelB = new TieredDecorator(
                new DiscountMode2Decorator(new BasicBilling()));
        System.out.println("最终结果: " + channelB.calculate(usage));
        System.out.println();

        System.out.println("===== 渠道C:基础 + 折扣写死(无阶梯)=====");
        BillingStep channelC = new DiscountFixedDecorator(new BasicBilling());
        System.out.println("最终结果: " + channelC.calculate(usage));
    }
}

运行输出:

===== 渠道A:基础 + 阶梯 + 折扣模式一 =====
  [BasicBilling] 基础费用 = 1200
  [TieredDecorator] 阶梯定价后 = 1360.00
  [DiscountMode1] 打9折后 = 1224.00
最终结果: 1224.00

===== 渠道B:基础 + 折扣模式二 + 阶梯(先折扣再阶梯)=====
  [BasicBilling] 基础费用 = 1200
  [DiscountMode2] 满100减20后 = 1180.00
  [TieredDecorator] 阶梯定价后 = 1344.00
最终结果: 1344.00

===== 渠道C:基础 + 折扣写死(无阶梯)=====
  [BasicBilling] 基础费用 = 1200
  [DiscountFixed] 固定减5后 = 1195.00
最终结果: 1195.00

10. 细节一:装饰模式的关键代码

  ① 统一接口

interface BillingStep {
    BigDecimal calculate(Usage usage);
}

所有参与者实现同一个接口。调用方只认接口,不认具体类型。

  ② 抽象装饰器持有接口引用

abstract class BillingDecorator implements BillingStep {
    protected BillingStep step;

    public BillingDecorator(BillingStep step) {
        this.step = step;
    }
}

  protected BillingStep step 做了两件事:类型统一(编译期不绑定具体实现类)+ 组合关系(靠持有同接口对象实现功能叠加)。

  ③ 具体装饰器的标准写法

class DiscountMode1Decorator extends BillingDecorator {
    public DiscountMode1Decorator(BillingStep step) { super(step); }

    @Override
    public BigDecimal calculate(Usage usage) {
        BigDecimal cost = step.calculate(usage);
        return cost.multiply(new BigDecimal("0.9"));
    }
}

三步规范:调 step.calculate() 拿前序结果 → 叠加自己的逻辑 → return。不调 step.calculate() 就是截断链,不是装饰器。

  ④ 组装

BillingStep channelA = new DiscountMode1Decorator(
                           new TieredDecorator(new BasicBilling()));

顺序由 new 的嵌套决定。换序 = 换嵌套,不碰任何类。

11. 细节二:编译期 vs 运行期的真相

BillingStep channelA = new DiscountMode1Decorator(
                           new TieredDecorator(new BasicBilling()));

  编译期:

位置

编译器知道什么

编译器不知道什么

BillingStep channelA

声明类型是接口 BillingStep

不知道运行时实际指向哪个类

BillingDecorator.step

字段类型是 BillingStep

不知道构造时传进来的是谁

step.calculate(usage)

校验接口里确实有 calculate 方法

不知道实际调的是哪个实现

  编译器只做了签名校验。它不关心、也无法关心 step 运行时到底指向 TieredDecorator 还是 BasicBilling

  运行期:

  JVM 执行到 step.calculate(usage) 时,查虚方法表,根据实际对象的类型找到真正的实现:

第一次调用(在 DiscountMode1Decorator 里):
  step 实际指向 TieredDecorator → 调 TieredDecorator.calculate()

第二次调用(在 TieredDecorator 里):
  step 实际指向 BasicBilling → 调 BasicBilling.calculate()

每一层的具体类型,只有代码执行到这里时才确定。

  protected BillingStep step 在编译期只是一纸接口声明。运行时才通过构造注入确定了具体类型。顺序不是写在类结构里的,是写在 new 的嵌套里的——而 new 的嵌套,只是运行期对象图的构建方式。

  一句话总结:继承把类型关系焊死在编译期;装饰模式把类型关系推迟到运行期,靠多态动态绑定。


四、总结

  装饰模式解决的核心问题只有一句话:把"动态添加功能"从核心类里剥离出来。

  回顾一下我走过的路:

  • 没有装饰模式时:条件判断扛不住增长,继承守住了开闭却把装饰逻辑焊死在类结构里——顺序固化、无法复用、核心被污染。
  • 有了装饰模式后:基础类只管核心,装饰器各自独立,调用方按需组装。同样的 BasicBilling、同样的 TieredDecorator,换个嵌套顺序就是不同的业务渠道。零个新类,功能自由穿脱。

  如果你也在做计费、做规则引擎、做任何"流程可编排、功能需动态装配"的系统,希望这篇能帮你少走我走过的弯路。后面还有博客会讲装饰模式基础篇哦~~~~~~~~


写在最后:

1、这篇文章初学者读起来可能有点吃力。可以先看类图,把三.9里面的代码跑一下,debug 一下;

2、看不懂也不要着急,下一篇我会从装饰模式基本概念开始讲。因为昨天把装饰模式读完引起了我强烈的共鸣(以前思考的有多痛,这里就有多大的共鸣),我太兴奋了,想快点结合之前工作的场景总结下来。

这个 demo 是我改编过的,可能改编的不太好,后面入门篇我还会举例子。但还是那句话:真实场景的复杂度比这高多了。

3、很感谢米老师,感谢以前的工作经验,感谢自己的思考,感谢 AI。米老师最近提起了装饰模式,我又回过头来看,看完发现能很大程度上解决以前我在工作中的痛点;昨天我在笔记中写下一句话:“没有一次思考是白费的,在XX遇到的那些曾经我觉得难搞的场景,我百思不得其解的场景,经过再次学习之后,我悟了”。现在有了 AI,我感到轻松了很多,比以前百度的方式高效了很多。我觉得现在我只悟到装饰模式的一点点,我觉得还能继续悟。

Logo

一站式 AI 云服务平台

更多推荐