38_Java设计模式之装饰器模式

Java设计模式之装饰器模式

文章目录

前言

在咖啡店里,你点了一杯浓缩咖啡,可以加牛奶、加摩卡、加奶泡,每种配料都在基础价格上叠加费用,而最终顾客喝到的仍是一杯"咖啡"。这种"不改变原始对象,通过层层包装来扩展功能"的思想,就是装饰器模式(Decorator Pattern)。它与代理模式外形相似但目的截然不同,本文将从辨析入手,深入剖析装饰器在Java IO流中的经典应用。

装饰器 vs 继承 :如果不用装饰器模式,你会怎么实现"浓缩咖啡+牛奶+摩卡"?一种做法是创建EspressoWithMilkEspressoWithMochaEspressoWithMilkAndMocha等子类------这就是"类爆炸"问题,3种配料就要2^3=8个子类,4种配料就要16个------完全没有可维护性。装饰器模式用组合替代继承,将每种配料设计为独立的装饰器类,可以任意组合叠加。这也是《Design Patterns》中"组合优于继承"原则的经典体现。

一、装饰器模式核心思想

装饰器模式的核心是:动态地给一个对象添加额外的职责,同时不改变其接口

java 复制代码
// 组件接口
interface Coffee {
    String getDescription();
    double cost();
}

// 具体组件(被装饰的主体)
class Espresso implements Coffee {
    @Override
    public String getDescription() {
        return "浓缩咖啡";
    }
    @Override
    public double cost() {
        return 15.0;
    }
}

class HouseBlend implements Coffee {
    @Override
    public String getDescription() {
        return "混合咖啡";
    }
    @Override
    public double cost() {
        return 12.0;
    }
}

// 抽象装饰器 - 持有组件的引用
abstract class CoffeeDecorator implements Coffee {
    protected Coffee coffee;  // 被装饰的对象

    public CoffeeDecorator(Coffee coffee) {
        this.coffee = coffee;
    }

    @Override
    public String getDescription() {
        return coffee.getDescription();
    }
    @Override
    public double cost() {
        return coffee.cost();
    }
}

// 具体装饰器:牛奶
class Milk extends CoffeeDecorator {
    public Milk(Coffee coffee) {
        super(coffee);
    }
    @Override
    public String getDescription() {
        return coffee.getDescription() + " + 牛奶";
    }
    @Override
    public double cost() {
        return coffee.cost() + 3.0;
    }
}

// 具体装饰器:摩卡
class Mocha extends CoffeeDecorator {
    public Mocha(Coffee coffee) {
        super(coffee);
    }
    @Override
    public String getDescription() {
        return coffee.getDescription() + " + 摩卡";
    }
    @Override
    public double cost() {
        return coffee.cost() + 5.0;
    }
}

// 具体装饰器:奶泡
class Whip extends CoffeeDecorator {
    public Whip(Coffee coffee) {
        super(coffee);
    }
    @Override
    public String getDescription() {
        return coffee.getDescription() + " + 奶泡";
    }
    @Override
    public double cost() {
        return coffee.cost() + 4.0;
    }
}

使用装饰器模式构建一杯"浓缩咖啡+牛奶+摩卡+奶泡":

java 复制代码
public class CoffeeShop {
    public static void main(String[] args) {
        // 基础咖啡
        Coffee coffee = new Espresso();
        System.out.println(coffee.getDescription() + ": ¥" + coffee.cost());
        // 浓缩咖啡: ¥15.0

        // 层层装饰
        coffee = new Milk(coffee);
        coffee = new Mocha(coffee);
        coffee = new Whip(coffee);

        System.out.println(coffee.getDescription() + ": ¥" + coffee.cost());
        // 浓缩咖啡 + 牛奶 + 摩卡 + 奶泡: ¥27.0
    }
}

这种设计的精妙之处在于:咖啡配料可以任意组合,新增配料只需添加一个装饰器类,完全符合开闭原则。

顺序重要吗? 在装饰器模式中,装饰器的叠加顺序有时会影响结果。比如先加密再压缩和先压缩再加密,最终得到的数据完全不同。虽然装饰器模式本身不强制顺序,但在实际使用中,你需要根据业务逻辑确定装饰器的调用链。Java IO流中也有类似情况:你应该先包装BufferedInputStream再包DataInputStream,而不是反过来,因为DataInputStream会做小批量读取,需要底层的缓冲来提升性能。

二、装饰器模式 vs 代理模式

这两个模式在结构上非常相似------都持有目标对象的引用,都实现相同的接口。但它们的目的截然不同:

维度 装饰器模式 代理模式
核心目的 增强功能/添加职责 控制访问/屏蔽细节
增强内容 使用者知道增强了什么(加牛奶) 使用者不知道代理做了什么(日志)
关注点 扩展对象的能力 控制对象的访问
关系 运行时动态组合(可多层) 一般一对一静态绑定
典型例子 Java IO流 Spring AOP

一句话总结:装饰器是"锦上添花",代理是"偷梁换柱"

如何记忆这个区别? 一个简单的口诀------"装饰器是透明的,代理是不透明的"。装饰器的使用者在代码中清醒地知道自己在包装什么(new Milk(new Espresso())),知道加了牛奶;而代理的使用者通常拿到的是一个已经创建好的代理对象,不知道背后有代理的存在。在Spring AOP中,你注入的Service对象其实是个代理,但代码中完全感知不到------这就是"不透明"。

三、Java IO流中的装饰器模式

Java的java.io包是装饰器模式最经典的应用,几乎所有流都通过装饰器扩展功能:

java 复制代码
import java.io.*;

// 基础组件:文件输入流(只能按字节读取)
InputStream fileInput = new FileInputStream("data.txt");

// 装饰器1:缓冲(添加缓冲区,提升性能)
BufferedInputStream bufferedInput = new BufferedInputStream(fileInput);

// 装饰器2:数据读取(添加按基本类型读取的能力)
DataInputStream dataInput = new DataInputStream(bufferedInput);

// 链式调用写法
DataInputStream in = new DataInputStream(
        new BufferedInputStream(
                new FileInputStream("data.txt")));

再看一个实际的文件读写示例,体会IO流中装饰器的嵌套之美:

java 复制代码
import java.io.*;

public class IODecoratorDemo {
    public static void main(String[] args) {
        String filePath = "demo.txt";

        // 写入文件(装饰器嵌套)
        try (BufferedWriter writer = new BufferedWriter(
                new OutputStreamWriter(
                        new FileOutputStream(filePath), "UTF-8"))) {
            writer.write("Hello 装饰器模式");
            writer.newLine();
            writer.write("Java IO流是装饰器模式的经典应用");
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 读取文件(装饰器嵌套)
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(
                        new FileInputStream(filePath), "UTF-8"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

来分析这个写入链new BufferedWriter(new OutputStreamWriter(new FileOutputStream(p), "UTF-8"))中每层装饰器的职责:

  • FileOutputStream:基础组件,将字节写入文件
  • OutputStreamWriter:装饰器,将字符流转换为字节流,指定UTF-8编码
  • BufferedWriter:装饰器,添加缓冲功能,批量写入减少IO次数

每一层只负责一件单一功能,通过层层包装组合出所需的能力。这比创建一个"带缓冲带编码的文件写入器"更加灵活、可复用。

IO流的装饰器嵌套有一个问题:关闭顺序 。幸运的是,Java IO流的装饰器设计考虑了这一点------只需关闭最外层的装饰器流,它会自动调用内层流的close()。比如new BufferedWriter(new OutputStreamWriter(new FileOutputStream(p))),关闭BufferedWriter就会级联关闭OutputStreamWriterFileOutputStream。所以在try-with-resources中,只需将最外层流声明在try括号里即可。但注意,如果你手动调用多个流的close(),千万不要先关内层再关外层------外层的flush()close()依赖内层流,先关内层会导致IOException: Stream Closed

四、装饰器模式在真实项目中的应用

以Web开发中的数据加密传输为例:

java 复制代码
// 基础数据处理器
interface DataProcessor {
    String process(String data);
}

class PlainDataProcessor implements DataProcessor {
    @Override
    public String process(String data) {
        return data;
    }
}

// 装饰器:压缩
class CompressDecorator extends AbstractDecorator {
    public CompressDecorator(DataProcessor processor) {
        super(processor);
    }
    @Override
    public String process(String data) {
        String processed = super.process(data);
        return "[压缩]" + processed + "[/压缩]";
    }
}

// 装饰器:加密
class EncryptDecorator extends AbstractDecorator {
    public EncryptDecorator(DataProcessor processor) {
        super(processor);
    }
    @Override
    public String process(String data) {
        String processed = super.process(data);
        return "[加密]" + processed + "[/加密]";
    }
}

// 抽象装饰器基类
abstract class AbstractDecorator implements DataProcessor {
    protected DataProcessor processor;
    public AbstractDecorator(DataProcessor processor) {
        this.processor = processor;
    }
    @Override
    public String process(String data) {
        return processor.process(data);
    }
}

// 测试
public class DataPipelineDemo {
    public static void main(String[] args) {
        // 构建处理链:加密 → 压缩
        DataProcessor pipeline = new EncryptDecorator(
                new CompressDecorator(
                        new PlainDataProcessor()));

        String result = pipeline.process("用户敏感数据123");
        System.out.println(result);
        // 输出: [加密][压缩]用户敏感数据123[/压缩][/加密]
    }
}

总结

装饰器模式的核心优势在于组合优于继承 。通过将功能拆分为独立的装饰器类,可以像搭积木一样灵活组合出所需的功能。Java IO流是装饰器模式的教科书级应用,理解了IO流的设计,也就真正掌握了装饰器模式。它与代理模式形似而神异:装饰器强调功能增强 ,代理强调访问控制。在实际开发中,当需要为对象动态叠加多种能力时,装饰器模式是你的首选方案。

一点补充:装饰器模式也有它的"阿喀琉斯之踵"------装饰器类和被装饰对象必须实现相同的接口,这意味着如果原始接口设计得不好(方法过多或不合理),装饰器也必须实现所有这些方法。此外,如果想用装饰器移除某个功能(而不是增强),它也无能为力------装饰器只能"做加法",不能"做减法"。在这些场景下,代理模式或适配器模式可能是更好的选择。

✅ 亮点总结

  • 装饰器模式完美诠释"组合优于继承"------功能叠加用组合,避免子类爆炸
  • Java IO流的装饰器体系剖析:BufferedInputStream(new FileInputStream(path)) 就是层层装饰
  • 数据管道实战:加密 → 压缩 → 基础处理,三个装饰器自由组合出6种处理链
  • 抽象装饰器基类(AbstractDecorator)的设计,减少具体装饰器的重复代码
  • 装饰器 vs 代理模式的核心区别------功能增强 vs 访问控制,面试重点辨析

适用场景

  • 数据处理管道------日志脱敏 + 加密 + 压缩,多步处理自由编排顺序
  • 中间件拦截链------按需叠加缓存、限流、鉴权等功能到服务调用上
  • UI组件扩展------基础文本框 + 滚动条装饰器 + 边框装饰器 + 阴影装饰器

扩展方向

  • Java IO源码阅读 :从 InputStreamFilterInputStreamBufferedInputStream 的继承链理解装饰器模式
  • 责任链模式对比:另一个支持功能叠加的模式,与装饰器的职责差异和各自适用场景
  • Spring中的装饰器应用TransactionAwareCacheDecoratorServerHttpRequestDecorator 等Spring内部装饰器(推荐阅读上一篇:Java设计模式之观察者模式)
相关推荐
折哥的程序人生 · 物流技术专研1 小时前
Java 23 种设计模式:从踩坑到精通 | 组合模式 —— 树形结构处理,部分与整体一视同仁
java·组合模式·java面试·springsecurity·结构型模式·java设计模式·从踩坑到精通
郝学胜-神的一滴1 小时前
完全二叉树与堆底层原理深度剖析 | 手写C++大顶堆实现
java·开发语言·数据结构·c++·python·算法
农民小飞侠1 小时前
[leetcode] 165. Compare Version Numbers
java·算法·leetcode
砍材农夫1 小时前
物联网实战|Spring Boot + Netty 搭建 MQTT 消息路由与流转层
java·spring boot·后端·物联网·spring
黄毛火烧雪下2 小时前
Java 基础笔记:文件、递归与字符编码
java·开发语言·笔记
学计算机的计算基2 小时前
链表算法上篇:LeetCode 206/234/141/142/160/21 题解与易错点
java·笔记·算法·链表
信也科技布道师2 小时前
从Istio 503 NC 错误深入理解 Mesh 路由全链路原理
java·服务器·网络
swordbob2 小时前
3 大 I/O 模型BIO / NIO / AIO
java·linux·spring
Pluto_CSND2 小时前
Cron表达式使用说明
java