设计模式 08 · 装饰器模式

发布时间:2026/9/24 19:29:39

设计模式 08 · 装饰器模式 上一篇代理模式结尾,我们埋了个钩子:代理和装饰器模式(Decorator)代码结构几乎一样,但意图相反——代理管控制,装饰器管增强。这一篇就来把装饰器讲透,顺便从另一头把这条界限夯实。装饰器要解决的问题,一句话:在不修改原对象、也不靠继承的前提下,动态地给对象叠加新功能。关键词是动态和叠加——你可以在运行时,像穿衣服一样,给一个对象一层层地包上新能力,想加哪层加哪层,顺序随意,还能自由组合。它最形象的比喻就是洋葱:核心是那个原始对象,外面一层层包裹着各种装饰,每一层都在前一层的基础上增强一点点。这一篇我们用订单价格计算这个再典型不过的场景切入:一个订单的最终价格,可能要在原价基础上叠加会员折扣、再叠加满减、再叠加优惠券……而且不同订单叠加的组合千变万化。你会看到,如果用继承来应对这种任意组合,会立刻陷入类爆炸的泥潭,而装饰器用组合优雅地化解了它——这正是第一篇合成复用原则(组合优于继承)最好的一个活教材。最后我们还会看 Java IO 流这个装饰器的教科书级应用,并把装饰器和代理彻底辨析清楚。这篇文章按这条线索展开:先看价格叠加用继承会怎样类爆炸;再引出装饰器,用洋葱式包裹优雅解决;然后讲清它的四个角色和骨架;接着剖析它为什么是组合优于继承的最佳范例;再看 Java IO 这个最经典的装饰器实例;最后从另一头辨析装饰器与代理的区别,给出适用边界。贯穿例是订单价格的优惠叠加。目录一个价格叠加引发的类爆炸装饰器模式:像洋葱一样层层包裹四个角色与骨架为什么装饰器是组合优于继承的活教材Java IO:装饰器的教科书级应用装饰器 vs 代理:从另一头把界限夯实什么时候用装饰器一、一个价格叠加引发的类爆炸我们的订单要算最终价格。基础是订单原价,但业务上有一堆可叠加的优惠:会员折扣(会员打 9 折)满减(满 100 减 20)优惠券(立减 10 元)麻烦在于,这些优惠是可以任意组合的:有的订单只用会员折扣,有的会员折扣 满减,有的三个全叠。如果用继承来表达这些组合,你得为每一种组合写一个子类:classOrder{doublegetPrice(){return100;}}classMemberOrderextendsOrder{...}// 只会员折扣classFullReductionOrderextendsOrder{...}// 只满减classCouponOrderextendsOrder{...}// 只优惠券classMemberFullReductionOrderextendsOrder{...}// 会员 满减classMemberCouponOrderextendsOrder{...}// 会员 优惠券classFullReductionCouponOrderextendsOrder{...}// 满减 优惠券classMemberFullReductionCouponOrderextendsOrder{...}// 三个全叠// ... 3 种优惠就已经 7 个组合类了看出灾难了吗?n 种优惠,组合起来就是 2ⁿ − 1 个子类。3 种就 7 个,再加一个限时折扣,立刻变成 15 个,以后每加一种优惠,类的数量翻倍式增长。这就是继承在多维度自由组合面前的死穴——继承是静态的、编译期定死的,它没法表达运行时任意搭配这件事。这正是第一篇讲合成复用原则时预告过的类爆炸。问题的本质是:每种优惠其实是一个独立的、可插拔的增强,它们本不该被继承关系锁死成一个个固定组合。我们需要的是——把每种优惠做成一个独立的装饰,谁想要哪几种,就在运行时把它们一层层套上去。这就是装饰器。二、装饰器模式:像洋葱一样层层包裹装饰器的核心思路:让装饰和被装饰的对象实现同一个接口,装饰持有一个被装饰对象的引用,在调用它的方法时,先(或后)加上自己的增强逻辑,再委托给内部对象。因为装饰本身也实现了同一接口,所以它又可以被下一层装饰继续包裹——层层嵌套,就像洋葱。先定义共同接口和原始对象:publicinterfaceOrder{doublegetPrice();// 算价格}// 原始对象:基础订单publicclassBasicOrderimplementsOrder{privatefinaldoubleprice;publicBasicOrder(doubleprice){this.priceprice;}publicdoublegetPrice(){returnprice;}}关键是一个抽象装饰器,它也实现Order,并持有一个Order:// 抽象装饰器:也是一个 Order,同时包着一个 OrderpublicabstractclassOrderDecoratorimplementsOrder{protectedfinalOrderorder;// 被装饰的对象publicOrderDecorator(Orderorder){this.orderorder;}}然后每种优惠是一个具体装饰器,各自只管自己那一点增强:// 会员折扣:在内部价格上打 9 折publicclassMemberDecoratorextendsOrderDecorator{publicMemberDecorator(Orderorder){super(order);}publicdoublegetPrice(){returnorder.getPrice()*0.9;// 先拿内部价,再叠加自己的逻辑}}// 满减:满 100 减 20publicclassFullReductionDecoratorextendsOrderDecorator{publicFullReductionDecorator(Orderorder){super(order);}publicdoublegetPrice(){doubleporder.getPrice();returnp100?p-20:p;}}// 优惠券:立减 10publicclassCouponDecoratorextendsOrderDecorator{publicCouponDecorator(Orderorder){super(order);}publicdoublegetPrice(){returnorder.getPrice()-10;}}用起来,就是把想要的优惠一层层套上去,像穿衣服:OrderordernewBasicOrder(100);// 原价 100ordernewMemberDecorator(order);// 套上会员折扣 → 90ordernewFullReductionDecorator(order);// 再套满减 → 90(不满100不减)ordernewCouponDecorator(order);// 再套优惠券 → 80System.out.println(order.getPrice());// 80对比第一节的类爆炸,升级点非常清晰:3 种优惠,现在只需要 3 个装饰器类,而不是 7 个组合类;n 种优惠是 n 个类,而不是 2ⁿ 个。而且组合是运行时决定的——想怎么叠就怎么叠,想换顺序就换顺序,全靠new的时候自由搭配,一个新组合都不用新增类。加一种限时折扣?加一个LimitTimeDecorator就行,已有代码一律不动。这就是装饰器的威力。三、四个角色与骨架把上面的代码抽象一下,装饰器模式有四个角色:角色本例中是谁职责抽象构件(Component)Order接口定义对象和装饰共同的接口具体构件(ConcreteComponent)BasicOrder被装饰的原始对象、核心抽象装饰器(Decorator)OrderDecorator实现 Component,持有一个 Component 引用具体装饰器(ConcreteDecorator)MemberDecorator等叠加具体的增强逻辑最关键的一点在抽象装饰器上:它既implements Component(所以它自己也是一个 Component,能被当作 Order 使用、也能被继续包裹),又持有一个 Component 引用(所以它能包住别人)。正是这既是又持有的双重身份,让装饰器能够无限层层嵌套。用一张洋葱图看最直观:这张图揭示了装饰器调用的精髓——调用像穿过洋葱一样由外向内层层深入,结果再由内向外层层加工返回。当你调用最外层CouponDecorator.getPrice()时:它要先知道内层价格,于是调FullReductionDecorator.getPrice(),后者又调MemberDecorator.getPrice(),再调到最核心的BasicOrder.getPrice()返回 100;然后 100 一层层往外走:会员折扣加工成 90、满减看不满 100 不动还是 90、优惠券减 10 变 80。每一层只关心在内层结果上加自己这一点,完全不知道整条链有多长、别的层是谁——这就是低耦合。四、为什么装饰器是组合优于继承的活教材第一篇讲合成复用原则时说过一句话:很多你以为需要继承的场景,其实用组合更好。装饰器就是这句话最完美的证明。我们把两种方案正面对一下账。继承方案(第一节)的问题:类爆炸:每个组合一个子类,2ⁿ 级增长;静态、编译期定死:一个订单一旦new成MemberOrder,它就永远是会员订单,运行时没法再给它加满减;组合关系写死在类里:想调整叠加顺序、动态增减优惠,统统做不到。装饰器方案(组合)的优势,正好逐条对上:类的数量是线性的:n 种增强 n 个装饰器,不随组合数膨胀;运行时动态组合:想叠哪几层、什么顺序,都在运行时用代码决定,极其灵活;符合开闭原则:加新增强 加新装饰器,不碰任何已有代码;每个装饰器职责单一:一个只管会员折扣、一个只管满减,清清爽爽(单一职责)。这里有个值得琢磨的细节:上面的OrderDecorator用了extends(继承)。这不矛盾吗,不是说组合优于继承吗?关键要看继承用在哪:装饰器用继承,只是为了让各个具体装饰器复用持有一个 Order 引用这个共同结构(一种代码复用),而它实现叠加功能这件核心的事,靠的是内部那个order引用——也就是组合。换句话说,装饰器的骨架是组合(持有并委托),继承只是个用来共享字段的辅助。真正让功能能层层叠加的,是组合,不是继承。这也提醒我们:“组合优于继承不是禁用继承”,而是用组合承担核心的能力扩展,继承退居为纯粹的代码复用。五、Java IO:装饰器的教科书级应用如果你觉得订单例子还不够有说服力,那 Java 标准库里有一个所有人天天用、却未必意识到是装饰器的经典——java.io 包。你一定写过这样的代码:// 一层层包起来,是不是和订单优惠叠加一模一样?InputStreaminnewBufferedInputStream(// 第三层:加缓冲newGZIPInputStream(// 第二层:加解压newFileInputStream(data.gz)));// 核心:从文件读这就是标准的装饰器!对照一下角色:抽象构件:InputStream(所有流的共同抽象);具体构件:FileInputStream(真正从文件读字节的核心);抽象装饰器:FilterInputStream(持有一个InputStream);具体装饰器:BufferedInputStream(加缓冲能力)、GZIPInputStream(加解压能力)、DataInputStream(加读取基本类型的能力)……Java IO 为什么要这么设计?因为数据来源(文件、网络、内存、数组)和处理能力(缓冲、加密、压缩、编码转换)是两个可以任意组合的维度。如果用继承,就会出现BufferedFileInputStream、GzipBufferedFileInputStream、BufferedNetworkInputStream……又一次 2ⁿ 类爆炸。而装饰器让你像搭积木一样自由组合:任何数据源,想加缓冲就包一层BufferedInputStream,想加解压就包一层GZIPInputStream,顺序、组合完全自由。理解了装饰器,你才算真正读懂了 java.io 那套套娃式的 API 为什么长这样。六、装饰器 vs 代理:从另一头把界限夯实上一篇从代理那头说过它俩结构相同、意图相反,这一篇我们从装饰器这头再夯一遍,让这条界限彻底清晰。先承认它们的共同点:都实现同一个接口、都持有一个同接口类型的对象、都在方法调用前后插入逻辑再委托给内部对象。光看代码,一个装饰器和一个代理确实可能长得几乎一样。区别在意图和用法上:代理 Proxy装饰器 Decorator意图控制访问(能不能访问、怎么访问)增强功能(叠加新能力)对内部对象的态度代理自己创建/管理真实对象,调用方常不知其存在被装饰对象从外部传入,调用方明确知道自己在包装数量通常一层(一个代理管一个真实对象)通常多层(为了叠加,层层嵌套是常态)关注点真实对象之外的事(日志、权限、事务、远程)真实对象本身能力的增强(缓冲、折扣、压缩)典型场景Spring AOP、RPC、权限校验Java IO、优惠叠加最好记的一条判据是这个:看内部对象是谁给的。装饰器:内部对象是你从外面new好、主动传进去的(new MemberDecorator(order))——你清楚地知道自己在一层层包装,目的是给它加料。层层叠加是装饰器的常态。代理:内部对象通常是代理自己内部持有/创建、你甚至看不到的(Spring 帮你生成代理,真实对象藏在里面)——你要的是访问它这件事被管控,而不是给它加料。只有一层、替你把关是代理的常态。一句话收束:你主动一层层往上包,是装饰器;它替你挡在前面把关,是代理。结构骗不了你,意图才是分水岭。七、什么时候用装饰器老规矩,泼冷水。装饰器很优雅,但它有代价:层层嵌套会产生很多小对象,而且调试时调用栈会变得很深——一个getPrice()要穿过好几层装饰器,排查问题时得一层层跟进去。所以它也不是万能的。适合用装饰器的信号:你需要给对象动态地、可选地、可叠加地添加功能,且这些功能能自由组合;用继承会导致类爆炸(多个独立维度的组合);希望增强逻辑能在运行时灵活搭配,而不是编译期写死。不必用装饰器的信号:功能组合是固定的、就那么一两种——那直接写死或用继承反而更简单,套装饰器是杀鸡用牛刀;你要的其实是控制访问而非增强能力——那是代理的活;增强只有一种、也不需要叠加——直接改或包一层普通方法就行。判断的核心还是那句贯穿全系列的话:先确认真的存在多个可自由组合的增强维度这个事实,装饰器才配得上它的嵌套复杂度。如果优惠就固定一种,硬套一套 Component/Decorator 的四角色结构,就是典型的过度设计。小结。装饰器模式让你在不改原对象、不靠继承的前提下,像洋葱一样给对象动态、可叠加地包裹新功能。它的核心是既是 Component 又持有 Component的双重身份,靠组合(持有并委托)实现层层嵌套,靠这一点优雅地干掉了继承在自由组合面前的 2ⁿ 类爆炸——是组合优于继承最好的活教材。Java IO 那套new BufferedInputStream(new GZIPInputStream(...))的套娃 API,就是它最经典的应用。记住它和代理的分水岭:你主动层层包裹去增强,是装饰器;它替你挡在前面做管控,是代理。下一篇我们讲适配器模式——当你手上的接口和你想用的接口对不上时(比如对接一个第三方物流 API),适配器就是那个转接头。
延伸阅读

更多相关文章

2026/9/24 19:28:07

设计模式 07 · 代理模式

从这一篇开始,我们离开"对象怎么造出来"(创建型),进入结构型模式——它们关心的是另一件事:已经造好的类和对象,该如何组合、连接、搭配,才能既满足需求又保持松耦合。 结构型七个模式,几乎都是在玩"包装"和"组合"的艺术。我们从其中实战出镜率最…

2026/9/21 9:43:09

Reia数据库架构揭秘:Turso如何支持离线与在线数据同步

Reia数据库架构揭秘:Turso如何支持离线与在线数据同步 【免费下载链接】Reia RPG game action-adventure MMO built with Godot and Rust. 项目地址: https://gitcode.com/gh_mirrors/re/Reia Reia作为一款基于Godot和Rust构建的动作冒险MMORPG游戏&#xff…

2026/9/23 8:14:48

深入解析信号量:从并发编程基石到生产者-消费者实战

1. 项目概述:从“抢车位”到“信号量”的认知跃迁如果你写过并发程序,或者调试过多线程的bug,那你一定对“竞态条件”这个词深恶痛绝。想象一个经典的场景:一个公共停车场,只有10个车位。如果没有管理,会发…

2026/9/24 19:26:52

从YOLOV5目录格式到猪圈目标检测:数据准备实战指南

简介:猪圈摄像头场景下的生猪检测数据集,类别为单一pig,按YOLOV5标准目录格式整理,可直接接入现有训练流程,减少数据预处理工作。图像均截取自猪圈监控视频,分辨率覆盖640至1080,每帧包含多个生…

2026/9/24 19:26:52

从零手写线性回归:原理、梯度下降与工程实践完全指南

从零实现一个线性回归模型,是我觉得入门机器学习最值得做的一件小事。很多人一开始就扎进复杂的神经网络、Transformer,结果被概念和数学砸得晕头转向,反而连最基本的“模型是怎么学习”的都没搞明白。这个项目标题里的“性回归模型”&#x…

2026/9/24 19:26:52

JavaWeb图书管理系统源码部署与二次开发实战指南

简介:这套JavaWeb图书管理系统完整项目包,定位于课程设计与期末大作业场景,覆盖图书的查询、借阅、归还等核心功能,既适合JavaWeb初学者对照学习,也可作为二次开发基底。项目共278个文件,其中Java源文件45个…

2026/9/24 19:26:52

Multi-Agent 编排实战:SubAgent 调度、等待机制与框架选型

最近在调 Multi-Agent 系统的时候,正好赶上圈里在刷“cursor waiting for subagent”这个状态提示。很多人一看 waiting 就以为卡死了,其实它背后是一整套 SubAgent 调度过程,只是产品层把它简化成了一个转圈图标。我一度也在 Microsoft Agen…

2026/9/24 19:26:52

前端开发者必补的后端与部署技能选型指南

1. 前端开发者为什么必须补上后端与部署这一课做了六年前端,我越来越强烈地感受到一个事实:只会写页面的人,正在被快速边缘化。这不是贩卖焦虑,而是我这两年带团队、面人、接私活的真实体感。以前一个项目,前端切完图交…

2026/9/24 19:21:52

热点新闻推荐系统毕设全栈实战:Django+Vue+深度学习

每年毕业季,我都会遇到一批被“推荐系统”这个题目吸引的同学,热点新闻推荐系统又是其中最常见的一款。选这个题的人多,但真正能把它做明白的不多——多数人不是不会调模型,而是不知道整个项目该怎么闭环:数据从哪来、…

2026/9/23 12:07:00

GAMP 5 基于风险的计算机化系统验证:软件分类与审计追踪实践

简介:《A Risk-Based Approach to Compliant GxP Computerized Systems》即业内熟知的GAMP 5指南,面向制药企业质量与IT合规人员、验证工程师及计算机化系统管理者,用于解决GxP法规环境下系统合规性难以科学落地的问题。文档以风险管理为主线…

2026/9/23 12:06:55

安全托管MSSP实战:从静态防御到人机协同的攻防运营与应急响应

简介:这份PPT围绕互联网业务安全托管服务展开,面向企业安全负责人、IT运维人员及关注MSSP/MSS选型的读者,重点回应传统安全过度依赖人工、碎片化静态防御难以对抗产业化攻击等痛点。资源共1个pptx文件,包体约30.63MB,以…

2026/9/24 0:00:21

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为…

2026/9/24 0:00:21

单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

简介:一份基于单细胞RNA测序数据的细胞类型注释算法研究Python毕业设计源码,针对计算机相关专业正在做毕设或需要项目实战的学习者,可用于课程设计与期末大作业。项目代码完整、经导师指导评审通过,可直接运行,覆盖数据…

2026/9/24 0:00:21

C#源生成器实战:用增量生成器替代反射,告别AOT崩溃

第一次在项目里被反射卡住,是在一个老旧的WinForms模块里:几十个类依赖PropertyChanged通知,运行时反射读属性、发通知,每次启动慢半拍不说,一上.NET Native/AOT裁剪模式几乎全面崩盘。后来我把这段逻辑全部改成C#源生…

2026/9/22 16:34:32

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/22 20:01:30

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/22 13:25:41

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码