设计模式与生活⑤:天冷了,给电动车套件挡风被

周三早上,闹钟响的时候,天没亮透,窗户缝里钻进来的不是光,是冷。

小王眼睛都没睁开,伸手把闹钟按掉。被窝里多赖了十秒,还是认命地爬起来,从柜子最底下翻出一条秋裤。

套上秋裤,再穿外裤。下楼,从车筐里抽出挡风被,两根绳子往车把上一系,把车头和腿前那片风整个兜住。

电动车还是去年那辆,人也还是昨天那个人。谁都没动,只是外面各多了一层。

车没换,人没换,各多了一层

天冷了不用换车,加一层就行。

今天聊的就是这件事在代码里的写法:装饰器(Decorator)------不改本体,在外面包一层,一层不够就再包一层。系列第五篇。

昨天那篇留了个尾巴:优惠要"叠一堆",策略管不了。今天到公司正好收这个尾------测试昨晚报了笔错账,先排查,再开发。

01 为每一层都建个子类

先看一个不太聪明的写法。天一冷,电动车的"配置"开始变:今天加挡风被,明天换加热把套,后天可能还要加对护手。每样都想要,用继承写:

java 复制代码
// 裸车
class ElectricScooter {
    void go() { System.out.println("骑电动车,十分钟到地铁站"); }
}

// 加被子
class ScooterWithCover extends ElectricScooter { /* ... */ }

// 加被子又加把套
class ScooterWithCoverAndGrips extends ScooterWithCover { /* ... */ }

// 只想要把套、不要被子呢?
class ScooterWithGripsOnly extends ElectricScooter { /* ... */ }

每种组合,都得焊一个类

三样配件,非空的组合是七种。再来第四样,十五种。类不是写不完,是"层"被长成了"本体"------挡风被明明开春就该拆下来,代码里它却焊死在车上。

人这边也一样。"穿了秋裤的人""穿了秋裤又加保暖袜的人"......为了几层布,给人开了个族谱。

外加的东西一旦写进本体,拆下来就得动本体。

02 包一层,别动本体

装饰器的解法就一句话:本体和外加层实现同一个接口,每一层手里都拿着里面那层。

java 复制代码
// 一个接口:能赶路就行
interface Commute {
    void go();
}

// 本体:电动车,一行不改
class ElectricScooter implements Commute {
    public void go() { System.out.println("骑电动车,十分钟到地铁站"); }
}

// 外加层:挡风被
class WindshieldCover implements Commute {
    private final Commute inner;              // ← 手里握着里面那层
    WindshieldCover(Commute inner) { this.inner = inner; }

    public void go() {
        System.out.println("挡风被把风挡在外面");
        inner.go();
    }
}

// 再加一层:加热把套
class HeatedGrips implements Commute {
    private final Commute inner;
    HeatedGrips(Commute inner) { this.inner = inner; }

    public void go() {
        System.out.println("把套先热起来");
        inner.go();
    }
}

三层一个接口,外层管加料,内层管赶路

今天出门,组装就这一行:

java 复制代码
Commute today = new HeatedGrips(new WindshieldCover(new ElectricScooter()));
today.go();
// 把套先热,被子挡风,最后才是车在跑

从外往里包,调用从外往里传

他自己今早也是这么组装的:先秋裤,再外裤,最后外套------只是人这个"接口",不用写代码。

加一层,只加一个类;拆一层,也只删一个类。

层抱着层,本体在中间没动过

这里有个对不上的地方,得先说清楚。现实里这层看得见:穿反了、穿厚了,身体马上有反馈。代码里的层是堆在内存里的引用,包错顺序编译器不吭声,跑起来才出事------上午就出了一件。

03 先排查,那笔账差两块

到公司还没坐稳,测试在群里 @ 他:昨晚那笔订单,120 块的东西,满 100 减 20 再打 9 折,该收 90,实际扣了 88。

差的两块出在顺序。昨晚赶工,叠加写成了"先打折、再满减":120 打 9 折是 108,108 还够着 100,再减 20,88。运营要的是先减再折:(120 − 20) × 0.9,90。

定位不难,难的是这段叠加写在下单流程的正中间。上一篇说过,主干上的判断,碰一次险一次------这次顺手把它抽出来,按今早的思路重写:

java 复制代码
// 一个接口:给个价就行
interface Price {
    BigDecimal value();
}

// 本体:原始金额
class OrderPrice implements Price {
    private final BigDecimal amount;
    OrderPrice(BigDecimal amount) { this.amount = amount; }
    public BigDecimal value() { return amount; }
}

// 满减层
class FullReduction implements Price {
    private final Price inner;
    FullReduction(Price inner) { this.inner = inner; }
    public BigDecimal value() {
        BigDecimal v = inner.value();
        return v.compareTo(new BigDecimal("100")) >= 0
                ? v.subtract(new BigDecimal("20")) : v;
    }
}

// 会员层
class MemberDiscount implements Price {
    private final Price inner;
    MemberDiscount(Price inner) { this.inner = inner; }
    public BigDecimal value() {
        return inner.value().multiply(new BigDecimal("0.9"));
    }
}

每条规则一层,各管各的加减

组装处只剩一行,顺序就写在这一行里:

java 复制代码
// 先减,后折:120 → 100 → 90
Price p = new MemberDiscount(new FullReduction(new OrderPrice(new BigDecimal("120"))));
p.value();

想换成"先折再减"?调一下嵌套,别动任何一层

昨天说的"叠一堆是装饰器的活",今天兑现:每层只管自己那点规则,里面是谁不关心;叠几种、按什么顺序叠,全在组装处。

和策略一样,装饰器也靠"同一个接口"走天下。差别在语义:策略的接口是"选一个",装饰器的接口是"里面还有一个"。

同一笔 120,两种叠法,两个结果

04 JDK 里全是这个

这套"包一层"的写法,JDK 里最经典的例子你八成写过:

java 复制代码
InputStream in = new DataInputStream(
                    new BufferedInputStream(
                        new FileInputStream("a.txt")));

本体读字节,中间层加缓冲,最外层按类型读

FileInputStream 是本体,只管从文件搬字节;BufferedInputStream 包一层,加缓冲;DataInputStream 再包一层,加按类型读。三层共用一个接口,谁也没改谁。

Collections 也是一路:synchronizedList 包一层加线程安全,unmodifiableList 包一层禁掉写。想要什么性质,就往上包什么。

05 什么时候,别乱加层

装饰器的门槛:**有"外加的性质",而且这些性质要自由组合。**如果增强只有一种、十年不变,直接改本体更省事,犯不上为它单开一层。

层也不是越多越好。现实里穿三条秋裤,先迈不开的是腿;代码里套五层,一次调用要在五层之间跳,排查问题得一层层拆。今早那两块钱要是套在五层里,光找顺序就得拆五遍。

顺序敏感,编译器还不管。先穿秋裤再穿外裤,天经地义,没人会搞反;先满减再打折、还是先折再减,编译器不拦你。所以顺序只能写在组装处,别藏进任何一层里。

和策略的分工再说一遍:策略是同一个位置换一个,装饰器是同一个本体叠几层。策略的零件互不认识,装饰器的层一个抱一个。

本体不动,层叠在外。

06 下班,把层裹紧

下班,天黑透了,比早上更冷。

他把挡风被侧面的绳子又勒紧一道,钥匙一拧,汇入非机动车道的灯河。腿前那片风被兜在外面,秋裤顶住剩下那点。

车还是去年那辆车,人还是那个人。谁都没动,只是各多了一层。

这大概是装饰器最像生活的地方:它不改变你是谁,只在你和天气之间,多垫一层。

跟前面四篇对一句:单例管"造几个",工厂管"怎么造",模板方法管"事情怎么走",策略管"这一步用什么做法",装饰器管"在本体外面加几层"。

带走一句话:装饰器 = 同一个接口,外面包一层,本体不拆。

能加一层解决的,别动原来的那一层。


参考资料

相关推荐
昭昭日月明17 小时前
AI 时代的《重构》
设计模式·架构
工一木子1 天前
设计模式与生活③:你的早晨,是一套模板
设计模式
是2的10次方啊1 天前
设计模式与生活④:冰箱空了,早餐换个买法
设计模式
知守观2 天前
设计模式实战:从真实老项目中看标准模板方法与工厂的落地
java·spring·设计模式
多弗朗皮卡丘2 天前
C++特殊类设计
开发语言·c++·设计模式
Carl_奕然2 天前
【智能体】Loop 的四种设计模式之:Verification Loop(2026 最新版)
人工智能·python·设计模式
是2的10次方啊2 天前
设计模式与生活③:你的早晨,是一套模板
设计模式
.Hypocritical.3 天前
JUC + 三大设计模式(单例、工厂、代理)
设计模式·juc·1024程序员节
是2的10次方啊3 天前
设计模式与生活②:你只管点外卖,别管堡怎么做
设计模式