Java 包装器模式:告别“类爆炸“

目录

一、先说为什么要用它

二、核心原理

三、代码实现

[1. 定义统一接口](#1. 定义统一接口)

[2. 实现核心对象](#2. 实现核心对象)

[3. 抽象包装器](#3. 抽象包装器)

[4. 具体包装器](#4. 具体包装器)

[5. 客户端调用](#5. 客户端调用)

[四、Java 源码里的实际案例](#四、Java 源码里的实际案例)

五、和代理模式的区别

六、优缺点

七、结构总结


在设计模式的标准目录里,包装器模式通常被称为装饰器模式(Decorator Pattern)。不管叫哪个名字,核心思想只有一个:像给礼物包包装纸一样,给对象动态添加功能,而不需要动原来的代码。


一、先说为什么要用它

假设你在做一个咖啡订购系统,最开始只有黑咖啡:

java 复制代码
public class BlackCoffee {
    public double cost() { return 5.0; }
    public String desc() { return "黑咖啡"; }
}

后来要加牛奶、加糖、加摩卡酱。用继承来实现的话,类会长这样:

复制代码
BlackCoffee
MilkCoffee
SugarCoffee
MilkSugarCoffee
MochaMilkSugarCoffee
...

10 种配料,理论上就有 2¹⁰ 种组合,也就是 1024 个类。更麻烦的是,哪天"牛奶"涨价,你可能要翻遍所有涉及牛奶的子类挨个改。

这个问题的根源在于:用继承来表达"功能叠加"本质上是错的。功能叠加是运行时的行为,继承是编译时固定的结构,两者天生不搭。

包装器模式的口号就是:组合优于继承


二、核心原理

理解包装器模式,用"俄罗斯套娃"来比喻最直观:

  • 最里面是核心对象;
  • 外面套一个包装器,它和核心对象长得一样(实现相同接口),但手里握着核心对象的引用;
  • 包装器在调用核心对象前后,可以加自己的逻辑;
  • 包装器外面,还可以再套一层。

调用链长这样:

复制代码
客户调用 → [ 糖包装器 → [ 奶包装器 → [ 黑咖啡 ] ] ]

每一层都只管自己那一点事,职责清晰,随意拆装。


三、代码实现

1. 定义统一接口

不管是原始咖啡还是加了料的咖啡,对外暴露的行为必须一致。

java 复制代码
public interface Coffee {
    double getCost();
    String getDescription();
}

2. 实现核心对象

java 复制代码
public class BlackCoffee implements Coffee {
    @Override
    public double getCost() { return 5.0; }

    @Override
    public String getDescription() { return "黑咖啡"; }
}

3. 抽象包装器

这是整个模式的关键,它同时做两件事:实现 Coffee 接口,并且持有一个 Coffee 对象的引用。

java 复制代码
public abstract class CoffeeDecorator implements Coffee {

    protected Coffee decoratedCoffee;

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

    @Override
    public double getCost() {
        return decoratedCoffee.getCost();
    }

    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription();
    }
}

默认行为是直接委托给内部对象,子类只需要覆盖自己关心的方法。

4. 具体包装器

加牛奶:

java 复制代码
public class MilkDecorator extends CoffeeDecorator {

    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public double getCost() {
        return super.getCost() + 2.0;
    }

    @Override
    public String getDescription() {
        return super.getDescription() + ", 牛奶";
    }
}

加糖:

java 复制代码
public class SugarDecorator extends CoffeeDecorator {

    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public double getCost() {
        return super.getCost() + 1.0;
    }

    @Override
    public String getDescription() {
        return super.getDescription() + ", 糖";
    }
}

5. 客户端调用

java 复制代码
public class Shop {
    public static void main(String[] args) {
        // 只要黑咖啡
        Coffee order1 = new BlackCoffee();
        System.out.println(order1.getDescription() + " : $" + order1.getCost());

        // 黑咖啡 + 牛奶
        Coffee order2 = new MilkDecorator(new BlackCoffee());
        System.out.println(order2.getDescription() + " : $" + order2.getCost());

        // 黑咖啡 + 牛奶 + 糖
        Coffee order3 = new SugarDecorator(new MilkDecorator(new BlackCoffee()));
        System.out.println(order3.getDescription() + " : $" + order3.getCost());
    }
}

输出:

复制代码
黑咖啡 : $5.0
黑咖啡, 牛奶 : $7.0
黑咖啡, 牛奶, 糖 : $8.0

10 种配料,现在只需要 10 个包装器类,想怎么组合怎么组合。


四、Java 源码里的实际案例

这不是玩具代码,Java 核心库里到处都在用这个结构。

Java IO 流是最典型的例子。你一定写过这样的代码:

java 复制代码
BufferedReader reader = new BufferedReader(
    new InputStreamReader(
        new FileInputStream("file.txt")
    )
);

拆开看:FileInputStream 是核心对象,只管读字节;InputStreamReader 包了一层,把字节转成字符;BufferedReader 再包一层,加上缓冲提高效率。三层套娃,每层职责单一,但组合起来功能完整。

Spring 里的 Request 包装也是同理。如果要在 Filter 里统一过滤 XSS,通常这样写:

java 复制代码
HttpServletRequest wrappedRequest = new HttpServletRequestWrapper(request) {
    @Override
    public String getParameter(String name) {
        return super.getParameter(name).replaceAll("<", "&lt;");
    }
};

Spring 提供 HttpServletRequestWrapper 这个基类,就是把抽象包装器这一层帮你写好了,直接继承用就行。


五、和代理模式的区别

结构上两者几乎一样,都持有对象引用,也都实现相同接口,所以很多人会搞混。

区别在于目的

  • 包装器(Decorator) 是为了增强功能,关心的是"这个对象能做什么",比如给咖啡加糖、给 IO 加缓冲;
  • 代理(Proxy) 是为了控制访问,关心的是"这个对象能不能被调用、怎么被调用",比如权限校验、事务管理、远程调用。

一句话区分:装饰器让对象更强,代理让对象更可控。


六、优缺点

优点很明显:不用为每个功能组合单独写子类,运行时可以随意叠加,新增功能也不需要改老代码,符合开闭原则。

缺点也值得注意。层层嵌套导致调试时堆栈很深,出错了要一层一层往里找。初始化代码里会出现 new A(new B(new C())) 这种连续嵌套,可读性一般。另外,包装之后的对象做 instanceof 检查会失效,如果代码里有这种判断要小心。


七、结构总结

复制代码
[ 接口 Interface ]            ← 客户端只依赖这个
       ↑
       ├── [ 核心实现 ]        ← 最里面的娃娃
       │
       └── [ 抽象包装器 ]      ← 持有 Interface 引用
               ↑
               ├── [ 具体包装器 A ]  ← 增强功能 A
               └── [ 具体包装器 B ]  ← 增强功能 B

调用链:客户端 → 包装器 B → 包装器 A → 核心实现

下次如果你发现自己在写 CoffeeWithMilkAndSugarAndMocha 这种类名,停一下,这个模式就该派上用场了。

相关推荐
Yweir1 小时前
Java 接口测试框架 Restassured
java·开发语言
wangbing11251 小时前
开发指南141-类和字节数组转换
java·服务器·前端
~央千澈~1 小时前
抖音弹幕游戏开发之第15集:添加配置文件·优雅草云桧·卓伊凡
java·前端·python
肖。35487870941 小时前
html中onclick误区,后续变量会更改怎么办?
android·java·javascript·css·html
郝学胜-神的一滴1 小时前
Effective Modern C++ 条款39:一次事件通信的优雅解决方案
开发语言·数据结构·c++·算法·多线程·并发
香芋Yu1 小时前
【从零构建AI Code终端系统】02 -- Bash 工具:一切能力的基础
开发语言·bash·agent·claude
码云数智-园园1 小时前
Java Swing 界面美化与 JPanel 优化完全指南:从复古到现代的视觉革命
java·开发语言
@atweiwei1 小时前
Rust 实现 LangChain
开发语言·算法·rust·langchain·llm·agent·rag
舟舟亢亢1 小时前
Java并发编程(下)
java·开发语言