[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site-settings":3,"\u002Farticles\u002Fdesign-pattern-10":26,"\u002Farticles\u002Fdesign-pattern-10\u002Frelated":43,"\u002Farticles\u002Fdesign-pattern-10\u002Fadjacent":71,"\u002Fcategories":82,"render-design-pattern-10":108},{"code":4,"message":5,"data":6},0,"ok",{"author_city":7,"author_years":8,"site_tagline":9,"site_description":10,"skill_one_desc":11,"author_status":12,"skill_two_title":13,"about_hero_image":9,"skill_one_title":14,"icp":15,"author_role":16,"og_default_image":9,"closing_quote_date":17,"toolkit":18,"skill_two_desc":19,"author_bio":9,"site_footer":9,"site_url":20,"author_tagline":21,"weekly_thought":9,"tagline_line_one":22,"author_bio_two":9,"closing_quote":23,"site_name":24,"tagline_line_two":25,"home_hero_image":9},"西安","10","","花开空白 - 个人博客","熟悉Java、Python、Go等后端语言。","努力工作中","前端开发","后端开发","陕ICP备2021009682号-1","开发者","2022年·秋","Java,JavaScript,TypeScript,Python,Go","熟悉Js、Ts语言以及Vue、uniapp等框架。","https:\u002F\u002Firule.cn","很高兴遇见你。","把复杂的事，","把每次一发布，都当成一次和陌生人的握手。","花开空白","写成简单的故事。",{"code":4,"message":5,"data":27},{"id":28,"slug":29,"title":30,"summary":31,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":34,"likeCount":4,"wordCount":35,"readingMinutes":36,"publishedAt":37,"contentMd":38,"contentHtml":39,"tags":40,"metaTitle":9,"metaDescription":9,"keywords":9,"canonicalUrl":9,"robots":41,"ogImage":9,"faq":42,"aiSummary":42},"2113931625669595138","design-pattern-10","设计模式修炼之路 ⑩ 装饰器模式——不改原类，动态加功能","不改原类动态加功能、\"我是组件+我有组件\"、Java IO 流的套娃设计","2113897628365230081",1,1003,7847,20,"2024-02-10T15:14:16","上一篇组合模式的结尾给它留了一句判词：\"UML 画出来，几乎是只准养一个孩子的组合模式\"。今天这个\"独生子\"正式登场：**装饰器模式（Decorator Pattern）**，GoF 23 人名单正牌选手、结构型第五位，有的书也叫它\"包装模式（Wrapper）\"。组合模式的容器里装的是一整个 `List\u003CFileComponent>`，孩子越多树越大；装饰器的\"容器\"里只准装**一个**对象，但它自己同时还是组件——于是这棵树不许分叉，只许长个儿：一杯咖啡包一层、再加一层，功能就这么一层层叠上去。它回答的问题直白又高频：**想给一个类加功能，但不想改它的代码、也不想为每种组合造一个子类，怎么办？**\n\n## 一、什么是装饰器模式？\n\n先看定义：\n\n> 动态地给一个对象添加一些额外的职责。就增加功能来说，装饰器模式比生成子类更为灵活。\n\n翻译成人话：**不改原类、不碰继承，把\"要加的功能\"做成一个个小包装类——包装类和原对象实现同一个接口，手里攥着原对象，调用时先干自己的活、再把剩下的活转交给手里那位**。要加什么料，运行时套几层、按什么顺序套，全在 new 的时候现场决定。\n\n生活类比是给手机贴膜：裸机是对象，钢化膜、防窥膜、磨砂膜是装饰器——贴完膜它还是一部手机（接口没变），但抗摔、防窥、不沾指纹这些职责已经加上去了；贴三层就是三层的效果，撕掉一层就少一层功能，手机本身一颗螺丝没动。\n\n它有四个角色：\n\n- **Component（组件接口）**：本篇里是 Coffee，规定\"一杯咖啡\"能干什么；\n- **ConcreteComponent（具体组件）**：被装饰的原始对象，本篇里是 SimpleCoffee——一杯什么都不加的裸咖啡；\n- **Decorator（抽象装饰器）**：实现组件接口，同时**持有一个组件类型的字段**，本篇里是 CondimentDecorator；\n- **ConcreteDecorator（具体装饰器）**：每种\"配料\"一个类，本篇里是 Milk、Sugar、Mocha。\n\n精髓全在抽象装饰器身上，一句话说透：**它自己\"是\"组件，手里又\"有\"组件**。是组件，所以它能出现在任何需要组件的地方——包括另一个装饰器的构造器里，于是装饰器可以套装饰器；有组件，所以它的方法实现基本是\"干自己的事 + 转发给手里那位\"。上一篇 Folder 的孩子是 `List\u003CFileComponent>`，数量不限长成树；这一篇 CondimentDecorator 的孩子是一个 `Coffee`，数量限一长成串——所谓\"只准养一个孩子的组合模式\"，就是从这个字段类型来的。\n\n## 二、装饰器模式的优缺点\n\n### 优点\n\n1. **组合替代继承，彻底躲开子类爆炸**：继承是编译期焊死的静态关系，加一种配料就得多一层类、多一种组合就多一个类；装饰是运行期自由组装的动态关系，n 种配料任意组合、任意顺序、甚至重复包装（双份奶），类的数量却纹丝不动——下面看事故现场。\n2. **职责拆得干净**：每个装饰器只干一件事，Milk 只管加牛奶，BufferedInputStream 只管缓冲——单一职责原则的模范生。\n3. **对扩展开放、对修改关闭**：新增一种配料 = 新写一个装饰器类，SimpleCoffee、Milk、Mocha 全都不用动，已上线的调用链更不用动——开闭原则。\n4. **可拆卸**：不包就是裸对象，包几层就有几层功能，同一套类支持\"裸奔\"到\"满配\"的全部档位。\n\n### 缺点\n\n1. **类数量依然不少**：虽然躲开了指数爆炸，但每种装饰还是一个类，配料多了之后\"小类满天飞\"。\n2. **调用栈深、排障绕**：`new Sugar(new Milk(new Milk(...)))` 出了问题，异常栈和调试断点都要一层层剥洋葱，读代码的人也得在脑子里手动解套娃。\n3. **装饰顺序敏感**：本篇算价格，加法交换律掩盖了顺序问题；但换成\"压缩+加密\"，先压缩再加密和先加密再压缩，产出的东西完全不同——顺序的自由是能力，也是坑。\n4. **初始化代码可读性差**：不熟悉的读者看到一长串 `new X(new Y(new Z(...)))`，第一反应多半是\"这是在干嘛\"。\n\n## 三、装饰器模式的应用场景\n\n- **Java IO 流（JDK）**：全宇宙最著名的装饰器家族，InputStream\u002FOutputStream\u002FReader\u002FWriter 四大家族全按这个模式搭的，第五节专门解剖。\n- **Collections 工具方法（JDK）**：`Collections.unmodifiableList(list)` 返回的其实是一个包着原 list 的装饰器——\"只读\"是它加的职责，原 list 一颗螺丝没动；`synchronizedList()`、`checkedList()` 同理，一个方法一种职责，想叠加就再包一层。\n- **Servlet 的 Request\u002FResponseWrapper**：HttpServletRequestWrapper 包着原生 request，想改写参数、伪造头部、透传额外属性时，包一层 Wrapper 扔给下游，下游毫不知情——Spring MVC、Spring Security 内部大量使用。\n- **MyBatis 的 Cache**：Cache 接口下，PerpetualCache 是裸缓存（具体组件），LruCache、FifoCache、LoggingCache、SynchronizedCache、TransactionalCache……全是装饰器。CachingExecutor 组装缓存时按配置把装饰器一层层套上去，\"容量淘汰 + 日志 + 事务感知\"全靠运行时点单——本系列第四次在 MyBatis 里撞见老朋友了。\n- **Spring 的 TransactionAwareCacheDecorator**：类名直接把模式写脸上了，给任意 Cache 实现\"感知 Spring 事务\"的职责。\n\n**什么时候该用**：功能是\"可叠加的增强\"而不是\"新物种\"（缓冲之于字节流，是增强；文件之于字节流，是来源）；增强方式存在组合的可能；或者原类是别人的、你压根无权改——三条中两条，就该想到它。\n\n## 四、装饰器模式的实现方式\n\n示例代码在 design-pattern 项目的 decorator 包里：\n\n```\ndesign-pattern\u002Fsrc\u002Fmain\u002Fjava\n└── decorator                 \u002F\u002F 装饰器模式\n    ├── PlainCoffee.java      \u002F\u002F 反面教材：裸咖啡，等着被子类爆炸\n    ├── MilkCoffee.java       \u002F\u002F 反面教材：加奶 = 继承一个子类\n    ├── MilkSugarCoffee.java  \u002F\u002F 反面教材：再加糖 = 再继承一个子类\n    ├── Coffee.java           \u002F\u002F 组件接口：一杯咖啡能干什么\n    ├── SimpleCoffee.java     \u002F\u002F 具体组件：什么都不加的裸咖啡\n    ├── CondimentDecorator.java \u002F\u002F 抽象装饰器：持有一杯咖啡，自己也还是一杯咖啡\n    ├── Milk.java             \u002F\u002F 具体装饰器：牛奶\n    ├── Sugar.java            \u002F\u002F 具体装饰器：糖\n    ├── Mocha.java            \u002F\u002F 具体装饰器：摩卡\n    └── DecoratorTest.java    \u002F\u002F 测试入口\n```\n\n场景：咖啡店点单。一杯裸咖啡 15 元，加牛奶 3 元、加糖 2 元、加摩卡 5 元，描述和价格都要随配料变。先看\"一种组合一个子类\"的下场，再看装饰器怎么救。\n\n### 1. 反面教材：继承爆炸\n\n最直觉的写法：要什么组合，就继承出什么子类：\n\n```java\n\u002F\u002F 反面教材：普通咖啡，等着被子类爆炸\npublic class PlainCoffee {\n    public String getDescription() {\n        return \"普通咖啡\";\n    }\n\n    public double cost() {\n        return 15.0;\n    }\n}\n\n\u002F\u002F 加奶 = 继承一个子类\npublic class MilkCoffee extends PlainCoffee {\n    @Override\n    public String getDescription() {\n        return super.getDescription() + \"+牛奶\";\n    }\n\n    @Override\n    public double cost() {\n        return super.cost() + 3.0;\n    }\n}\n\n\u002F\u002F 再加糖 = 再继承一个子类\npublic class MilkSugarCoffee extends MilkCoffee {\n    @Override\n    public String getDescription() {\n        return super.getDescription() + \"+加糖\";\n    }\n\n    @Override\n    public double cost() {\n        return super.cost() + 2.0;\n    }\n}\n```\n\n测试代码，注释里就是判决书：\n\n```java\nPlainCoffee plain = new PlainCoffee();\nSystem.out.println(plain.getDescription() + \"：\" + plain.cost() + \"元\");\nMilkCoffee milk = new MilkCoffee();\nSystem.out.println(milk.getDescription() + \"：\" + milk.cost() + \"元\");\nMilkSugarCoffee milkSugar = new MilkSugarCoffee();\nSystem.out.println(milkSugar.getDescription() + \"：\" + milkSugar.cost() + \"元\");\n\u002F\u002F 只想加糖不加奶？得再造 SugarCoffee\n\u002F\u002F 想要双份奶？得再造 DoubleMilkCoffee\n\u002F\u002F 三种配料任意组合就是 2 的 3 次方 = 8 个类，每多一种配料类数量翻一倍\n```\n\n运行结果：\n\n```\n===== 反面教材：继承爆炸 =====\n普通咖啡：15.0元\n普通咖啡+牛奶：18.0元\n普通咖啡+牛奶+加糖：20.0元\n```\n\n能跑，但雷已经埋下：\n\n1. **类数量指数级爆炸**：3 种配料全组合是 2³ = 8 个子类，每上新一种配料，子类数量直接翻倍——现实咖啡店菜单上几十种配料，这模型写出来是个天文数字；\n2. **继承链还在误导人**：MilkSugarCoffee extends MilkCoffee 看着像\"加糖咖啡是一种加奶咖啡\"，逻辑上根本不成立——继承表达的是 is-a，这里硬凑的是 has-then-add；\n3. **组合写死在类型里**：\"加奶加糖\"和\"加糖加奶\"是两个不同的类，可它俩本该是一杯咖啡；运行时想换配料？编译期就定死了，换不了。\n\n病根只有一个：**把\"运行时的配料组合\"翻译成了\"编译期的类型层级\"**。\n\n### 2. 装饰器版：一个接口，一层包装\n\n先立公共抽象——所有角色唯一的共同身份：\n\n```java\n\u002F\u002F 组件接口：裸咖啡和所有\"加了料的咖啡\"共同的身份\npublic interface Coffee {\n    String getDescription();\n\n    double cost();\n}\n```\n\n具体组件，一杯裸咖啡：\n\n```java\n\u002F\u002F 具体组件：什么都不加的裸咖啡\npublic class SimpleCoffee implements Coffee {\n    @Override\n    public String getDescription() {\n        return \"普通咖啡\";\n    }\n\n    @Override\n    public double cost() {\n        return 15.0;\n    }\n}\n```\n\n重头戏是抽象装饰器，全模式的心脏：\n\n```java\n\u002F\u002F 抽象装饰器：持有一杯咖啡，自己也还是一杯咖啡——容量被限制为 1 的组合模式\npublic abstract class CondimentDecorator implements Coffee {\n    protected final Coffee coffee;\n\n    protected CondimentDecorator(Coffee coffee) {\n        this.coffee = coffee;\n    }\n}\n```\n\n四行代码，两个字都占全了：`implements Coffee` 是\"我是\"，`protected final Coffee coffee` 是\"我有\"。每个具体装饰器只干一件事——\"在手里那杯的基础上，加上自己这味料\"：\n\n```java\n\u002F\u002F 具体装饰器：牛奶，在自己包裹的那杯基础上加价\npublic class Milk extends CondimentDecorator {\n    public Milk(Coffee coffee) {\n        super(coffee);\n    }\n\n    @Override\n    public String getDescription() {\n        return coffee.getDescription() + \"+牛奶\";\n    }\n\n    @Override\n    public double cost() {\n        return coffee.cost() + 3.0;\n    }\n}\n\n\u002F\u002F Mocha 同理：getDescription() 返回 coffee.getDescription() + \"+摩卡\"，cost() 返回 coffee.cost() + 5.0\n\u002F\u002F Sugar 同理：+加糖，+2.0，完整代码见项目\n```\n\n然后是本篇的招牌镜头——点单全在运行时组装，配方要几层套几层：\n\n```java\nCoffee order1 = new SimpleCoffee();                              \u002F\u002F 什么都不加\nSystem.out.println(order1.getDescription() + \"：\" + order1.cost() + \"元\");\n\nCoffee order2 = new Milk(new SimpleCoffee());                    \u002F\u002F 加奶\nSystem.out.println(order2.getDescription() + \"：\" + order2.cost() + \"元\");\n\nCoffee order3 = new Mocha(new Milk(new SimpleCoffee()));         \u002F\u002F 加奶加摩卡\nSystem.out.println(order3.getDescription() + \"：\" + order3.cost() + \"元\");\n\n\u002F\u002F 双份奶不用新类，再包一层就行；配料全在运行时按单组装\nCoffee order4 = new Sugar(new Milk(new Milk(new SimpleCoffee()))); \u002F\u002F 双份奶加糖\nSystem.out.println(order4.getDescription() + \"：\" + order4.cost() + \"元\");\n```\n\n运行结果：\n\n```\n===== 正解：装饰器模式 =====\n普通咖啡：15.0元\n普通咖啡+牛奶：18.0元\n普通咖啡+牛奶+摩卡：23.0元\n普通咖啡+牛奶+牛奶+加糖：23.0元\n```\n\n对照反面教材逐条验收：\n\n- 反面教材 2³ = 8 个子类才能覆盖的组合，正解 4 个装饰器类（含抽象装饰器）覆盖了**全部**组合、全部顺序、外加任意重复包装——order4 的双份奶，继承方案里还得再造一个 DoubleMilkCoffee；\n- MilkCoffee 里\"is-a 硬凑 has-then-add\"的别扭没了：Milk 不是一种咖啡，是一层\"往咖啡里加牛奶\"的包装；\n- order4 调 cost() 时的链路值得盯一眼：Sugar 问手里的 Milk，Milk 问手里的 Milk，内层 Milk 问手里的 SimpleCoffee——15 一路加 3、加 3、加 2 报回 23，描述同理一层层拼回来。**每个对象只干自己那一小段，剩下的转发**，这正是\"装饰\"二字的工作方式，也是上一篇\"递归不用发明\"的翻版——串是类型的自然结果；\n- SimpleCoffee 一行没改，Milk、Mocha 也一行没改，明天上新\"香草糖浆\"？写一个 Vanilla 类就完事——开闭原则；\n- 一个细节：order1 到 order4 全部声明为 `Coffee` 类型——客户端从头到尾只认组件接口，它根本不知道（也不需要知道）手里这杯包了几层。\n\n## 五、Java IO 流：装饰器的封神之作\n\n理论讲完，去看这个模式在 JDK 里最著名的落地——Java IO 流。当年多少人对着 `new BufferedInputStream(new FileInputStream(file))` 这行代码发懵，学完本篇再看，每个角色都能对号入座：\n\n```java\n\u002F\u002F 抽象组件：所有字节输入流的共同身份\npublic abstract class InputStream implements Closeable {\n    public abstract int read() throws IOException;\n    \u002F\u002F 其余方法略\n}\n\n\u002F\u002F 抽象装饰器：和 CondimentDecorator 一个模子刻出来的\npublic class FilterInputStream extends InputStream {\n    protected volatile InputStream in;   \u002F\u002F 手里攥着的那个\"原咖啡\"\n\n    protected FilterInputStream(InputStream in) {\n        this.in = in;\n    }\n\n    public int read() throws IOException {\n        return in.read();               \u002F\u002F 默认实现：纯转发\n    }\n}\n\n\u002F\u002F 具体装饰器：缓冲，就是 IO 界的\"牛奶\"\npublic class BufferedInputStream extends FilterInputStream {\n    \u002F\u002F 内部一个缓冲数组，read 时先看缓冲区，空了才让 in 去真正读盘\n    public synchronized int read() throws IOException { \u002F* 略 *\u002F }\n}\n```\n\n对照表直接抄：\n\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>咖啡店\u003C\u002Fth>\u003Cth>Java IO\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\u003Ctr>\u003Ctd>Coffee\u003C\u002Ftd>\u003Ctd>InputStream\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>SimpleCoffee\u003C\u002Ftd>\u003Ctd>FileInputStream、ByteArrayInputStream（裸流）\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>CondimentDecorator\u003C\u002Ftd>\u003Ctd>FilterInputStream\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Milk、Mocha、Sugar\u003C\u002Ftd>\u003Ctd>BufferedInputStream、DataInputStream、PushbackInputStream\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>new Milk(new SimpleCoffee())\u003C\u002Ftd>\u003Ctd>new BufferedInputStream(new FileInputStream(file))\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\n\n为什么 IO 非这么设计不可？算笔账就明白。字节流的**来源**有三种：文件、字节数组、网络；**增强**也有三种：缓冲、按基本类型读、支持回退。如果靠继承组合，3 × 3 = 9 个类，每加一种增强，类数量翻一倍——和咖啡店的配料是同一道数学题。而装饰器方案：3 个来源类 + 3 个增强类 + 1 个抽象装饰器，7 个类覆盖全部组合，还白送两项继承永远给不了的能力：\n\n1. **增强对来源无感**：BufferedInputStream 根本不关心手里那杯是文件还是网络，它只管加缓冲——写一个类，所有来源全家受益；\n2. **增强可以叠增强**：`new DataInputStream(new BufferedInputStream(new FileInputStream(file)))`，先缓冲再按类型读，套娃完全合法，运行时想怎么搭就怎么搭。\n\n这套设计 1996 年落地，JDK 沿用至今没动过骨架——模式选对了，二十多年不过时。最后补一句它的边界：IO 里的 FileInputStream 是\"另一种来源\"（新物种，所以是具体组件），BufferedInputStream 是\"一种增强\"（职责，所以是装饰器）——**分清\"来源\"和\"增强\"，是决定该开新类还是该包一层的第一判据**。\n\n## 写在最后\n\n回顾这一篇：装饰器模式用\"我是组件 + 我有组件\"四个字给对象动态加职责——抽象装饰器实现接口并持有一个组件，具体装饰器各加一味料，运行时套娃组装；反面教材\"一种组合一个子类\"的 2ⁿ 爆炸，在装饰器面前只剩下 1 个抽象装饰器 + n 个配料类；Collections 的 unmodifiableList、Servlet 的 Wrapper、MyBatis 的 Cache 装饰器链、Java IO 全家族，都是它的地盘；而\"来源开新类、增强包一层\"，是它留下的第一判据。\n\n下一篇 ⑪ 外观模式，结构型第六位，也是 GoF 里最\"厚道\"的一个：不整虚的，就是把你不想伺候的一大堆对象收拢成一个门面——子系统该多乱还多乱，客户端只见一个入口、一个方法、一句话办完。设计模式里难得的\"降复杂度不涨姿势\"，程序员和甲方都爱的那一款。","\u003Cp>上一篇组合模式的结尾给它留了一句判词：\"UML 画出来，几乎是只准养一个孩子的组合模式\"。今天这个\"独生子\"正式登场：\u003Cstrong>装饰器模式（Decorator Pattern）\u003C\u002Fstrong>，GoF 23 人名单正牌选手、结构型第五位，有的书也叫它\"包装模式（Wrapper）\"。组合模式的容器里装的是一整个 \u003Ccode>List&lt;FileComponent&gt;\u003C\u002Fcode>，孩子越多树越大；装饰器的\"容器\"里只准装\u003Cstrong>一个\u003C\u002Fstrong>对象，但它自己同时还是组件——于是这棵树不许分叉，只许长个儿：一杯咖啡包一层、再加一层，功能就这么一层层叠上去。它回答的问题直白又高频：\u003Cstrong>想给一个类加功能，但不想改它的代码、也不想为每种组合造一个子类，怎么办？\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Ch2>一、什么是装饰器模式？\u003C\u002Fh2>\n\u003Cp>先看定义：\u003C\u002Fp>\n\u003Cblockquote>\n \u003Cp>动态地给一个对象添加一些额外的职责。就增加功能来说，装饰器模式比生成子类更为灵活。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>翻译成人话：\u003Cstrong>不改原类、不碰继承，把\"要加的功能\"做成一个个小包装类——包装类和原对象实现同一个接口，手里攥着原对象，调用时先干自己的活、再把剩下的活转交给手里那位\u003C\u002Fstrong>。要加什么料，运行时套几层、按什么顺序套，全在 new 的时候现场决定。\u003C\u002Fp>\n\u003Cp>生活类比是给手机贴膜：裸机是对象，钢化膜、防窥膜、磨砂膜是装饰器——贴完膜它还是一部手机（接口没变），但抗摔、防窥、不沾指纹这些职责已经加上去了；贴三层就是三层的效果，撕掉一层就少一层功能，手机本身一颗螺丝没动。\u003C\u002Fp>\n\u003Cp>它有四个角色：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>Component（组件接口）\u003C\u002Fstrong>：本篇里是 Coffee，规定\"一杯咖啡\"能干什么；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>ConcreteComponent（具体组件）\u003C\u002Fstrong>：被装饰的原始对象，本篇里是 SimpleCoffee——一杯什么都不加的裸咖啡；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>Decorator（抽象装饰器）\u003C\u002Fstrong>：实现组件接口，同时\u003Cstrong>持有一个组件类型的字段\u003C\u002Fstrong>，本篇里是 CondimentDecorator；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>ConcreteDecorator（具体装饰器）\u003C\u002Fstrong>：每种\"配料\"一个类，本篇里是 Milk、Sugar、Mocha。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>精髓全在抽象装饰器身上，一句话说透：\u003Cstrong>它自己\"是\"组件，手里又\"有\"组件\u003C\u002Fstrong>。是组件，所以它能出现在任何需要组件的地方——包括另一个装饰器的构造器里，于是装饰器可以套装饰器；有组件，所以它的方法实现基本是\"干自己的事 + 转发给手里那位\"。上一篇 Folder 的孩子是 \u003Ccode>List&lt;FileComponent&gt;\u003C\u002Fcode>，数量不限长成树；这一篇 CondimentDecorator 的孩子是一个 \u003Ccode>Coffee\u003C\u002Fcode>，数量限一长成串——所谓\"只准养一个孩子的组合模式\"，就是从这个字段类型来的。\u003C\u002Fp>\n\u003Ch2>二、装饰器模式的优缺点\u003C\u002Fh2>\n\u003Ch3>优点\u003C\u002Fh3>\n\u003Col>\n \u003Cli>\u003Cstrong>组合替代继承，彻底躲开子类爆炸\u003C\u002Fstrong>：继承是编译期焊死的静态关系，加一种配料就得多一层类、多一种组合就多一个类；装饰是运行期自由组装的动态关系，n 种配料任意组合、任意顺序、甚至重复包装（双份奶），类的数量却纹丝不动——下面看事故现场。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>职责拆得干净\u003C\u002Fstrong>：每个装饰器只干一件事，Milk 只管加牛奶，BufferedInputStream 只管缓冲——单一职责原则的模范生。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>对扩展开放、对修改关闭\u003C\u002Fstrong>：新增一种配料 = 新写一个装饰器类，SimpleCoffee、Milk、Mocha 全都不用动，已上线的调用链更不用动——开闭原则。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>可拆卸\u003C\u002Fstrong>：不包就是裸对象，包几层就有几层功能，同一套类支持\"裸奔\"到\"满配\"的全部档位。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch3>缺点\u003C\u002Fh3>\n\u003Col>\n \u003Cli>\u003Cstrong>类数量依然不少\u003C\u002Fstrong>：虽然躲开了指数爆炸，但每种装饰还是一个类，配料多了之后\"小类满天飞\"。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>调用栈深、排障绕\u003C\u002Fstrong>：\u003Ccode>new Sugar(new Milk(new Milk(...)))\u003C\u002Fcode> 出了问题，异常栈和调试断点都要一层层剥洋葱，读代码的人也得在脑子里手动解套娃。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>装饰顺序敏感\u003C\u002Fstrong>：本篇算价格，加法交换律掩盖了顺序问题；但换成\"压缩+加密\"，先压缩再加密和先加密再压缩，产出的东西完全不同——顺序的自由是能力，也是坑。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>初始化代码可读性差\u003C\u002Fstrong>：不熟悉的读者看到一长串 \u003Ccode>new X(new Y(new Z(...)))\u003C\u002Fcode>，第一反应多半是\"这是在干嘛\"。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>三、装饰器模式的应用场景\u003C\u002Fh2>\n\u003Cul>\n \u003Cli>\u003Cstrong>Java IO 流（JDK）\u003C\u002Fstrong>：全宇宙最著名的装饰器家族，InputStream\u002FOutputStream\u002FReader\u002FWriter 四大家族全按这个模式搭的，第五节专门解剖。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>Collections 工具方法（JDK）\u003C\u002Fstrong>：\u003Ccode>Collections.unmodifiableList(list)\u003C\u002Fcode> 返回的其实是一个包着原 list 的装饰器——\"只读\"是它加的职责，原 list 一颗螺丝没动；\u003Ccode>synchronizedList()\u003C\u002Fcode>、\u003Ccode>checkedList()\u003C\u002Fcode> 同理，一个方法一种职责，想叠加就再包一层。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>Servlet 的 Request\u002FResponseWrapper\u003C\u002Fstrong>：HttpServletRequestWrapper 包着原生 request，想改写参数、伪造头部、透传额外属性时，包一层 Wrapper 扔给下游，下游毫不知情——Spring MVC、Spring Security 内部大量使用。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>MyBatis 的 Cache\u003C\u002Fstrong>：Cache 接口下，PerpetualCache 是裸缓存（具体组件），LruCache、FifoCache、LoggingCache、SynchronizedCache、TransactionalCache……全是装饰器。CachingExecutor 组装缓存时按配置把装饰器一层层套上去，\"容量淘汰 + 日志 + 事务感知\"全靠运行时点单——本系列第四次在 MyBatis 里撞见老朋友了。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>Spring 的 TransactionAwareCacheDecorator\u003C\u002Fstrong>：类名直接把模式写脸上了，给任意 Cache 实现\"感知 Spring 事务\"的职责。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cstrong>什么时候该用\u003C\u002Fstrong>：功能是\"可叠加的增强\"而不是\"新物种\"（缓冲之于字节流，是增强；文件之于字节流，是来源）；增强方式存在组合的可能；或者原类是别人的、你压根无权改——三条中两条，就该想到它。\u003C\u002Fp>\n\u003Ch2>四、装饰器模式的实现方式\u003C\u002Fh2>\n\u003Cp>示例代码在 design-pattern 项目的 decorator 包里：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>design-pattern\u002Fsrc\u002Fmain\u002Fjava\n└── decorator                 \u002F\u002F 装饰器模式\n    ├── PlainCoffee.java      \u002F\u002F 反面教材：裸咖啡，等着被子类爆炸\n    ├── MilkCoffee.java       \u002F\u002F 反面教材：加奶 = 继承一个子类\n    ├── MilkSugarCoffee.java  \u002F\u002F 反面教材：再加糖 = 再继承一个子类\n    ├── Coffee.java           \u002F\u002F 组件接口：一杯咖啡能干什么\n    ├── SimpleCoffee.java     \u002F\u002F 具体组件：什么都不加的裸咖啡\n    ├── CondimentDecorator.java \u002F\u002F 抽象装饰器：持有一杯咖啡，自己也还是一杯咖啡\n    ├── Milk.java             \u002F\u002F 具体装饰器：牛奶\n    ├── Sugar.java            \u002F\u002F 具体装饰器：糖\n    ├── Mocha.java            \u002F\u002F 具体装饰器：摩卡\n    └── DecoratorTest.java    \u002F\u002F 测试入口\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>场景：咖啡店点单。一杯裸咖啡 15 元，加牛奶 3 元、加糖 2 元、加摩卡 5 元，描述和价格都要随配料变。先看\"一种组合一个子类\"的下场，再看装饰器怎么救。\u003C\u002Fp>\n\u003Ch3>1. 反面教材：继承爆炸\u003C\u002Fh3>\n\u003Cp>最直觉的写法：要什么组合，就继承出什么子类：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F\u002F 反面教材：普通咖啡，等着被子类爆炸\npublic class PlainCoffee {\n    public String getDescription() {\n        return \"普通咖啡\";\n    }\n\n    public double cost() {\n        return 15.0;\n    }\n}\n\n\u002F\u002F 加奶 = 继承一个子类\npublic class MilkCoffee extends PlainCoffee {\n    @Override\n    public String getDescription() {\n        return super.getDescription() + \"+牛奶\";\n    }\n\n    @Override\n    public double cost() {\n        return super.cost() + 3.0;\n    }\n}\n\n\u002F\u002F 再加糖 = 再继承一个子类\npublic class MilkSugarCoffee extends MilkCoffee {\n    @Override\n    public String getDescription() {\n        return super.getDescription() + \"+加糖\";\n    }\n\n    @Override\n    public double cost() {\n        return super.cost() + 2.0;\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>测试代码，注释里就是判决书：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">PlainCoffee plain = new PlainCoffee();\nSystem.out.println(plain.getDescription() + \"：\" + plain.cost() + \"元\");\nMilkCoffee milk = new MilkCoffee();\nSystem.out.println(milk.getDescription() + \"：\" + milk.cost() + \"元\");\nMilkSugarCoffee milkSugar = new MilkSugarCoffee();\nSystem.out.println(milkSugar.getDescription() + \"：\" + milkSugar.cost() + \"元\");\n\u002F\u002F 只想加糖不加奶？得再造 SugarCoffee\n\u002F\u002F 想要双份奶？得再造 DoubleMilkCoffee\n\u002F\u002F 三种配料任意组合就是 2 的 3 次方 = 8 个类，每多一种配料类数量翻一倍\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行结果：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>===== 反面教材：继承爆炸 =====\n普通咖啡：15.0元\n普通咖啡+牛奶：18.0元\n普通咖啡+牛奶+加糖：20.0元\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>能跑，但雷已经埋下：\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>类数量指数级爆炸\u003C\u002Fstrong>：3 种配料全组合是 2³ = 8 个子类，每上新一种配料，子类数量直接翻倍——现实咖啡店菜单上几十种配料，这模型写出来是个天文数字；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>继承链还在误导人\u003C\u002Fstrong>：MilkSugarCoffee extends MilkCoffee 看着像\"加糖咖啡是一种加奶咖啡\"，逻辑上根本不成立——继承表达的是 is-a，这里硬凑的是 has-then-add；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>组合写死在类型里\u003C\u002Fstrong>：\"加奶加糖\"和\"加糖加奶\"是两个不同的类，可它俩本该是一杯咖啡；运行时想换配料？编译期就定死了，换不了。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>病根只有一个：\u003Cstrong>把\"运行时的配料组合\"翻译成了\"编译期的类型层级\"\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Ch3>2. 装饰器版：一个接口，一层包装\u003C\u002Fh3>\n\u003Cp>先立公共抽象——所有角色唯一的共同身份：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F\u002F 组件接口：裸咖啡和所有\"加了料的咖啡\"共同的身份\npublic interface Coffee {\n    String getDescription();\n\n    double cost();\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>具体组件，一杯裸咖啡：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F\u002F 具体组件：什么都不加的裸咖啡\npublic class SimpleCoffee implements Coffee {\n    @Override\n    public String getDescription() {\n        return \"普通咖啡\";\n    }\n\n    @Override\n    public double cost() {\n        return 15.0;\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>重头戏是抽象装饰器，全模式的心脏：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F\u002F 抽象装饰器：持有一杯咖啡，自己也还是一杯咖啡——容量被限制为 1 的组合模式\npublic abstract class CondimentDecorator implements Coffee {\n    protected final Coffee coffee;\n\n    protected CondimentDecorator(Coffee coffee) {\n        this.coffee = coffee;\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>四行代码，两个字都占全了：\u003Ccode>implements Coffee\u003C\u002Fcode> 是\"我是\"，\u003Ccode>protected final Coffee coffee\u003C\u002Fcode> 是\"我有\"。每个具体装饰器只干一件事——\"在手里那杯的基础上，加上自己这味料\"：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F\u002F 具体装饰器：牛奶，在自己包裹的那杯基础上加价\npublic class Milk extends CondimentDecorator {\n    public Milk(Coffee coffee) {\n        super(coffee);\n    }\n\n    @Override\n    public String getDescription() {\n        return coffee.getDescription() + \"+牛奶\";\n    }\n\n    @Override\n    public double cost() {\n        return coffee.cost() + 3.0;\n    }\n}\n\n\u002F\u002F Mocha 同理：getDescription() 返回 coffee.getDescription() + \"+摩卡\"，cost() 返回 coffee.cost() + 5.0\n\u002F\u002F Sugar 同理：+加糖，+2.0，完整代码见项目\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>然后是本篇的招牌镜头——点单全在运行时组装，配方要几层套几层：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">Coffee order1 = new SimpleCoffee();                              \u002F\u002F 什么都不加\nSystem.out.println(order1.getDescription() + \"：\" + order1.cost() + \"元\");\n\nCoffee order2 = new Milk(new SimpleCoffee());                    \u002F\u002F 加奶\nSystem.out.println(order2.getDescription() + \"：\" + order2.cost() + \"元\");\n\nCoffee order3 = new Mocha(new Milk(new SimpleCoffee()));         \u002F\u002F 加奶加摩卡\nSystem.out.println(order3.getDescription() + \"：\" + order3.cost() + \"元\");\n\n\u002F\u002F 双份奶不用新类，再包一层就行；配料全在运行时按单组装\nCoffee order4 = new Sugar(new Milk(new Milk(new SimpleCoffee()))); \u002F\u002F 双份奶加糖\nSystem.out.println(order4.getDescription() + \"：\" + order4.cost() + \"元\");\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行结果：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>===== 正解：装饰器模式 =====\n普通咖啡：15.0元\n普通咖啡+牛奶：18.0元\n普通咖啡+牛奶+摩卡：23.0元\n普通咖啡+牛奶+牛奶+加糖：23.0元\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>对照反面教材逐条验收：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>反面教材 2³ = 8 个子类才能覆盖的组合，正解 4 个装饰器类（含抽象装饰器）覆盖了\u003Cstrong>全部\u003C\u002Fstrong>组合、全部顺序、外加任意重复包装——order4 的双份奶，继承方案里还得再造一个 DoubleMilkCoffee；\u003C\u002Fli>\n \u003Cli>MilkCoffee 里\"is-a 硬凑 has-then-add\"的别扭没了：Milk 不是一种咖啡，是一层\"往咖啡里加牛奶\"的包装；\u003C\u002Fli>\n \u003Cli>order4 调 cost() 时的链路值得盯一眼：Sugar 问手里的 Milk，Milk 问手里的 Milk，内层 Milk 问手里的 SimpleCoffee——15 一路加 3、加 3、加 2 报回 23，描述同理一层层拼回来。\u003Cstrong>每个对象只干自己那一小段，剩下的转发\u003C\u002Fstrong>，这正是\"装饰\"二字的工作方式，也是上一篇\"递归不用发明\"的翻版——串是类型的自然结果；\u003C\u002Fli>\n \u003Cli>SimpleCoffee 一行没改，Milk、Mocha 也一行没改，明天上新\"香草糖浆\"？写一个 Vanilla 类就完事——开闭原则；\u003C\u002Fli>\n \u003Cli>一个细节：order1 到 order4 全部声明为 \u003Ccode>Coffee\u003C\u002Fcode> 类型——客户端从头到尾只认组件接口，它根本不知道（也不需要知道）手里这杯包了几层。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2>五、Java IO 流：装饰器的封神之作\u003C\u002Fh2>\n\u003Cp>理论讲完，去看这个模式在 JDK 里最著名的落地——Java IO 流。当年多少人对着 \u003Ccode>new BufferedInputStream(new FileInputStream(file))\u003C\u002Fcode> 这行代码发懵，学完本篇再看，每个角色都能对号入座：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F\u002F 抽象组件：所有字节输入流的共同身份\npublic abstract class InputStream implements Closeable {\n    public abstract int read() throws IOException;\n    \u002F\u002F 其余方法略\n}\n\n\u002F\u002F 抽象装饰器：和 CondimentDecorator 一个模子刻出来的\npublic class FilterInputStream extends InputStream {\n    protected volatile InputStream in;   \u002F\u002F 手里攥着的那个\"原咖啡\"\n\n    protected FilterInputStream(InputStream in) {\n        this.in = in;\n    }\n\n    public int read() throws IOException {\n        return in.read();               \u002F\u002F 默认实现：纯转发\n    }\n}\n\n\u002F\u002F 具体装饰器：缓冲，就是 IO 界的\"牛奶\"\npublic class BufferedInputStream extends FilterInputStream {\n    \u002F\u002F 内部一个缓冲数组，read 时先看缓冲区，空了才让 in 去真正读盘\n    public synchronized int read() throws IOException { \u002F* 略 *\u002F }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>对照表直接抄：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>咖啡店\u003C\u002Fth>\n   \u003Cth>Java IO\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>Coffee\u003C\u002Ftd>\n   \u003Ctd>InputStream\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>SimpleCoffee\u003C\u002Ftd>\n   \u003Ctd>FileInputStream、ByteArrayInputStream（裸流）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>CondimentDecorator\u003C\u002Ftd>\n   \u003Ctd>FilterInputStream\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Milk、Mocha、Sugar\u003C\u002Ftd>\n   \u003Ctd>BufferedInputStream、DataInputStream、PushbackInputStream\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>new Milk(new SimpleCoffee())\u003C\u002Ftd>\n   \u003Ctd>new BufferedInputStream(new FileInputStream(file))\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>为什么 IO 非这么设计不可？算笔账就明白。字节流的\u003Cstrong>来源\u003C\u002Fstrong>有三种：文件、字节数组、网络；\u003Cstrong>增强\u003C\u002Fstrong>也有三种：缓冲、按基本类型读、支持回退。如果靠继承组合，3 × 3 = 9 个类，每加一种增强，类数量翻一倍——和咖啡店的配料是同一道数学题。而装饰器方案：3 个来源类 + 3 个增强类 + 1 个抽象装饰器，7 个类覆盖全部组合，还白送两项继承永远给不了的能力：\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>增强对来源无感\u003C\u002Fstrong>：BufferedInputStream 根本不关心手里那杯是文件还是网络，它只管加缓冲——写一个类，所有来源全家受益；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>增强可以叠增强\u003C\u002Fstrong>：\u003Ccode>new DataInputStream(new BufferedInputStream(new FileInputStream(file)))\u003C\u002Fcode>，先缓冲再按类型读，套娃完全合法，运行时想怎么搭就怎么搭。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>这套设计 1996 年落地，JDK 沿用至今没动过骨架——模式选对了，二十多年不过时。最后补一句它的边界：IO 里的 FileInputStream 是\"另一种来源\"（新物种，所以是具体组件），BufferedInputStream 是\"一种增强\"（职责，所以是装饰器）——\u003Cstrong>分清\"来源\"和\"增强\"，是决定该开新类还是该包一层的第一判据\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Ch2>写在最后\u003C\u002Fh2>\n\u003Cp>回顾这一篇：装饰器模式用\"我是组件 + 我有组件\"四个字给对象动态加职责——抽象装饰器实现接口并持有一个组件，具体装饰器各加一味料，运行时套娃组装；反面教材\"一种组合一个子类\"的 2ⁿ 爆炸，在装饰器面前只剩下 1 个抽象装饰器 + n 个配料类；Collections 的 unmodifiableList、Servlet 的 Wrapper、MyBatis 的 Cache 装饰器链、Java IO 全家族，都是它的地盘；而\"来源开新类、增强包一层\"，是它留下的第一判据。\u003C\u002Fp>\n\u003Cp>下一篇 ⑪ 外观模式，结构型第六位，也是 GoF 里最\"厚道\"的一个：不整虚的，就是把你不想伺候的一大堆对象收拢成一个门面——子系统该多乱还多乱，客户端只见一个入口、一个方法、一句话办完。设计模式里难得的\"降复杂度不涨姿势\"，程序员和甲方都爱的那一款。\u003C\u002Fp>",[],"index,follow",null,{"code":4,"message":5,"data":44},[45,53,62],{"id":46,"slug":47,"title":48,"summary":49,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":50,"likeCount":4,"wordCount":51,"readingMinutes":36,"publishedAt":52},"2113931567565901825","design-pattern-09","设计模式修炼之路 ⑨ 组合模式——叶子和容器一视同仁","部分-整体、叶子和容器一视同仁、在 DOM\u002FMyBatis 里找它",3281,7735,"2024-01-25T15:35:13",{"id":54,"slug":55,"title":56,"summary":57,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":58,"likeCount":4,"wordCount":59,"readingMinutes":60,"publishedAt":61},"2113932970753527809","design-pattern-13","设计模式修炼之路 ⑬ 代理模式——控制访问，而非增强","控制访问而非增强、JDK 动态代理的 $Proxy0、CGLIB 与 Spring AOP 的老搭档",3804,9153,23,"2024-05-02T10:29:24",{"id":63,"slug":64,"title":65,"summary":66,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":67,"likeCount":4,"wordCount":68,"readingMinutes":69,"publishedAt":70},"2113931305484816386","design-pattern-06","设计模式修炼之路 ⑥ 适配器模式——让不兼容的接口握手","类适配器 vs 对象适配器、在 Spring MVC 里找它",1578,5381,14,"2023-12-11T21:59:29",{"code":4,"message":5,"data":72},{"prev":73,"next":74},{"id":46,"slug":47,"title":48,"summary":49,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":50,"likeCount":4,"wordCount":51,"readingMinutes":36,"publishedAt":52},{"id":75,"slug":76,"title":77,"summary":78,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":79,"likeCount":4,"wordCount":80,"readingMinutes":36,"publishedAt":81},"2113933638763548673","design-pattern-11","设计模式修炼之路 ⑪ 外观模式——给杂乱子系统一个入口","给杂乱子系统一个统一入口、只编排不干活、SLF4J 名字里的 Facade",3176,7681,"2024-03-20T15:49:38",{"code":4,"message":5,"data":83},[84,90,95,101],{"id":85,"name":86,"slug":87,"description":88,"sortOrder":4,"articleCount":89},"1946000000000000001","antlr4","tech","编程与技术笔记","13",{"id":32,"name":91,"slug":92,"description":93,"sortOrder":4,"articleCount":94},"设计模式","design-pattern","设计模式系列","34",{"id":96,"name":97,"slug":98,"description":99,"sortOrder":33,"articleCount":100},"1946000000000000002","随笔","essay","生活与思考","0",{"id":102,"name":103,"slug":104,"description":105,"sortOrder":106,"articleCount":107},"2105200093764698113","工具","tools","好用的工具推荐",2,"1",{"html":109,"toc":110},"\u003Cp>上一篇组合模式的结尾给它留了一句判词：\"UML 画出来，几乎是只准养一个孩子的组合模式\"。今天这个\"独生子\"正式登场：\u003Cstrong>装饰器模式（Decorator Pattern）\u003C\u002Fstrong>，GoF 23 人名单正牌选手、结构型第五位，有的书也叫它\"包装模式（Wrapper）\"。组合模式的容器里装的是一整个 \u003Ccode>List&lt;FileComponent&gt;\u003C\u002Fcode>，孩子越多树越大；装饰器的\"容器\"里只准装\u003Cstrong>一个\u003C\u002Fstrong>对象，但它自己同时还是组件——于是这棵树不许分叉，只许长个儿：一杯咖啡包一层、再加一层，功能就这么一层层叠上去。它回答的问题直白又高频：\u003Cstrong>想给一个类加功能，但不想改它的代码、也不想为每种组合造一个子类，怎么办？\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Ch2 id=\"toc-1\">一、什么是装饰器模式？\u003C\u002Fh2>\n\u003Cp>先看定义：\u003C\u002Fp>\n\u003Cblockquote>\n \u003Cp>动态地给一个对象添加一些额外的职责。就增加功能来说，装饰器模式比生成子类更为灵活。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>翻译成人话：\u003Cstrong>不改原类、不碰继承，把\"要加的功能\"做成一个个小包装类——包装类和原对象实现同一个接口，手里攥着原对象，调用时先干自己的活、再把剩下的活转交给手里那位\u003C\u002Fstrong>。要加什么料，运行时套几层、按什么顺序套，全在 new 的时候现场决定。\u003C\u002Fp>\n\u003Cp>生活类比是给手机贴膜：裸机是对象，钢化膜、防窥膜、磨砂膜是装饰器——贴完膜它还是一部手机（接口没变），但抗摔、防窥、不沾指纹这些职责已经加上去了；贴三层就是三层的效果，撕掉一层就少一层功能，手机本身一颗螺丝没动。\u003C\u002Fp>\n\u003Cp>它有四个角色：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>Component（组件接口）\u003C\u002Fstrong>：本篇里是 Coffee，规定\"一杯咖啡\"能干什么；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>ConcreteComponent（具体组件）\u003C\u002Fstrong>：被装饰的原始对象，本篇里是 SimpleCoffee——一杯什么都不加的裸咖啡；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>Decorator（抽象装饰器）\u003C\u002Fstrong>：实现组件接口，同时\u003Cstrong>持有一个组件类型的字段\u003C\u002Fstrong>，本篇里是 CondimentDecorator；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>ConcreteDecorator（具体装饰器）\u003C\u002Fstrong>：每种\"配料\"一个类，本篇里是 Milk、Sugar、Mocha。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>精髓全在抽象装饰器身上，一句话说透：\u003Cstrong>它自己\"是\"组件，手里又\"有\"组件\u003C\u002Fstrong>。是组件，所以它能出现在任何需要组件的地方——包括另一个装饰器的构造器里，于是装饰器可以套装饰器；有组件，所以它的方法实现基本是\"干自己的事 + 转发给手里那位\"。上一篇 Folder 的孩子是 \u003Ccode>List&lt;FileComponent&gt;\u003C\u002Fcode>，数量不限长成树；这一篇 CondimentDecorator 的孩子是一个 \u003Ccode>Coffee\u003C\u002Fcode>，数量限一长成串——所谓\"只准养一个孩子的组合模式\"，就是从这个字段类型来的。\u003C\u002Fp>\n\u003Ch2 id=\"toc-2\">二、装饰器模式的优缺点\u003C\u002Fh2>\n\u003Ch3 id=\"toc-3\">优点\u003C\u002Fh3>\n\u003Col>\n \u003Cli>\u003Cstrong>组合替代继承，彻底躲开子类爆炸\u003C\u002Fstrong>：继承是编译期焊死的静态关系，加一种配料就得多一层类、多一种组合就多一个类；装饰是运行期自由组装的动态关系，n 种配料任意组合、任意顺序、甚至重复包装（双份奶），类的数量却纹丝不动——下面看事故现场。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>职责拆得干净\u003C\u002Fstrong>：每个装饰器只干一件事，Milk 只管加牛奶，BufferedInputStream 只管缓冲——单一职责原则的模范生。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>对扩展开放、对修改关闭\u003C\u002Fstrong>：新增一种配料 = 新写一个装饰器类，SimpleCoffee、Milk、Mocha 全都不用动，已上线的调用链更不用动——开闭原则。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>可拆卸\u003C\u002Fstrong>：不包就是裸对象，包几层就有几层功能，同一套类支持\"裸奔\"到\"满配\"的全部档位。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch3 id=\"toc-4\">缺点\u003C\u002Fh3>\n\u003Col>\n \u003Cli>\u003Cstrong>类数量依然不少\u003C\u002Fstrong>：虽然躲开了指数爆炸，但每种装饰还是一个类，配料多了之后\"小类满天飞\"。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>调用栈深、排障绕\u003C\u002Fstrong>：\u003Ccode>new Sugar(new Milk(new Milk(...)))\u003C\u002Fcode> 出了问题，异常栈和调试断点都要一层层剥洋葱，读代码的人也得在脑子里手动解套娃。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>装饰顺序敏感\u003C\u002Fstrong>：本篇算价格，加法交换律掩盖了顺序问题；但换成\"压缩+加密\"，先压缩再加密和先加密再压缩，产出的东西完全不同——顺序的自由是能力，也是坑。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>初始化代码可读性差\u003C\u002Fstrong>：不熟悉的读者看到一长串 \u003Ccode>new X(new Y(new Z(...)))\u003C\u002Fcode>，第一反应多半是\"这是在干嘛\"。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"toc-5\">三、装饰器模式的应用场景\u003C\u002Fh2>\n\u003Cul>\n \u003Cli>\u003Cstrong>Java IO 流（JDK）\u003C\u002Fstrong>：全宇宙最著名的装饰器家族，InputStream\u002FOutputStream\u002FReader\u002FWriter 四大家族全按这个模式搭的，第五节专门解剖。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>Collections 工具方法（JDK）\u003C\u002Fstrong>：\u003Ccode>Collections.unmodifiableList(list)\u003C\u002Fcode> 返回的其实是一个包着原 list 的装饰器——\"只读\"是它加的职责，原 list 一颗螺丝没动；\u003Ccode>synchronizedList()\u003C\u002Fcode>、\u003Ccode>checkedList()\u003C\u002Fcode> 同理，一个方法一种职责，想叠加就再包一层。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>Servlet 的 Request\u002FResponseWrapper\u003C\u002Fstrong>：HttpServletRequestWrapper 包着原生 request，想改写参数、伪造头部、透传额外属性时，包一层 Wrapper 扔给下游，下游毫不知情——Spring MVC、Spring Security 内部大量使用。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>MyBatis 的 Cache\u003C\u002Fstrong>：Cache 接口下，PerpetualCache 是裸缓存（具体组件），LruCache、FifoCache、LoggingCache、SynchronizedCache、TransactionalCache……全是装饰器。CachingExecutor 组装缓存时按配置把装饰器一层层套上去，\"容量淘汰 + 日志 + 事务感知\"全靠运行时点单——本系列第四次在 MyBatis 里撞见老朋友了。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>Spring 的 TransactionAwareCacheDecorator\u003C\u002Fstrong>：类名直接把模式写脸上了，给任意 Cache 实现\"感知 Spring 事务\"的职责。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cstrong>什么时候该用\u003C\u002Fstrong>：功能是\"可叠加的增强\"而不是\"新物种\"（缓冲之于字节流，是增强；文件之于字节流，是来源）；增强方式存在组合的可能；或者原类是别人的、你压根无权改——三条中两条，就该想到它。\u003C\u002Fp>\n\u003Ch2 id=\"toc-6\">四、装饰器模式的实现方式\u003C\u002Fh2>\n\u003Cp>示例代码在 design-pattern 项目的 decorator 包里：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>design-pattern\u002Fsrc\u002Fmain\u002Fjava\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>└── decorator                 \u002F\u002F 装饰器模式\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── PlainCoffee.java      \u002F\u002F 反面教材：裸咖啡，等着被子类爆炸\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── MilkCoffee.java       \u002F\u002F 反面教材：加奶 = 继承一个子类\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── MilkSugarCoffee.java  \u002F\u002F 反面教材：再加糖 = 再继承一个子类\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── Coffee.java           \u002F\u002F 组件接口：一杯咖啡能干什么\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── SimpleCoffee.java     \u002F\u002F 具体组件：什么都不加的裸咖啡\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── CondimentDecorator.java \u002F\u002F 抽象装饰器：持有一杯咖啡，自己也还是一杯咖啡\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── Milk.java             \u002F\u002F 具体装饰器：牛奶\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── Sugar.java            \u002F\u002F 具体装饰器：糖\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── Mocha.java            \u002F\u002F 具体装饰器：摩卡\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    └── DecoratorTest.java    \u002F\u002F 测试入口\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>场景：咖啡店点单。一杯裸咖啡 15 元，加牛奶 3 元、加糖 2 元、加摩卡 5 元，描述和价格都要随配料变。先看\"一种组合一个子类\"的下场，再看装饰器怎么救。\u003C\u002Fp>\n\u003Ch3 id=\"toc-7\">1. 反面教材：继承爆炸\u003C\u002Fh3>\n\u003Cp>最直觉的写法：要什么组合，就继承出什么子类：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 反面教材：普通咖啡，等着被子类爆炸\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> PlainCoffee\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getDescription\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"普通咖啡\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> double\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> cost\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 15.0\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 加奶 = 继承一个子类\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> MilkCoffee\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> extends\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> PlainCoffee\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getDescription\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> super\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getDescription\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"+牛奶\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> double\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> cost\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> super\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">cost\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 3.0\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 再加糖 = 再继承一个子类\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> MilkSugarCoffee\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> extends\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> MilkCoffee\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getDescription\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> super\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getDescription\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"+加糖\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> double\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> cost\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> super\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">cost\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 2.0\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>测试代码，注释里就是判决书：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">PlainCoffee plain \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> PlainCoffee\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(plain.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getDescription\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> plain.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">cost\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"元\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">MilkCoffee milk \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> MilkCoffee\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(milk.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getDescription\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> milk.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">cost\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"元\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">MilkSugarCoffee milkSugar \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> MilkSugarCoffee\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(milkSugar.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getDescription\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> milkSugar.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">cost\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"元\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 只想加糖不加奶？得再造 SugarCoffee\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 想要双份奶？得再造 DoubleMilkCoffee\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 三种配料任意组合就是 2 的 3 次方 = 8 个类，每多一种配料类数量翻一倍\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行结果：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>===== 反面教材：继承爆炸 =====\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>普通咖啡：15.0元\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>普通咖啡+牛奶：18.0元\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>普通咖啡+牛奶+加糖：20.0元\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>能跑，但雷已经埋下：\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>类数量指数级爆炸\u003C\u002Fstrong>：3 种配料全组合是 2³ = 8 个子类，每上新一种配料，子类数量直接翻倍——现实咖啡店菜单上几十种配料，这模型写出来是个天文数字；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>继承链还在误导人\u003C\u002Fstrong>：MilkSugarCoffee extends MilkCoffee 看着像\"加糖咖啡是一种加奶咖啡\"，逻辑上根本不成立——继承表达的是 is-a，这里硬凑的是 has-then-add；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>组合写死在类型里\u003C\u002Fstrong>：\"加奶加糖\"和\"加糖加奶\"是两个不同的类，可它俩本该是一杯咖啡；运行时想换配料？编译期就定死了，换不了。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>病根只有一个：\u003Cstrong>把\"运行时的配料组合\"翻译成了\"编译期的类型层级\"\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Ch3 id=\"toc-8\">2. 装饰器版：一个接口，一层包装\u003C\u002Fh3>\n\u003Cp>先立公共抽象——所有角色唯一的共同身份：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 组件接口：裸咖啡和所有\"加了料的咖啡\"共同的身份\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> interface\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Coffee\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    String \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getDescription\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    double\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> cost\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>具体组件，一杯裸咖啡：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 具体组件：什么都不加的裸咖啡\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> SimpleCoffee\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> implements\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Coffee\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getDescription\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"普通咖啡\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> double\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> cost\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 15.0\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>重头戏是抽象装饰器，全模式的心脏：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 抽象装饰器：持有一杯咖啡，自己也还是一杯咖啡——容量被限制为 1 的组合模式\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> abstract\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> CondimentDecorator\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> implements\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Coffee\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    protected\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Coffee coffee;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    protected\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> CondimentDecorator\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(Coffee \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">coffee\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">        this\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">.coffee \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> coffee;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>四行代码，两个字都占全了：\u003Ccode>implements Coffee\u003C\u002Fcode> 是\"我是\"，\u003Ccode>protected final Coffee coffee\u003C\u002Fcode> 是\"我有\"。每个具体装饰器只干一件事——\"在手里那杯的基础上，加上自己这味料\"：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 具体装饰器：牛奶，在自己包裹的那杯基础上加价\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Milk\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> extends\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> CondimentDecorator\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Milk\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(Coffee \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">coffee\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">        super\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(coffee);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getDescription\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> coffee.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getDescription\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"+牛奶\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> double\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> cost\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> coffee.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">cost\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 3.0\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F Mocha 同理：getDescription() 返回 coffee.getDescription() + \"+摩卡\"，cost() 返回 coffee.cost() + 5.0\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F Sugar 同理：+加糖，+2.0，完整代码见项目\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>然后是本篇的招牌镜头——点单全在运行时组装，配方要几层套几层：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">Coffee order1 \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> SimpleCoffee\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();                              \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 什么都不加\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(order1.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getDescription\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> order1.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">cost\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"元\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">Coffee order2 \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Milk\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> SimpleCoffee\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());                    \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 加奶\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(order2.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getDescription\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> order2.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">cost\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"元\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">Coffee order3 \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Mocha\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Milk\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> SimpleCoffee\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()));         \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 加奶加摩卡\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(order3.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getDescription\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> order3.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">cost\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"元\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 双份奶不用新类，再包一层就行；配料全在运行时按单组装\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">Coffee order4 \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Sugar\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Milk\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Milk\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> SimpleCoffee\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()))); \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 双份奶加糖\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(order4.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getDescription\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> order4.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">cost\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"元\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行结果：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>===== 正解：装饰器模式 =====\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>普通咖啡：15.0元\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>普通咖啡+牛奶：18.0元\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>普通咖啡+牛奶+摩卡：23.0元\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>普通咖啡+牛奶+牛奶+加糖：23.0元\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>对照反面教材逐条验收：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>反面教材 2³ = 8 个子类才能覆盖的组合，正解 4 个装饰器类（含抽象装饰器）覆盖了\u003Cstrong>全部\u003C\u002Fstrong>组合、全部顺序、外加任意重复包装——order4 的双份奶，继承方案里还得再造一个 DoubleMilkCoffee；\u003C\u002Fli>\n \u003Cli>MilkCoffee 里\"is-a 硬凑 has-then-add\"的别扭没了：Milk 不是一种咖啡，是一层\"往咖啡里加牛奶\"的包装；\u003C\u002Fli>\n \u003Cli>order4 调 cost() 时的链路值得盯一眼：Sugar 问手里的 Milk，Milk 问手里的 Milk，内层 Milk 问手里的 SimpleCoffee——15 一路加 3、加 3、加 2 报回 23，描述同理一层层拼回来。\u003Cstrong>每个对象只干自己那一小段，剩下的转发\u003C\u002Fstrong>，这正是\"装饰\"二字的工作方式，也是上一篇\"递归不用发明\"的翻版——串是类型的自然结果；\u003C\u002Fli>\n \u003Cli>SimpleCoffee 一行没改，Milk、Mocha 也一行没改，明天上新\"香草糖浆\"？写一个 Vanilla 类就完事——开闭原则；\u003C\u002Fli>\n \u003Cli>一个细节：order1 到 order4 全部声明为 \u003Ccode>Coffee\u003C\u002Fcode> 类型——客户端从头到尾只认组件接口，它根本不知道（也不需要知道）手里这杯包了几层。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"toc-9\">五、Java IO 流：装饰器的封神之作\u003C\u002Fh2>\n\u003Cp>理论讲完，去看这个模式在 JDK 里最著名的落地——Java IO 流。当年多少人对着 \u003Ccode>new BufferedInputStream(new FileInputStream(file))\u003C\u002Fcode> 这行代码发懵，学完本篇再看，每个角色都能对号入座：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 抽象组件：所有字节输入流的共同身份\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> abstract\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> InputStream\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> implements\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Closeable\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> abstract\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> int\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> read\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">throws\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> IOException;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F 其余方法略\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 抽象装饰器：和 CondimentDecorator 一个模子刻出来的\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> FilterInputStream\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> extends\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> InputStream\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    protected\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> volatile\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> InputStream in;   \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 手里攥着的那个\"原咖啡\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    protected\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> FilterInputStream\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(InputStream \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">in\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">        this\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">.in \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> in;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> int\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> read\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">throws\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> IOException {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> in.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">read\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();               \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 默认实现：纯转发\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 具体装饰器：缓冲，就是 IO 界的\"牛奶\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> BufferedInputStream\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> extends\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> FilterInputStream\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F 内部一个缓冲数组，read 时先看缓冲区，空了才让 in 去真正读盘\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> synchronized\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> int\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> read\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">throws\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> IOException { \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F* 略 *\u002F\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>对照表直接抄：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>咖啡店\u003C\u002Fth>\n   \u003Cth>Java IO\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>Coffee\u003C\u002Ftd>\n   \u003Ctd>InputStream\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>SimpleCoffee\u003C\u002Ftd>\n   \u003Ctd>FileInputStream、ByteArrayInputStream（裸流）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>CondimentDecorator\u003C\u002Ftd>\n   \u003Ctd>FilterInputStream\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Milk、Mocha、Sugar\u003C\u002Ftd>\n   \u003Ctd>BufferedInputStream、DataInputStream、PushbackInputStream\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>new Milk(new SimpleCoffee())\u003C\u002Ftd>\n   \u003Ctd>new BufferedInputStream(new FileInputStream(file))\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>为什么 IO 非这么设计不可？算笔账就明白。字节流的\u003Cstrong>来源\u003C\u002Fstrong>有三种：文件、字节数组、网络；\u003Cstrong>增强\u003C\u002Fstrong>也有三种：缓冲、按基本类型读、支持回退。如果靠继承组合，3 × 3 = 9 个类，每加一种增强，类数量翻一倍——和咖啡店的配料是同一道数学题。而装饰器方案：3 个来源类 + 3 个增强类 + 1 个抽象装饰器，7 个类覆盖全部组合，还白送两项继承永远给不了的能力：\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>增强对来源无感\u003C\u002Fstrong>：BufferedInputStream 根本不关心手里那杯是文件还是网络，它只管加缓冲——写一个类，所有来源全家受益；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>增强可以叠增强\u003C\u002Fstrong>：\u003Ccode>new DataInputStream(new BufferedInputStream(new FileInputStream(file)))\u003C\u002Fcode>，先缓冲再按类型读，套娃完全合法，运行时想怎么搭就怎么搭。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>这套设计 1996 年落地，JDK 沿用至今没动过骨架——模式选对了，二十多年不过时。最后补一句它的边界：IO 里的 FileInputStream 是\"另一种来源\"（新物种，所以是具体组件），BufferedInputStream 是\"一种增强\"（职责，所以是装饰器）——\u003Cstrong>分清\"来源\"和\"增强\"，是决定该开新类还是该包一层的第一判据\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Ch2 id=\"toc-10\">写在最后\u003C\u002Fh2>\n\u003Cp>回顾这一篇：装饰器模式用\"我是组件 + 我有组件\"四个字给对象动态加职责——抽象装饰器实现接口并持有一个组件，具体装饰器各加一味料，运行时套娃组装；反面教材\"一种组合一个子类\"的 2ⁿ 爆炸，在装饰器面前只剩下 1 个抽象装饰器 + n 个配料类；Collections 的 unmodifiableList、Servlet 的 Wrapper、MyBatis 的 Cache 装饰器链、Java IO 全家族，都是它的地盘；而\"来源开新类、增强包一层\"，是它留下的第一判据。\u003C\u002Fp>\n\u003Cp>下一篇 ⑪ 外观模式，结构型第六位，也是 GoF 里最\"厚道\"的一个：不整虚的，就是把你不想伺候的一大堆对象收拢成一个门面——子系统该多乱还多乱，客户端只见一个入口、一个方法、一句话办完。设计模式里难得的\"降复杂度不涨姿势\"，程序员和甲方都爱的那一款。\u003C\u002Fp>",[111,114,117,121,124,127,130,133,136,139],{"id":112,"text":113,"level":106},"toc-1","一、什么是装饰器模式？",{"id":115,"text":116,"level":106},"toc-2","二、装饰器模式的优缺点",{"id":118,"text":119,"level":120},"toc-3","优点",3,{"id":122,"text":123,"level":120},"toc-4","缺点",{"id":125,"text":126,"level":106},"toc-5","三、装饰器模式的应用场景",{"id":128,"text":129,"level":106},"toc-6","四、装饰器模式的实现方式",{"id":131,"text":132,"level":120},"toc-7","1. 反面教材：继承爆炸",{"id":134,"text":135,"level":120},"toc-8","2. 装饰器版：一个接口，一层包装",{"id":137,"text":138,"level":106},"toc-9","五、Java IO 流：装饰器的封神之作",{"id":140,"text":141,"level":106},"toc-10","写在最后"]