【设计模式】装饰模式(一)实战:从计费系统的“动态加功能“说起
一、宏观:为什么要写这篇
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()));
编译期:
|
位置 |
编译器知道什么 |
编译器不知道什么 |
|---|---|---|
|
|
声明类型是接口 |
不知道运行时实际指向哪个类 |
|
|
字段类型是 |
不知道构造时传进来的是谁 |
|
|
校验接口里确实有 |
不知道实际调的是哪个实现 |
编译器只做了签名校验。它不关心、也无法关心 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,我感到轻松了很多,比以前百度的方式高效了很多。我觉得现在我只悟到装饰模式的一点点,我觉得还能继续悟。
更多推荐




所有评论(0)