结构型模式:外观、享元与代理
结构型模式的「第三幕」
前面两节我们认识了四种结构型模式:适配器模式像转接头一样解决接口不匹配的问题,组合模式用递归树形结构统一处理整体与部分,桥接模式将抽象与实现分离让它们独立变化,装饰者模式在不修改原有对象的前提下动态添加职责。
这些模式都围绕一个共同主题:如何把类和对象「拼」成更大的结构。但结构上的问题还远没有穷尽——
当你面对一个由几十个类交织而成的子系统时,你真的愿意搞清楚每个类的作用再去调用吗?当你的围棋程序需要在棋盘上放下 361 个棋子对象,每个棋子还带着颜色和坐标信息时,难道不能更省点内存吗?当你想在访问某个对象之前先做权限检查、日志记录或者懒加载,却不想修改这个对象本身时,该怎么办?
本节介绍最后三种结构型模式,它们分别回答上面三个问题:
- 外观模式——化繁为简:为复杂的子系统提供一个「总入口」
- 享元模式——以少胜多:用共享技术减少大量相似对象的内存开销
- 代理模式——以代制直:通过「替身」控制对原始对象的访问
外观模式
你需要一个「前台」
想象你第一天去一家大公司办事。公司有法务部、财务部、人事部、技术部——你的事情可能同时涉及其中三个部门。如果没有前台,你得自己跑遍每个部门,搞清楚先找谁、后找谁、每个部门的办事流程是什么。但如果有一个前台,你只需要把需求告诉前台,前台会帮你协调所有部门,最后把结果给你。
前台不会为你做法务、财务或技术的工作——这些还是由各部门完成。前台只是简化了你和这个复杂组织之间的交互。
外观模式做的正是同样的事情。
模式定义
外观模式
外观模式(Facade Pattern)为子系统中的一组接口提供一个一致的界面,定义了一个高层接口,使得这一子系统更加容易使用。
Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use.
别名:门面模式。属于对象结构型模式。
简单来说,外观模式在客户端和复杂子系统之间加了一层——客户端只需要和外观对象交互,外观对象负责协调子系统内部的多个组件。客户端不需要知道子系统的内部结构,也不需要和子系统中的多个对象直接打交道。
模式中只有两个角色:
| 角色 | 职责 |
|---|---|
| Facade(外观角色) | 客户端直接调用的角色,知道子系统各组件的功能和责任,将客户端请求委派给相应的子系统对象 |
| SubSystem(子系统角色) | 可以是一个或多个类的集合,实现子系统的具体功能。子系统不知道外观的存在——它只是普通的类 |
模式结构
classDiagram
direction LR
class Facade {
-subSystemA : SubSystemA
-subSystemB : SubSystemB
-subSystemC : SubSystemC
+method() void
}
class SubSystemA {
+methodA() void
}
class SubSystemB {
+methodB() void
}
class SubSystemC {
+methodC() void
}
class Client {
}
Client --> Facade
Facade --> SubSystemA
Facade --> SubSystemB
Facade --> SubSystemC
从结构图可以看出外观模式的关键特点:客户端只依赖 Facade,不直接依赖任何子系统类。子系统之间可以有内部依赖,但这些复杂性被 Facade 封装起来了。
外观模式的代码实现非常直接——Facade 持有各子系统的引用,在自己的方法中按正确的顺序调用子系统的方法:
1 2 3 4 5 6 7 8 9 10 11 12 | public class Facade { private SubSystemA subA = new SubSystemA(); private SubSystemB subB = new SubSystemB(); private SubSystemC subC = new SubSystemC(); // 外观方法——封装子系统的交互逻辑 public void method() { subA.methodA(); subB.methodB(); subC.methodC(); } } |
客户端只需要:
1 2 | Facade facade = new Facade(); facade.method(); // 一行搞定,不需要知道子系统的存在 |
外观模式的设计哲学
外观模式看似简单,但背后是两个重要的设计原则在支撑。
单一职责原则告诉我们应该将复杂系统分解为职责单一的子系统。但子系统多了,客户端的使用成本也会上升——需要知道找哪些类、调用顺序是什么。外观模式在「分」和「合」之间找到了平衡:子系统的内部继续保持良好的模块化,但对外提供一个统一的简单入口。
迪米特法则要求一个对象尽量少地了解其他对象。外观模式创造了一个「中间层」,将客户端需要直接交互的对象数量从「子系统中的 N 个类」减少为「1 个外观类」——这正是迪米特法则的经典实践。
值得强调的是:外观模式并不阻止客户端直接使用子系统类。它只是提供了一个更简单的选择。当客户端需要精细控制子系统的行为时,仍然可以绕过外观直接与子系统交互。外观是「方便的快捷方式」,不是「强制的围墙」。
实例:电源总开关
一个简单但直观的例子:家里有四盏灯、一个风扇、一台空调和一台电视机,每个设备都有独立的开关。如果每天晚上睡觉前要逐一关闭所有设备,会很麻烦。一个电源总开关就是所有设备的外观——按一下总开关,所有设备同时关闭。
classDiagram
class GeneralSwitch {
-lights : Light[]
-fan : Fan
-ac : AirConditioner
-tv : Television
+on() void
+off() void
}
class Light {
+on() void
+off() void
}
class Fan {
+on() void
+off() void
}
class AirConditioner {
+on() void
+off() void
}
class Television {
+on() void
+off() void
}
GeneralSwitch --> Light
GeneralSwitch --> Fan
GeneralSwitch --> AirConditioner
GeneralSwitch --> Television
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | public class GeneralSwitch { private Light[] lights = new Light[4]; private Fan fan = new Fan(); private AirConditioner ac = new AirConditioner(); private Television tv = new Television(); public GeneralSwitch() { for (int i = 0; i < 4; i++) { lights[i] = new Light("灯" + (i + 1)); } } public void on() { for (Light light : lights) light.on(); fan.on(); ac.on(); tv.on(); System.out.println("=== 所有设备已开启 ==="); } public void off() { for (Light light : lights) light.off(); fan.off(); ac.off(); tv.off(); System.out.println("=== 所有设备已关闭 ==="); } } |
用户不再需要记住有哪些设备、每个设备怎么操作——一个 GeneralSwitch 搞定一切。
实例:文件加密
再看一个更贴近软件开发的例子。某系统需要一个文件加密模块,流程分三步:读取源文件 → 加密 → 保存加密后的文件。这三个操作由三个独立的类实现:
classDiagram
direction LR
class EncryptFacade {
-reader : FileReader
-cipher : CipherMachine
-writer : FileWriter
+fileEncrypt(String src, String dst) void
}
class FileReader {
+read(String path) String
}
class CipherMachine {
+encrypt(String plainText) String
}
class FileWriter {
+write(String data, String path) void
}
EncryptFacade --> FileReader
EncryptFacade --> CipherMachine
EncryptFacade --> FileWriter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | // 三个子系统类各司其职 public class FileReader { public String read(String path) { System.out.println("读取文件:" + path); // 使用流读取文件内容 return "文件内容..."; } } public class CipherMachine { public String encrypt(String plainText) { System.out.println("加密数据..."); // 执行加密算法 return "加密后的内容"; } } public class FileWriter { public void write(String data, String path) { System.out.println("写入文件:" + path); // 使用流写入文件 } } // 加密外观——一行调用完成三步操作 public class EncryptFacade { private FileReader reader = new FileReader(); private CipherMachine cipher = new CipherMachine(); private FileWriter writer = new FileWriter(); public void fileEncrypt(String srcPath, String dstPath) { String data = reader.read(srcPath); // 步骤 1:读取 String encrypted = cipher.encrypt(data); // 步骤 2:加密 writer.write(encrypted, dstPath); // 步骤 3:保存 } } |
客户端只需调用 facade.fileEncrypt("input.txt", "output.dat"),完全不需要知道背后有几个类、调用顺序是什么。更重要的是,如果将来更换加密算法(比如从 Caesar 换成 AES),只需修改 CipherMachine 或替换为新的子系统类,外观的接口保持不变。
优缺点与适用场景
优点:
| 优点 | 说明 |
|---|---|
| 简化使用 | 屏蔽子系统组件,减少客户端需要交互的对象数量 |
| 松耦合 | 子系统内部变化不影响客户端,只需调整外观类 |
| 降低编译依赖 | 编译一个子系统一般不需要编译其他子系统,简化了跨平台移植 |
| 不限制直接访问 | 外观只是快捷通道,不阻止高级用户直接使用子系统 |
缺点:
- 难以限制客户端:如果过度限制客户端只能通过外观访问子系统,会降低灵活性
- 可能违背开闭原则:增加新的子系统时,如果不引入抽象外观类,可能需要修改现有的外观类
适用场景:
- 需要为一个复杂子系统提供简单接口——大多数用户只需要默认行为,高级用户可以绕过外观
- 客户程序与多个子系统之间存在高度依赖——引入外观将子系统与客户端解耦
- 层次化结构中定义入口——在分层架构中,可以为每一层提供一个外观作为入口,层与层之间通过外观通信,降低层间耦合
模式扩展
抽象外观类
外观模式最大的缺点是违背开闭原则——新增或替换子系统时需要修改外观类。解决方案是引入抽象外观类:客户端面向抽象外观编程,新的业务需求对应一个新的具体外观类,通过配置文件切换外观实现。
classDiagram
class AbstractFacade {
<<abstract>>
+method()* void
}
class FacadeA {
+method() void
}
class FacadeB {
+method() void
}
class Client {
}
AbstractFacade <|-- FacadeA
AbstractFacade <|-- FacadeB
Client --> AbstractFacade
这样,增加新子系统时不需要修改已有代码,只需添加新的具体外观类并更新配置——符合开闭原则。
外观与单例
一个系统中通常只需要一个外观对象。为了节约资源,外观类常被设计为单例。但这并不意味着系统中只能有一个外观类——完全可以为不同的子系统组合提供不同的外观类,每个外观类负责和一组特定的子系统交互。
外观不是新行为的容器
不要通过外观类为子系统增加新的行为。外观的职责是简化现有接口,不是添加新功能。如果需要新行为,应该修改原有子系统类或增加新的子系统类,而不是把新逻辑塞进外观里。外观是沟通渠道,不是功能容器。
实际应用
JDBC 操作封装。在使用 JDBC 进行数据库操作时,完整流程包括加载驱动、获取连接、创建语句、执行查询、处理结果集、关闭资源——涉及多个类和复杂的异常处理。一个 JDBCFacade 可以将这些步骤封装起来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public class JDBCFacade { private Connection conn = null; private Statement stmt = null; public void open(String driver, String url, String user, String pwd) { // 加载驱动 + 获取连接 } public ResultSet executeQuery(String sql) { // 创建语句 + 执行查询 } public int executeUpdate(String sql) { // 创建语句 + 执行更新 } public void close() { // 关闭语句 + 关闭连接 } } |
客户端只需 open → executeQuery → close,不必操心连接管理、资源释放等细节。
SLF4J。Java 日志领域的 SLF4J(Simple Logging Facade for Java)——名字里就带着 "Facade"——正是外观模式的体现。SLF4J 为各种日志框架(Log4j、java.util.logging、Logback 等)提供了一套统一的 API。开发者面向 SLF4J 接口编程,底层日志实现可以通过配置自由切换,应用代码无需修改。
Session Facade。在 Java EE 架构中,Session Facade 模式使用一个 Session Bean 作为外观,封装业务逻辑层中多个 Entity Bean 的交互。客户端(通常是 Web 层)不需要直接与多个 Entity Bean 打交道,只需调用 Session Bean 的方法。
享元模式
外观模式解决的是「交互太复杂」的问题——子系统的功能没问题,只是使用起来太麻烦。接下来要面对的则是一个截然不同的问题:当系统中需要创建大量相同或相似的对象时,内存怎么扛得住?
当对象多到爆炸
想象你在开发一个文本编辑器。一份文档中可能出现几万个字符,每个字符都有字体、大小、颜色等属性。如果为每个字符都创建一个独立的对象,包含字符值、字体、大小、颜色、位置等信息——假设每个对象 100 字节,一份 10 万字的文档就需要约 的内存来存储字符对象。
但仔细想想,这些字符对象中有大量的重复信息。英文文档用到的字符种类不过几十种(字母、数字、标点),中文文档常用字也不过几千种。字体、大小、颜色的组合也是有限的。真正因字符而异的只有位置——每个字符在文档中出现的坐标不同。
如果我们能让所有相同的字符共享一个对象,而把位置这种因实例而异的信息提取出来由外部管理,内存占用会大幅下降。
这就是享元模式的核心思想:通过共享技术来复用细粒度对象。
内部状态与外部状态
享元模式能够实现共享的关键,在于区分两种状态:
内部状态(Intrinsic State)是存储在享元对象内部、不会随环境改变而改变的状态。比如字符的 Unicode 值、字体名——无论这个字符出现在文档的哪个位置,这些属性都是一样的。内部状态可以共享。
外部状态(Extrinsic State)是随环境改变而改变的、不可共享的状态。比如字符在文档中的坐标位置——同一个字符 "a" 出现在第 1 行和第 100 行,位置不同。外部状态由客户端保存,在使用享元对象时作为参数传入。一个重要的性质是:不同的外部状态之间是相互独立的,一个外部状态的变化不会影响其他外部状态。
一个享元对象 = 可共享的内部状态。外部状态不存储在享元对象中,而是在需要时从外部传入。这种分离使得一个享元对象可以在不同的上下文中被复用——虽然每次使用时的外部状态不同,但对象本身只需要一份。
模式定义
享元模式
享元模式(Flyweight Pattern)运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。
Use sharing to support large numbers of fine-grained objects efficiently.
别名:轻量级模式。属于对象结构型模式。
模式结构
classDiagram
class Flyweight {
<<abstract>>
-intrinsicState
+operation(extrinsicState)* void
}
class ConcreteFlyweight {
-intrinsicState
+operation(extrinsicState) void
}
class UnsharedConcreteFlyweight {
-allState
+operation(extrinsicState) void
}
class FlyweightFactory {
-flyweights : Map~String, Flyweight~
+getFlyweight(String key) Flyweight
}
class Client {
}
Flyweight <|-- ConcreteFlyweight
Flyweight <|-- UnsharedConcreteFlyweight
FlyweightFactory o--> Flyweight : flyweights
Client --> FlyweightFactory
Client --> Flyweight
四个角色:
| 角色 | 职责 |
|---|---|
| Flyweight(抽象享元类) | 声明享元对象的接口,通过参数接受外部状态 |
| ConcreteFlyweight(具体享元类) | 实现享元接口,存储内部状态。实例是可共享的 |
| UnsharedConcreteFlyweight(非共享具体享元类) | 并非所有享元子类都需要共享,不能被共享的子类可以直接实例化 |
| FlyweightFactory(享元工厂类) | 创建和管理享元对象,维护一个享元池(Flyweight Pool) |
享元工厂——共享的枢纽
享元模式的核心在于享元工厂。工厂维护一个享元池(通常用 HashMap 实现),当客户端请求享元对象时,工厂先检查池中是否已有该对象——有则直接返回,没有则创建新对象并放入池中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class FlyweightFactory { private Map<String, Flyweight> pool = new HashMap<>(); public Flyweight getFlyweight(String key) { if (pool.containsKey(key)) { return pool.get(key); // 已有,直接复用 } else { Flyweight fw = new ConcreteFlyweight(key); pool.put(key, fw); // 新建并缓存 return fw; } } public int getPoolSize() { return pool.size(); } } |
享元类将内部状态作为成员属性存储,外部状态通过方法参数传入:
1 2 3 4 5 6 7 8 9 10 11 12 13 | public class ConcreteFlyweight extends Flyweight { private String intrinsicState; // 内部状态——可共享 public ConcreteFlyweight(String intrinsicState) { this.intrinsicState = intrinsicState; } @Override public void operation(String extrinsicState) { System.out.println("内部状态: " + intrinsicState + ", 外部状态: " + extrinsicState); } } |
这种「先查池、再创建」的逻辑,正是享元工厂与普通工厂的区别——普通工厂每次都创建新对象,享元工厂尽可能复用已有对象。
实例:共享网络设备
来看一个具体的例子。在计算机网络中,交换机和集线器等网络设备可以被多台终端共享——多台计算机连接同一台交换机。
无外部状态的版本
先看最简单的情况——只考虑设备类型的共享,不涉及端口分配:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | // 抽象网络设备(享元接口) public interface NetworkDevice { String getType(); void use(); } // 具体享元——集线器 public class Hub implements NetworkDevice { private String type = "集线器"; // 内部状态 @Override public String getType() { return type; } @Override public void use() { System.out.println("使用 " + type); } } // 具体享元——交换机 public class Switch implements NetworkDevice { private String type = "交换机"; // 内部状态 @Override public String getType() { return type; } @Override public void use() { System.out.println("使用 " + type); } } // 享元工厂 public class DeviceFactory { private Map<String, NetworkDevice> pool = new HashMap<>(); public NetworkDevice getDevice(String type) { if (!pool.containsKey(type)) { if ("Hub".equals(type)) { pool.put(type, new Hub()); } else if ("Switch".equals(type)) { pool.put(type, new Switch()); } System.out.println("新建设备:" + type); } return pool.get(type); } public int getPoolSize() { return pool.size(); } } |
1 2 3 4 5 6 7 | DeviceFactory factory = new DeviceFactory(); NetworkDevice d1 = factory.getDevice("Hub"); // 新建 NetworkDevice d2 = factory.getDevice("Hub"); // 复用! NetworkDevice d3 = factory.getDevice("Switch"); // 新建 System.out.println(d1 == d2); // true —— 同一个对象 System.out.println("池中设备数: " + factory.getPoolSize()); // 2 |
无论多少台计算机「使用」集线器,池中始终只有一个 Hub 对象。
有外部状态的版本
现实中,虽然多台计算机可以共享一台交换机,但每台计算机使用的端口(Port)是不同的——端口号就是外部状态。我们将端口从设备对象中提取出来,在使用时作为参数传入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public interface NetworkDevice { String getType(); void use(int port); // 端口号作为外部状态传入 } public class Switch implements NetworkDevice { private String type = "交换机"; // 内部状态 @Override public String getType() { return type; } @Override public void use(int port) { System.out.println("使用 " + type + " 的端口 " + port); } } |
1 2 3 4 5 | NetworkDevice sw = factory.getDevice("Switch"); sw.use(1); // 计算机 A 使用端口 1 sw.use(2); // 计算机 B 使用端口 2 sw.use(3); // 计算机 C 使用端口 3 // 三次调用共享同一个 Switch 对象,只是端口号不同 |
同一个 Switch 对象在不同的端口号上被复用——内部状态(设备类型)共享,外部状态(端口号)由客户端管理。
优缺点与适用场景
优点:
| 优点 | 说明 |
|---|---|
| 大幅减少对象数量 | 相同或相似对象在内存中只保存一份 |
| 外部状态独立 | 外部状态不影响内部状态,享元对象可在不同环境中共享 |
缺点:
| 缺点 | 说明 |
|---|---|
| 系统复杂化 | 需要分离内部/外部状态,增加了设计复杂度 |
| 运行时间增加 | 外部状态需要从外部传入而非直接访问,增加了一定的运行开销 |
适用场景:
- 系统中有大量相同或相似的对象,造成内存的大量耗费
- 对象的大部分状态都可以外部化,可以将外部状态传入对象中
- 享元池需要耗费资源维护,因此应当在多次重复使用享元对象时才值得使用
使用享元模式的前提是对象确实大量重复。如果对象数量不多,引入享元模式反而增加了不必要的复杂性。只有在对象数量大到足以影响系统性能时,享元模式的优势才能显现。
实际应用
Java String 池。String 是 Java 中享元模式最经典的应用。JVM 维护一个字符串常量池——相同内容的字符串字面量在池中只存储一份:
1 2 3 4 5 6 7 8 9 | String s1 = "abcd"; String s2 = "abcd"; String s3 = "ab" + "cd"; // 编译期常量折叠 String s4 = "ab"; s4 += "cd"; // 运行时拼接,创建新对象 System.out.println(s1 == s2); // true —— 池中同一对象 System.out.println(s1 == s3); // true —— 编译器优化为 "abcd" System.out.println(s1 == s4); // false —— s4 是运行时新建的 |
s1 和 s2 指向常量池中同一个 "abcd" 对象——这就是享元。s3 因为是两个字面量拼接,编译器在编译期就计算出了结果 "abcd" 并指向池中已有对象。但 s4 涉及运行时拼接(+=),会创建新的 String 对象,不再指向池中对象。
Integer 缓存。Integer.valueOf() 方法对 范围内的整数使用了享元模式——这个范围内的 Integer 对象被预先创建并缓存:
1 2 3 4 5 6 7 | Integer a = Integer.valueOf(100); Integer b = Integer.valueOf(100); System.out.println(a == b); // true —— 缓存命中 Integer c = Integer.valueOf(200); Integer d = Integer.valueOf(200); System.out.println(c == d); // false —— 超出缓存范围,新建对象 |
Integer.valueOf() 就是一个享元工厂——先检查值是否在缓存范围内,是则返回缓存对象,否则创建新对象。这也是为什么 Java 编码规范推荐使用 Integer.valueOf() 而非 new Integer() 的原因之一。
编辑器与游戏。文档编辑器中多次出现的相同图片只需创建一个图片对象,不同位置通过外部状态设置。游戏中大量重复的图形元素(如森林中的树、棋盘上的棋子)也是典型应用——树的纹理和模型(内部状态)共享,每棵树的位置和大小(外部状态)各不相同。
模式扩展
单纯享元与复合享元
单纯享元模式中所有享元对象都是可共享的——不存在非共享具体享元类。这是最简单的形式。
复合享元模式将享元模式与组合模式结合:将多个单纯享元对象组合成一个复合享元对象。复合享元本身不能共享(因为它包含不同组合的单纯享元),但它内部的单纯享元仍然是共享的。这种方式可以统一地为一组享元对象设置外部状态。
与其他模式的联用
享元模式经常与其他模式搭配使用:
- 享元工厂中常用简单工厂模式来创建享元对象
- 享元工厂通常设计为单例——系统中只需一个工厂来管理享元池
- 结合组合模式可以构建复合享元,统一管理多个享元对象的外部状态
代理模式
外观模式是「给复杂系统加个前台」,享元模式是「让相似对象共享内存」。接下来的代理模式要解决的是又一个不同的问题:当你不想(或不能)直接访问一个对象时,怎么通过一个「替身」来间接访问它?
为什么需要「替身」?
日常生活中,代理无处不在。你找律师代替你出庭——你是当事人,律师是代理。你在国内买海外商品——商品在海外仓库,代购帮你搞定一切。你在公司找不到总经理——先去找秘书问问什么时候有空。
在软件系统中,我们需要代理的原因也类似:
- 对象在远程机器上:直接访问需要处理网络通信,但客户端不想管这些
- 对象创建代价很高:比如加载一张高清大图需要几秒,但用户只是在浏览缩略图列表,没必要立即加载所有原图
- 需要控制访问权限:不同角色的用户能做的操作不同
- 需要在访问前后附加功能:如日志记录、性能监控、事务管理
在这些场景中,我们不直接访问真实对象,而是通过一个与真实对象实现了相同接口的代理对象来间接访问——代理对象在内部持有真实对象的引用,可以在转发请求的前后做额外的事情。
模式定义
代理模式
代理模式(Proxy Pattern)给某一个对象提供一个代理,并由代理对象控制对原对象的引用。
Provide a surrogate or placeholder for another object to control access to it.
别名:替身模式(Surrogate)。属于对象结构型模式。
模式结构
classDiagram
direction LR
class Subject {
<<interface>>
+request()* void
}
class RealSubject {
+request() void
}
class Proxy {
-realSubject : RealSubject
+request() void
-preRequest() void
-postRequest() void
}
class Client {
}
Subject <|.. RealSubject
Subject <|.. Proxy
Proxy --> RealSubject : realSubject
Client --> Subject
三个角色:
| 角色 | 职责 |
|---|---|
| Subject(抽象主题) | 声明真实主题和代理的共同接口,使代理可以替代真实主题 |
| RealSubject(真实主题) | 实现了具体的业务逻辑,是代理所代表的真实对象 |
| Proxy(代理) | 持有真实主题的引用,实现与真实主题相同的接口,在请求前后可执行附加操作 |
代理类的典型实现——注意 preRequest() 和 postRequest() 的结构,这是代理模式区别于简单转发的关键:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public class Proxy implements Subject { private RealSubject realSubject = new RealSubject(); @Override public void request() { preRequest(); // 前置处理 realSubject.request(); // 委托给真实主题 postRequest(); // 后置处理 } private void preRequest() { System.out.println("前置处理:权限检查、日志记录..."); } private void postRequest() { System.out.println("后置处理:结果缓存、性能统计..."); } } |
代理类和真实主题类实现相同的接口——这意味着客户端可以用完全相同的方式使用代理对象和真实对象,代理的存在对客户端是透明的。
实例:论坛权限控制
在一个论坛系统中,已注册用户可以发帖、修改注册信息、修改帖子;游客只能查看帖子。使用保护代理来实现权限控制:
classDiagram
direction LR
class AbstractPermission {
<<interface>>
+modifyUserInfo()* void
+viewNote()* void
+publishNote()* void
+modifyNote()* void
}
class RealPermission {
+modifyUserInfo() void
+viewNote() void
+publishNote() void
+modifyNote() void
}
class PermissionProxy {
-permission : RealPermission
-level : int
+modifyUserInfo() void
+viewNote() void
+publishNote() void
+modifyNote() void
}
AbstractPermission <|.. RealPermission
AbstractPermission <|.. PermissionProxy
PermissionProxy --> RealPermission : permission
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | public class PermissionProxy implements AbstractPermission { private RealPermission permission = new RealPermission(); private int level; // 0: 游客, 1: 注册用户 public PermissionProxy(int level) { this.level = level; } @Override public void modifyUserInfo() { if (level == 1) { permission.modifyUserInfo(); // 注册用户——放行 } else { System.out.println("对不起,游客无法修改用户信息"); } } @Override public void viewNote() { permission.viewNote(); // 所有人都可以查看 } @Override public void publishNote() { if (level == 1) { permission.publishNote(); } else { System.out.println("对不起,游客无法发布帖子"); } } @Override public void modifyNote() { if (level == 1) { permission.modifyNote(); } else { System.out.println("对不起,游客无法修改帖子"); } } } |
代理在转发请求之前先检查用户权限——这是保护代理的典型行为。客户端只需面向 AbstractPermission 编程,不知道背后是真实对象还是代理在处理。
代理的种类
代理模式根据用途的不同,衍生出多种具体类型。它们的结构几乎相同(都是代理持有真实对象引用并实现相同接口),区别在于代理在转发请求时做的「额外工作」不同:
| 代理类型 | 用途 | 典型场景 |
|---|---|---|
| 远程代理(Remote) | 为位于不同地址空间的对象提供本地代表 | Java RMI、Web Service |
| 虚拟代理(Virtual) | 延迟创建开销大的对象,需要时才真正创建 | 大图片懒加载 |
| 保护代理(Protection) | 控制对象的访问权限 | 论坛权限控制 |
| 缓冲代理(Cache) | 为操作结果提供临时存储,多个客户端可共享 | 数据库查询缓存 |
| Copy-on-Write 代理 | 延迟复制/克隆操作,直到真正需要修改时才执行 | 写时复制容器 |
| 智能引用代理(Smart Reference) | 在访问对象时附加额外操作 | 引用计数、访问日志 |
| 防火墙代理(Firewall) | 保护目标对象不被恶意访问 | 网络安全 |
来重点看两种最常用的代理。
远程代理
远程代理将网络通信的细节隐藏起来。客户端调用本地代理对象的方法,代理在背后通过网络将请求转发给远程服务器上的真实对象,接收响应后返回给客户端。客户端完全感觉不到对象在远程——就像在调用本地方法一样。
sequenceDiagram
participant C as 客户端
participant P as 本地代理 (Stub)
participant R as 远程对象 (RealSubject)
C->>P: request()
P->>R: 序列化 & 网络传输
R->>P: 返回结果
P->>C: 反序列化 & 返回
Java 的 RMI(Remote Method Invocation,远程方法调用)就是远程代理的经典实现。客户端通过 Stub(桩,即代理对象)调用远程方法,Stub 负责将方法调用序列化并通过网络发送给远程 JVM,远程端执行完毕后将结果发回。EJB、Web Service 等分布式技术都是基于同样的代理思想——远程服务器中的企业级 Bean 在本地有一个桩代理,客户端通过桩调用远程对象的方法,网络通信的复杂性完全被封装。
虚拟代理
虚拟代理将开销大的对象的创建推迟到真正需要时。一个常见的例子是网页中的大图加载:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class ImageProxy implements Image { private String path; private RealImage realImage; // 真实大图——延迟创建 public ImageProxy(String path) { this.path = path; } @Override public void display() { if (realImage == null) { System.out.println("加载中,显示占位图..."); realImage = new RealImage(path); // 首次使用时才加载真实大图 } realImage.display(); } } |
用户浏览网页时,先通过代理显示占位图(小图)。只有当用户点击查看大图时,代理才创建 RealImage 对象加载真正的大图。如果用户从不点击,大图永远不会被加载——节省了资源,也加快了页面初始加载速度。
优缺点
优点:
| 优点 | 说明 |
|---|---|
| 调用者与被调用者解耦 | 代理对象在两者之间起到中介作用 |
| 远程代理 | 隐藏网络细节,客户端可以访问远程机器上的对象 |
| 虚拟代理 | 用小对象代表大对象,减少资源消耗,加速系统启动 |
| 保护代理 | 控制对象的使用权限,给不同用户提供不同级别的访问 |
缺点:
- 在客户端和真实对象之间增加了一层间接调用,某些代理类型可能会降低请求处理速度
- 实现代理模式需要额外的工作,某些代理模式(如远程代理)的实现较为复杂
代理 vs 装饰者:形似而神不同
如果你觉得代理模式和装饰者模式长得很像——你的直觉完全正确。从类图上看,它们的结构几乎一模一样:都持有一个被包装对象的引用,都实现了与被包装对象相同的接口。
但它们的意图截然不同:
| 维度 | 代理模式 | 装饰者模式 |
|---|---|---|
| 核心意图 | 控制对对象的访问 | 增强对象的功能 |
| 对象创建 | 代理通常在内部创建或获取真实对象 | 装饰者从外部接收被装饰对象 |
| 使用场景 | 权限控制、懒加载、远程访问 | 动态添加职责,如 I/O 流层层包装 |
| 客户端感知 | 客户端通常不知道代理的存在 | 客户端主动选择装饰组合 |
简单来说:代理模式控制的是谁能访问对象以及何时访问对象,装饰者模式增强的是对象能做什么。
实际应用
Java RMI。远程方法调用是远程代理的经典应用——客户端通过本地 Stub 调用远程服务器上的对象,网络通信的复杂性完全被代理封装。EJB 中的远程调用也基于同样的原理。
Spring AOP。Spring 框架的面向切面编程(Aspect-Oriented Programming, AOP)底层就是代理模式。当你在 Spring 中使用 @Transactional、@Cacheable 等注解时,Spring 会为目标 Bean 创建一个代理对象。调用方法时,代理在真实方法执行前后织入事务管理、缓存处理等横切逻辑——这正是 preRequest() → request() → postRequest() 结构的体现。
Spring 默认使用 JDK 动态代理(要求目标类实现接口)或 CGLIB 代理(通过生成子类,不要求接口)。与前面手写的静态代理不同,动态代理可以在运行时为任意接口创建代理类,避免了为每个接口编写代理类的繁琐:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // JDK 动态代理——一个 Handler 可以代理任意接口 InvocationHandler handler = (proxy, method, args) -> { System.out.println("前置处理:" + method.getName()); Object result = method.invoke(realSubject, args); // 转发给真实对象 System.out.println("后置处理:" + method.getName()); return result; }; Subject proxy = (Subject) Proxy.newProxyInstance( Subject.class.getClassLoader(), new Class[]{Subject.class}, handler ); proxy.request(); // 自动调用 handler 中的逻辑 |
动态代理不需要为每个 Subject 都手写一个 Proxy 类——一个 InvocationHandler 可以为任意接口创建代理。这种灵活性使得 AOP、ORM 等框架的实现成为可能。
三种模式纵览
本节介绍了结构型模式的最后三位成员。它们虽然同属结构型模式,但解决的问题截然不同:
| 维度 | 外观模式 | 享元模式 | 代理模式 |
|---|---|---|---|
| 核心问题 | 子系统太复杂,使用成本高 | 相似对象太多,内存开销大 | 需要控制或间接访问对象 |
| 关键机制 | 提供统一入口,封装子系统 | 共享内部状态,分离外部状态 | 替身转发请求,附加额外功能 |
| 核心角色 | Facade 封装 SubSystem | FlyweightFactory 管理享元池 | Proxy 持有 RealSubject |
| 典型场景 | API 封装、模块入口 | 字符对象、字符串池、棋子 | 权限控制、懒加载、远程调用 |
| 涉及原则 | 迪米特法则、单一职责 | — | — |
至此,七种结构型模式全部登场:
flowchart LR
S[结构型模式] --> A[适配器]
S --> CO[组合]
S --> B[桥接]
S --> D[装饰者]
S --> F[外观]
S --> FW[享元]
S --> P[代理]
A --> AD[接口转换]
CO --> COD[整体-部分统一]
B --> BD[抽象与实现分离]
D --> DD[动态添加职责]
F --> FD[子系统简化]
FW --> FWD[对象共享复用]
P --> PD[间接访问控制]
classDef pattern fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
classDef desc fill:#f8f9fa,stroke:#495057,stroke-width:1px
class S pattern
class A,CO,B,D,F,FW,P pattern
class AD,COD,BD,DD,FD,FWD,PD desc
设计工具箱更新:
| 层次 | 内容 |
|---|---|
| OO 基础 | 抽象、封装、多态、继承 |
| OO 原则 | 封装变化、面向接口编程、组合优于继承、好莱坞原则 |
| OO 模式 | 策略、简单工厂、工厂方法、抽象工厂、建造者、原型、状态、命令、观察者、中介者、模板方法、适配器、组合、桥接、装饰者、外观、享元、代理 |