结构型模式:组合与包装的艺术
结构型模式关心的核心问题:类和对象之间怎么组合,才能形成更大、更灵活的结构? 用组合代替继承,用包装扩展功能,让系统像乐高一样随意拼装。
前言
创建型模式管"对象怎么来",结构型模式管"对象怎么搭"。
写代码时经常遇到这类问题:
- 接口不匹配,两个现成组件对不上——适配器
- 想给对象加功能,但不想改原来的类——装饰器
- 系统太复杂,外面用起来太累——外观
- 对象太重,不想一次性全加载——代理
结构型模式一共 7 种,每种解决一类"搭积木"的问题。
一、适配器模式(Adapter)
一句话
把一个类的接口转换成客户端期望的另一个接口,让原本不兼容的类能一起工作。
生活中的例子
- 电源转换头:你带了个美标插头的笔记本去欧洲,插不进去。买一个转换头(适配器),插头没变,插座没变,中间加了一层转换。
- 翻译员:中国老板和外国客户谈生意,语言不通。找个翻译,两边都不需要改变自己的语言,翻译负责转换。
- 读卡器:电脑只有 USB 口,你的数据在 SD 卡里。读卡器就是适配器——把 SD 卡接口适配成 USB 接口。
场景
你系统里用的是自定义的 Logger 接口,但现在要接入第三方日志库,接口完全不同。第三方库不可能为了你改代码,你的系统已有几百处调用也不想改。
示意图

代码实现
Java代码
// ───────────────────────────────────────────
// 第一层:你系统的接口(目标接口)
// 系统里几百处代码都在用这个接口,绝对不能改
// ───────────────────────────────────────────
public interface MyLogger {
// 统一的日志方法:传入级别和消息
// 所有业务代码都依赖这个方法签名
void log(String level, String message);
}
// ───────────────────────────────────────────
// 第二层:第三方日志库的类(被适配者)
// 这是别人写的库,你拿不到源码,改不了
// 它的接口和你的 MyLogger 完全不一样——
// 它不是一个 log() 方法,而是分成 info/error/warn 三个方法
// ───────────────────────────────────────────
public class Slf4jLogger {
public void info(String msg) { /* 输出 INFO 级别日志 */ }
public void error(String msg) { /* 输出 ERROR 级别日志 */ }
public void warn(String msg) { /* 输出 WARN 级别日志 */ }
}
// ───────────────────────────────────────────
// 第三层:适配器(转换头)
// 实现你系统的 MyLogger 接口,内部持有第三方的 Slf4jLogger
// 它的作用就是:接收 log(level, msg) 调用 → 翻译成对应的 info/error/warn 调用
// ───────────────────────────────────────────
public class Slf4jAdapter implements MyLogger {
// 持有第三方日志对象的引用
// 所有实际的日志输出都委托给它
private Slf4jLogger logger;
// 构造时注入第三方对象
public Slf4jAdapter(Slf4jLogger logger) {
this.logger = logger;
}
@Override
public void log(String level, String message) {
// 核心转换逻辑:根据 level 字符串,调用第三方对应的方法
// 这就是"翻译"——把一种调用方式转成另一种
switch (level.toUpperCase()) {
case "INFO": logger.info(message); break; // "INFO" → 调 info()
case "ERROR": logger.error(message); break; // "ERROR" → 调 error()
case "WARN": logger.warn(message); break; // "WARN" → 调 warn()
default: logger.info(message); // 兜底:未知级别当 info 处理
}
}
}
// ───────────────────────────────────────────
// 客户端使用
// 原有代码不需要任何改动!
// 对外暴露的还是 MyLogger 接口,底层已经悄悄换成了 Slf4j
// ───────────────────────────────────────────
// 创建适配器实例,包装住第三方对象
MyLogger logger = new Slf4jAdapter(new Slf4jLogger());
// 业务代码继续调用 log(),完全不知道底层是 Slf4j
logger.log("ERROR", "数据库连接失败");
// 实际执行的是:slf4jLogger.error("数据库连接失败")
python实现
# ───────────────────────────────────────────
# 第一层:你系统的接口(目标接口)
# 所有业务代码都依赖这个接口
# ───────────────────────────────────────────
class MyLogger:
def log(self, level: str, message: str):
# 抽象方法,子类必须实现
# 用 raise 模拟 Java 的 interface,强制子类重写
raise NotImplementedError
# ───────────────────────────────────────────
# 第二层:第三方日志库(被适配者)
# 假设这是别人写的包,你改不了源码
# 它的方法名是 write_info / write_error,不是 log()
# ───────────────────────────────────────────
class ThirdPartyLogger:
def write_info(self, msg):
# 第三方的 info 输出方式
print(f"[INFO] {msg}")
def write_error(self, msg):
# 第三方的 error 输出方式
print(f"[ERROR] {msg}")
# ───────────────────────────────────────────
# 第三层:适配器(转换头)
# 继承 MyLogger 接口,内部持有第三方对象
# 把 log(level, msg) 翻译成 write_info / write_error
# ───────────────────────────────────────────
class LoggerAdapter(MyLogger):
def __init__(self, third_party: ThirdPartyLogger):
# 保存第三方对象的引用,后续所有调用都委托给它
self._logger = third_party
def log(self, level: str, message: str):
# 核心翻译逻辑:
# 接收到 log("ERROR", ...) → 转换成 write_error(...)
# 接收到其他级别 → 统一转换成 write_info(...)
if level.upper() == "ERROR":
self._logger.write_error(message)
else:
self._logger.write_info(message)
# ───────────────────────────────────────────
# 客户端使用
# 对外暴露的还是 MyLogger 接口
# 业务代码完全感知不到底层是 ThirdPartyLogger
# ───────────────────────────────────────────
# 创建适配器,包装住第三方对象
logger = LoggerAdapter(ThirdPartyLogger())
# 调用方式和以前一样,用的是 log() 方法
logger.log("ERROR", "连接超时")
# 输出:[ERROR] 连接超时
# 实际执行的是:third_party.write_error("连接超时")
---
二、桥接模式(Bridge)
一句话
把抽象和实现分离,让两者可以独立变化。
生活中的例子
- 电视 + 遥控器:电视有索尼、三星、小米等品牌(实现维度)。遥控器有基础版和高级版(抽象维度)。任何遥控器都可以控制任何电视——两个维度独立组合。
- 咖啡点单:大小(大/中/小)× 口味(拿铁/美式/摩卡)= 9 种组合。如果用继承,要写 9 个类。用桥接,只要 3 + 3 = 6 个类。
- 消息通知:消息类型(普通/紧急/超时)× 发送渠道(短信/邮件/微信/钉钉)= 12 种组合。桥接让两边独立扩展。
为什么需要它
如果用继承来搞所有组合,类数量是两个维度的乘积。桥接把它降为加法。
示例图

代码实现
Java代码
// ═══════════════════════════════════════════════════
// 实现维度:发送渠道
// 这一侧专注"怎么发",和消息类型完全解耦
// 新增渠道只需加一个实现类,不影响任何消息类型
// ═══════════════════════════════════════════════════
// 发送渠道的统一接口:只要求实现 doSend()
// 所有具体渠道都实现这一个方法
public interface MessageSender {
void doSend(String message);
}
// 短信渠道:实现 doSend(),内部走短信 SDK
public class SmsSender implements MessageSender {
public void doSend(String message) {
System.out.println("短信发送: " + message);
}
}
// 邮件渠道:实现 doSend(),内部走邮件 SDK
public class EmailSender implements MessageSender {
public void doSend(String message) {
System.out.println("邮件发送: " + message);
}
}
// 微信渠道:实现 doSend(),内部走微信 SDK
public class WeChatSender implements MessageSender {
public void doSend(String message) {
System.out.println("微信发送: " + message);
}
}
// 想加钉钉?只需新建 DingTalkSender implements MessageSender,其他类零改动

// ═══════════════════════════════════════════════════
// 抽象维度:消息类型
// 这一侧专注"发什么"(内容格式),不关心"怎么发"
// 新增类型只需继承 Message,不影响任何渠道
// ═══════════════════════════════════════════════════
public abstract class Message {
// 这就是"桥"的核心字段
// Message 持有 MessageSender 接口引用,而不是某个具体渠道
// 运行时传哪种 sender,就走哪种渠道——两个维度在这里连接
protected MessageSender sender;
// 构造时注入渠道,子类通过 super(sender) 调用
public Message(MessageSender sender) {
this.sender = sender;
}
// 抽象方法:由子类决定消息格式(加不加前缀、后缀等)
public abstract void send(String content);
}
// 紧急消息:在内容前后加【紧急】标记,然后委托 sender 发出去
public class UrgentMessage extends Message {
public UrgentMessage(MessageSender sender) { super(sender); }
@Override
public void send(String content) {
// 消息类型的职责:加工内容格式
// 渠道的职责:实际发送——两件事分开,互不干扰
sender.doSend("【紧急】" + content + " ⚠️请立即处理");
}
}
// 普通消息:不加任何修饰,直接发
public class NormalMessage extends Message {
public NormalMessage(MessageSender sender) { super(sender); }
@Override
public void send(String content) {
sender.doSend(content); // 原样传递,不做任何加工
}
}
// ═══════════════════════════════════════════════════
// 客户端使用:运行时自由组合两个维度
// 不需要 UrgentWeChatMessage、NormalSmsMessage 这种爆炸式子类
// ═══════════════════════════════════════════════════
// 组合一:紧急消息 + 微信渠道
Message msg1 = new UrgentMessage(new WeChatSender());
msg1.send("服务器宕机");
// 输出:微信发送: 【紧急】服务器宕机 ⚠️请立即处理
// 组合二:普通消息 + 短信渠道
Message msg2 = new NormalMessage(new SmsSender());
msg2.send("会议提醒");
// 输出:短信发送: 会议提醒
// 想换渠道?只改构造参数那一行,消息逻辑代码一行不动
python代码
from abc import ABC, abstractmethod
# ═══════════════════════════════════════════════════
# 实现维度:发送渠道
# 抽象基类 + 多个具体实现,各自独立,互不依赖
# ═══════════════════════════════════════════════════
class MessageSender(ABC):
"""发送渠道的抽象基类:所有渠道都必须实现 do_send()"""
@abstractmethod
def do_send(self, message: str): pass
class SmsSender(MessageSender):
"""短信渠道:走短信网关发送"""
def do_send(self, message):
print(f"📱 短信: {message}")
class EmailSender(MessageSender):
"""邮件渠道:走邮件服务器发送"""
def do_send(self, message):
print(f"📧 邮件: {message}")
class WeChatSender(MessageSender):
"""微信渠道:走微信公众号接口发送"""
def do_send(self, message):
print(f"💬 微信: {message}")
# 想加钉钉?
# class DingTalkSender(MessageSender):
# def do_send(self, message): ...
# 只加这一个类,其余代码零改动
# ═══════════════════════════════════════════════════
# 抽象维度:消息类型
# 通过构造函数注入渠道,self._sender 就是那座「桥」
# ═══════════════════════════════════════════════════
class Message(ABC):
"""消息类型的抽象基类"""
def __init__(self, sender: MessageSender):
# 「桥」:持有渠道接口引用,不绑定具体渠道
# 运行时注入哪个渠道对象,就走哪条路
self._sender = sender
@abstractmethod
def send(self, content: str): pass
class UrgentMessage(Message):
"""紧急消息:在内容前后附加紧急标识"""
def send(self, content: str):
# 消息类型的唯一职责:决定内容格式
# 然后调用 self._sender.do_send() 完成实际发送
# ——"格式化"和"发送"被分到两个维度,互不耦合
self._sender.do_send(f"【紧急】{content} ⚠️请立即处理")
class NormalMessage(Message):
"""普通消息:原样发送,不做格式处理"""
def send(self, content: str):
self._sender.do_send(content)
# ═══════════════════════════════════════════════════
# 客户端使用:运行时任意组合
# 2种消息类型 × 3种渠道 = 6种组合,却只有 5 个类
# ═══════════════════════════════════════════════════
# 紧急消息 + 微信渠道
UrgentMessage(WeChatSender()).send("数据库磁盘满了")
# 输出:💬 微信: 【紧急】数据库磁盘满了 ⚠️请立即处理
# 普通消息 + 邮件渠道
NormalMessage(EmailSender()).send("周报已提交")
# 输出:📧 邮件: 周报已提交
# 新增渠道不影响消息类,新增消息类型不影响渠道——两边互不影响
新增渠道(钉钉)?加一个 DingTalkSender。新增类型(定时)?加一个 ScheduledMessage。两边互不影响。
三、组合模式(Composite)
一句话
让单个对象和组合对象具有一致的接口,客户端无需区分。
生活中的例子
- 文件系统:文件夹可以包含文件,也可以包含子文件夹。你对一个文件夹说"算总大小",它自动递归加总所有子项。
- 公司组织架构:一个部门可以包含员工,也可以包含子部门。"统计人数"对任何层级都一样调用。
- 电商购物车:购物车里既有单品,也有"套餐"(套餐里包含多个单品)。"算总价"的逻辑对单品和套餐是统一的。
"组合"指的是把单个对象和对象的集合组合成同一种东西。
用文件系统举例:
文件 = 单个对象(叶子)
文件夹 = 多个对象的集合(容器)
正常来说这是两种完全不同的东西,操作起来要区别对待。
组合模式说:不,让它们变成同一种东西。
文件夹
├── 文件A ← 单个对象
├── 文件B ← 单个对象
└── 子文件夹 ← 也是一种"文件夹"
├── 文件C
└── 文件D
这里"组合"的意思是:子文件夹本身也是一个文件夹,它把「单个文件」和「文件夹」组合在一起,对外表现得和单个文件完全一样。
换句话说——你不知道你拿到的是一片叶子还是一棵树,但你可以用同样的方式操作它。
对文件调 getSize() → 返回自身大小。
对文件夹调 getSize() → 里面可能有文件、也可能有子文件夹,但你不关心,它自己会处理。
这就是"组合"——把整体和部分组合成统一的接口,让外部无法也无需区分。
示意图

代码实现
java代码
// ═══════════════════════════════════════════════════
// 统一接口:FileComponent
// 让文件和文件夹对外表现一致——调用方无需区分类型
// 只要是 FileComponent,就能调 getName() / getSize() / display()
// ═══════════════════════════════════════════════════
public interface FileComponent {
String getName();
long getSize(); // 文件返回自身大小,文件夹返回所有子项之和
void display(String indent); // indent 控制缩进层级,实现树形打印
}
// ═══════════════════════════════════════════════════
// 叶子节点:File(文件)
// 没有子节点,getSize() 直接返回自身大小
// ═══════════════════════════════════════════════════
public class File implements FileComponent {
private String name;
private long size;
public File(String name, long size) {
this.name = name;
this.size = size;
}
public String getName() { return name; }
// 叶子节点的大小就是它自己,没有子项需要累加
public long getSize() { return size; }
public void display(String indent) {
// 直接打印,不需要递归——叶子节点没有子项
System.out.println(indent + "📄 " + name + " (" + size + "KB)");
}
}
// ═══════════════════════════════════════════════════
// 组合节点:Folder(文件夹)
// 可以包含任意数量的 FileComponent(文件或子文件夹)
// getSize() 和 display() 都通过递归处理子节点
// ═══════════════════════════════════════════════════
public class Folder implements FileComponent {
private String name;
// 子节点列表:类型是 FileComponent 接口,可以混放文件和文件夹
private List<FileComponent> children = new ArrayList<>();
public Folder(String name) { this.name = name; }
// 添加子节点(文件或文件夹都行,因为都实现了 FileComponent)
public void add(FileComponent component) { children.add(component); }
public String getName() { return name; }
// 递归累加:把每个子项的 getSize() 加起来
// 子项如果是文件夹,它自己也会递归往下算,最终汇总到这里
public long getSize() {
return children.stream().mapToLong(FileComponent::getSize).sum();
}
public void display(String indent) {
// 先打印自己(含总大小)
System.out.println(indent + "📁 " + name + " (" + getSize() + "KB)");
// 再递归打印每个子项,缩进加两个空格表示层级
for (FileComponent child : children) {
child.display(indent + " "); // 每深一层,缩进加两格
}
}
}
// ═══════════════════════════════════════════════════
// 客户端使用:组装树形结构,对根节点调一次 display()
// ═══════════════════════════════════════════════════
Folder root = new Folder("项目");
Folder src = new Folder("src");
src.add(new File("Main.java", 15)); // 叶子节点
src.add(new File("Utils.java", 8)); // 叶子节点
root.add(src); // 子文件夹(组合节点)
root.add(new File("README.md", 3)); // 叶子节点
// 对根节点调一次,整棵树递归打印
root.display("");
// 📁 项目 (26KB)
// 📁 src (23KB)
// 📄 Main.java (15KB)
// 📄 Utils.java (8KB)
// 📄 README.md (3KB)
点击查看代码
from abc import ABC, abstractmethod
# ═══════════════════════════════════════════════════
# 统一接口:FileComponent
# 文件和文件夹都继承这个抽象基类
# 调用方只和 FileComponent 打交道,不区分具体类型
# ═══════════════════════════════════════════════════
class FileComponent(ABC):
@abstractmethod
def get_size(self) -> int: pass # 文件返回自身大小,文件夹返回递归累加结果
@abstractmethod
def display(self, indent=""): pass # indent 控制树形打印的缩进层级
# ═══════════════════════════════════════════════════
# 叶子节点:File(文件)
# 没有子节点,get_size() 直接返回自身大小
# ═══════════════════════════════════════════════════
class File(FileComponent):
def __init__(self, name: str, size: int):
self.name = name
self.size = size
def get_size(self):
# 叶子节点的大小就是它自己,无需递归
return self.size
def display(self, indent=""):
# 直接打印,没有子项需要继续展开
print(f"{indent}📄 {self.name} ({self.size}KB)")
# ═══════════════════════════════════════════════════
# 组合节点:Folder(文件夹)
# 内部维护子节点列表,支持嵌套任意层级
# get_size() 和 display() 都通过递归处理子节点
# ═══════════════════════════════════════════════════
class Folder(FileComponent):
def __init__(self, name: str):
self.name = name
# 子节点列表:类型是 FileComponent,文件和文件夹可以混放
self._children: list[FileComponent] = []
def add(self, component: FileComponent):
# 添加子节点,接受任何 FileComponent 子类
self._children.append(component)
def get_size(self):
# 递归累加所有子节点的大小
# 子文件夹会继续向下递归,最终汇总到这里
return sum(child.get_size() for child in self._children)
def display(self, indent=""):
# 先打印自己(含递归算出的总大小)
print(f"{indent}📁 {self.name} ({self.get_size()}KB)")
# 再递归打印每个子项,缩进加两格表示层级加深
for child in self._children:
child.display(indent + " ")
# ═══════════════════════════════════════════════════
# 客户端使用:组装树形结构,对根节点调一次 display()
# ═══════════════════════════════════════════════════
root = Folder("项目")
src = Folder("src")
src.add(File("main.py", 12)) # 叶子
src.add(File("utils.py", 5)) # 叶子
root.add(src) # 子文件夹(组合节点)
root.add(File("README.md", 3)) # 叶子
root.display()
# 📁 项目 (20KB)
# 📁 src (17KB)
# 📄 main.py (12KB)
# 📄 utils.py (5KB)
# 📄 README.md (3KB)
无论嵌套多深,get_size() 一调用就能拿到结果。
四、装饰器模式(Decorator)
一句话
在不修改原有类的前提下,动态地给对象添加新功能。
装饰器就是把功能做成可以叠加的"包装盒",每个盒子里放着下一个盒子,最里面才是真正干活的东西。
你只管对最外层的盒子发命令,它自动一层层传进去
生活中的例子
- 奶茶加料:基础奶茶 12 元,加珍珠 +3 元,加椰果 +2 元,加奶盖 +5 元。你可以任意组合:基础+珍珠+奶盖 = 20 元。每种加料就是一个装饰器。
- 手机壳:手机本身功能不变,套个壳加了防摔功能,贴个膜加了防刮功能,装个支架加了立放功能——一层层叠加。
- 俄罗斯套娃:最里面是核心功能,外面一层层包,每层加一个能力。
和继承的区别
- 继承是编译时写死的:
BufferedFileReader extends FileReader写好了就不能变 - 装饰器是运行时组合的:想加就套一层,想去就剥一层,动态组装
示意图

代码实现
java代码
// 基础接口:定义数据源的读写契约,所有装饰器和具体实现都必须遵循
public interface DataSource {
void writeData(String data);
String readData();
}
// 具体组件:最基础的文件读写实现,是整个装饰器链的"内核"
public class FileDataSource implements DataSource {
private String filename;
public FileDataSource(String filename) {
this.filename = filename;
}
public void writeData(String data) {
// 将数据直接写入文件,不做任何额外处理
}
public String readData() {
// 从文件读取原始数据并返回
return "原始数据";
}
}
// 装饰器基类:持有一个 DataSource 引用,将所有操作默认委托给被包装对象
// 子类只需覆盖需要"增强"的方法,其余行为自动透传
public abstract class DataSourceDecorator implements DataSource {
protected DataSource wrappee; // 被包装的对象,可以是具体组件,也可以是另一个装饰器
public DataSourceDecorator(DataSource source) {
this.wrappee = source;
}
}
// 加密装饰器:在写入前加密数据,在读取后解密数据
public class EncryptionDecorator extends DataSourceDecorator {
public EncryptionDecorator(DataSource source) {
super(source);
}
@Override
public void writeData(String data) {
// 写入链:先对数据加密,再交给下一层处理
wrappee.writeData(encrypt(data));
}
@Override
public String readData() {
// 读取链:先从下一层拿到数据,再解密后返回给上层
return decrypt(wrappee.readData());
}
}
// 压缩装饰器:在写入前压缩数据,在读取后解压数据
public class CompressionDecorator extends DataSourceDecorator {
public CompressionDecorator(DataSource source) {
super(source);
}
@Override
public void writeData(String data) {
// 写入链:先压缩,再交给下一层处理
wrappee.writeData(compress(data));
}
@Override
public String readData() {
// 读取链:先从下一层拿到数据,再解压后返回给上层
return decompress(wrappee.readData());
}
}
// 组合使用:像套娃一样将多个装饰器叠加在同一个数据源上
// 构造顺序 = 包裹顺序(由外到内);执行顺序 = 由外到内再由内到外
DataSource ds = new EncryptionDecorator( // 第三层(最外层):负责加密/解密
new CompressionDecorator( // 第二层:负责压缩/解压
new FileDataSource("data.bin") // 第一层(最内层):实际读写文件
));
ds.writeData("机密数据");
// writeData 调用链:EncryptionDecorator → CompressionDecorator → FileDataSource
// 实际执行顺序:加密 → 压缩 → 写文件
python代码
from abc import ABC, abstractmethod
# 抽象基类:定义数据源的统一接口,与 Java 的 DataSource 接口作用相同
class DataSource(ABC):
@abstractmethod
def write_data(self, data: str): pass
@abstractmethod
def read_data(self) -> str: pass
# 具体组件:最基础的文件读写,是装饰器链的起点
class FileDataSource(DataSource):
def __init__(self, filename):
self.filename = filename
def write_data(self, data: str):
# 直接将数据写入指定文件,无任何额外处理
print(f"写入文件 {self.filename}: {data}")
def read_data(self) -> str:
# 从文件读取并返回原始数据
return "原始数据"
# 装饰器基类:默认将读写操作完整透传给被包装对象
# 子类继承后只需覆盖需要增强的方法,减少重复代码
class DataSourceDecorator(DataSource):
def __init__(self, source: DataSource):
self._wrappee = source # 持有被包装对象的引用
def write_data(self, data: str):
# 默认透传:不做任何处理,直接交给下一层
self._wrappee.write_data(data)
def read_data(self) -> str:
# 默认透传:直接返回下一层的结果
return self._wrappee.read_data()
# 加密装饰器:写入时先加密,读取时先解密
class EncryptionDecorator(DataSourceDecorator):
def write_data(self, data: str):
encrypted = f"🔒{data}🔒" # 用 emoji 模拟加密效果,生产中替换为真实算法
super().write_data(encrypted) # 加密后交给下一层继续处理
def read_data(self) -> str:
data = super().read_data() # 先让下一层读取并处理
return data.replace("🔒", "") # 再对结果解密后返回给上层
# 压缩装饰器:写入时先压缩,读取时自动解压(此处仅演示写入侧)
class CompressionDecorator(DataSourceDecorator):
def write_data(self, data: str):
compressed = f"[ZIP:{data}]" # 用包裹格式模拟压缩效果,生产中替换为 zlib 等
super().write_data(compressed) # 压缩后交给下一层继续处理
# 组合使用:由外到内依次套上加密、压缩、文件读写三层
source = EncryptionDecorator( # 最外层:加密
CompressionDecorator( # 中间层:压缩
FileDataSource("secret.dat") # 最内层:文件 I/O
))
source.write_data("机密信息")
# write_data 调用链:EncryptionDecorator → CompressionDecorator → FileDataSource
# 输出: 写入文件 secret.dat: [ZIP:🔒机密信息🔒]
# 数据变换过程:机密信息 → 🔒机密信息🔒(加密)→ [ZIP:🔒机密信息🔒](压缩)→ 写入文件
五、外观模式(Facade)
一句话
为一个复杂子系统提供一个简单的统一接口。
生活中的例子
- 酒店前台:你说"帮我订明天去上海的出差行程"。前台帮你搞定机票+酒店+接机+会议室。你不需要分别打三个电话。
- 一键启动:汽车钥匙一拧,引擎启动+油泵工作+电路通电+仪表亮起——你只做了一个动作。
- 外卖 App:你点一下"下单",后面触发了支付、通知商家、分配骑手、开始配送……你只按了一个按钮。
结构
客户端 → 外观类(Facade) → 子系统A
→ 子系统B
→ 子系统C

代码实现
java代码
// ========== 子系统:各自独立,只负责自己的领域 ==========
// 子系统1:负责机票预订
public class FlightBooking {
/**
* 预订机票
* @param from 出发城市
* @param to 目的城市
* @param date 出发日期
* @return 预订结果描述
*/
public String book(String from, String to, String date) {
return "机票已预订: " + from + " → " + to;
}
}
// 子系统2:负责酒店预订
public class HotelBooking {
/**
* 预订酒店
* @param city 入住城市
* @param date 入住日期
* @param nights 入住天数
* @return 预订结果描述
*/
public String reserve(String city, String date, int nights) {
return "酒店已预订: " + city + " " + nights + "晚";
}
}
// 子系统3:负责用车安排
public class CarService {
/**
* 安排接机用车
* @param airport 接机地点(机场)
* @param hotel 目的地(酒店)
* @return 安排结果描述
*/
public String arrange(String airport, String hotel) {
return "接机已安排: " + airport + " → " + hotel;
}
}
// ========== 外观类:统一入口,屏蔽子系统复杂度 ==========
public class TravelFacade {
// 持有三个子系统的引用,由外观类统一管理,客户端无需感知
private FlightBooking flight = new FlightBooking();
private HotelBooking hotel = new HotelBooking();
private CarService car = new CarService();
/**
* 一键完成出差全套预订:机票 + 酒店 + 接机
* 客户端只需调用这一个方法,内部协调顺序由外观类负责
*
* @param from 出发城市
* @param to 目的城市
* @param date 出发日期
* @param nights 住宿天数
* @return 三项预订的汇总结果
*/
public String bookTrip(String from, String to, String date, int nights) {
String f = flight.book(from, to, date); // 第一步:订机票
String h = hotel.reserve(to, date, nights); // 第二步:订酒店
String c = car.arrange(to + "机场", "酒店"); // 第三步:安排接机
return f + "\n" + h + "\n" + c;
}
}
// ========== 客户端:只与外观类打交道 ==========
// 客户端不需要了解 FlightBooking / HotelBooking / CarService 的存在
TravelFacade travel = new TravelFacade();
System.out.println(travel.bookTrip("北京", "上海", "2024-03-15", 2));
python代码
# ========== 子系统:各自独立,只负责自己的领域 ==========
# 子系统1:负责机票预订
class FlightBooking:
def book(self, src, dst, date):
"""
预订机票
:param src: 出发城市
:param dst: 目的城市
:param date: 出发日期
:return: 预订结果描述
"""
return f"✈️ 机票已订: {src} → {dst} ({date})"
# 子系统2:负责酒店预订
class HotelBooking:
def reserve(self, city, date, nights):
"""
预订酒店
:param city: 入住城市
:param date: 入住日期
:param nights: 入住天数
:return: 预订结果描述
"""
return f"🏨 酒店已订: {city} {nights}晚"
# 子系统3:负责用车安排
class CarService:
def arrange(self, pickup, dropoff):
"""
安排接机用车
:param pickup: 接机地点(机场)
:param dropoff: 目的地(酒店)
:return: 安排结果描述
"""
return f"🚗 接机已排: {pickup} → {dropoff}"
# ========== 外观类:统一入口,屏蔽子系统复杂度 ==========
class TravelFacade:
def __init__(self):
# 持有三个子系统实例,由外观类统一管理,客户端无需感知
self._flight = FlightBooking()
self._hotel = HotelBooking()
self._car = CarService()
def book_trip(self, src, dst, date, nights):
"""
一键完成出差全套预订:机票 + 酒店 + 接机
客户端只需调用这一个方法,内部协调顺序由外观类负责
:param src: 出发城市
:param dst: 目的城市
:param date: 出发日期
:param nights: 住宿天数
:return: 三项预订的汇总结果(换行分隔)
"""
results = [
self._flight.book(src, dst, date), # 第一步:订机票
self._hotel.reserve(dst, date, nights), # 第二步:订酒店
self._car.arrange(f"{dst}机场", "酒店"), # 第三步:安排接机
]
return "\n".join(results) # 将三条结果合并为一段文本返回
# ========== 客户端:只与外观类打交道 ==========
# 客户端不需要了解 FlightBooking / HotelBooking / CarService 的存在
facade = TravelFacade()
print(facade.book_trip("北京", "上海", "2024-03-15", 2))
# ✈️ 机票已订: 北京 → 上海 (2024-03-15)
# 🏨 酒店已订: 上海 2晚
# 🚗 接机已排: 上海机场 → 酒店
复杂度被藏在门面后面,外面看到的永远是简单的一个方法。
六、享元模式(Flyweight)
一句话
共享大量细粒度对象中相同的部分,节省内存。
生活中的例子
- 围棋:黑子和白子只有两种"类型",但棋盘上可能有几百颗棋子。不需要为每颗棋子都存一份"颜色、材质、大小"数据——共享就够了,每颗只需要记住坐标。
- Word 文档中的字符:一篇文章有 10 万个字,但字体信息(宋体/14号/黑色)是大量字符共享的,不需要每个字都存一份字体对象。
- 游戏地图的树:森林里 100 万棵树,但树种只有"松树、柳树、银杏"三种。纹理数据很大(每种 2MB),共享后只需 6MB 而不是 2TB。
示意图

代码实现
java代码
/**
* 享元对象(Flyweight)
* 存储"内部状态":所有树共享的、不可变的数据(品种、颜色、纹理)
* 这个对象会被大量树节点复用,所以设计为不可变(final 字段)
*/
public class TreeType {
private final String name; // 树的品种(内部状态)
private final String color; // 颜色(内部状态)
private final byte[] texture; // 纹理数据,约 2MB(内部状态,最值得共享的大对象)
public TreeType(String name, String color, byte[] texture) {
this.name = name;
this.color = color;
this.texture = texture;
}
/**
* draw 接收"外部状态":坐标 (x, y)
* 外部状态由调用方传入,不存储在享元对象里
* 这正是享元模式的关键:把"变化的"和"不变的"数据分离
*/
public void draw(int x, int y) {
System.out.println("在(" + x + "," + y + ")画" + name);
}
}
/**
* 享元工厂(Flyweight Factory)
* 负责管理享元对象的缓存池,确保相同类型的树只创建一次
* 对外提供统一的获取入口,调用方无需关心对象是新建还是复用
*/
public class TreeFactory {
// 缓存池:key = "树种_颜色",value = 对应的享元对象
private static Map<String, TreeType> cache = new HashMap<>();
/**
* 获取享元对象:缓存命中则直接返回,否则新建并存入缓存
* computeIfAbsent 是线程安全写法的简洁替代(单线程场景下足够)
*
* 注意:如果 key 已存在,传入的 texture 参数会被忽略,
* 返回的是缓存中已有对象的纹理 —— 调用方需保证相同 key 的 texture 一致
*/
public static TreeType getTreeType(String name, String color, byte[] texture) {
String key = name + "_" + color;
return cache.computeIfAbsent(key, k -> new TreeType(name, color, texture));
}
}
/**
* 森林(Context / 客户端)
* 存储每棵树的"外部状态"(坐标),通过引用享元对象复用"内部状态"
* 百万棵树只持有百万个坐标 + 几个 TreeType 引用,而不是百万份 2MB 纹理
*/
public class Forest {
private List<int[]> treePositions = new ArrayList<>(); // 外部状态:每棵树独有的坐标
private List<TreeType> treeTypes = new ArrayList<>(); // 享元对象引用:大量树指向同一个实例
/**
* 种一棵树
* 坐标是外部状态,存在 treePositions 里
* TreeType 是内部状态的载体,通过工厂获取(可能复用已有对象)
*/
public void plantTree(int x, int y, String name, String color, byte[] texture) {
TreeType type = TreeFactory.getTreeType(name, color, texture);
treePositions.add(new int[]{x, y});
treeTypes.add(type); // 百万棵树共用几个 TreeType,内存从 2GB 降到几 MB
}
}
python代码
class TreeType:
"""
享元对象(Flyweight / 内部状态载体)
存储所有树共享的、不会随实例变化的数据。
这个类的实例数量极少(有几种树就几个),但会被数百万棵树引用。
"""
def __init__(self, name, color, texture_data):
self.name = name # 树种(内部状态)
self.color = color # 颜色(内部状态)
self.texture_data = texture_data # 纹理二进制数据,体积大,最值得共享(内部状态)
def draw(self, x, y):
"""
渲染这棵树。
x, y 是外部状态,由调用方传入,享元对象本身不保存坐标。
"""
print(f"在({x},{y})画{self.name}")
class TreeFactory:
"""
享元工厂(Flyweight Factory)
维护一个类级别的缓存字典,保证相同品种+颜色的 TreeType 全局只创建一次。
使用 @classmethod 而非单例,是因为缓存属于"类"而非某个实例。
"""
_cache: dict[str, TreeType] = {} # 类变量:所有调用共享同一份缓存
@classmethod
def get_tree_type(cls, name, color, texture_data) -> TreeType:
"""
获取享元对象。
- 缓存命中:直接返回已有对象,texture_data 参数被忽略
- 缓存未命中:创建新对象并存入缓存
"""
key = f"{name}_{color}"
if key not in cls._cache:
cls._cache[key] = TreeType(name, color, texture_data)
print(f" [新建 TreeType: {key}]") # 可观察到只新建了 3 次
return cls._cache[key]
# ---- 客户端代码 ----
import random
# 模拟种 100 万棵树
# 每次循环只是从缓存里取享元对象 + 保存坐标,不会重复分配 2MB 纹理
for i in range(1_000_000):
tree_name = random.choice(["松树", "柳树", "银杏"])
# texture 在真实场景中应提前加载,这里用占位数据模拟
tree_type = TreeFactory.get_tree_type(tree_name, "green", b"texture...")
# tree_type.draw(random.randint(0,1000), random.randint(0,1000))
# 验证效果:100 万次调用,只创建了 3 个 TreeType 对象
print(f"TreeType 对象总数: {len(TreeFactory._cache)}") # 输出:3
100 万棵树,内存里只有 3 个 TreeType 对象 + 100 万组坐标。
Java 里常见的享元:
String常量池、Integer.valueOf(-128~127)缓存、Boolean.TRUE/FALSE。
享元模式和原型模式的区别
| 维度 | 原型模式 | 享元模式 |
|---|---|---|
| 核心目的 | 快速创建新对象(避免重复初始化的开销) | 减少对象数量(多个上下文共享同一个实例) |
| 对象数量 | 每次 clone 都产生一个新实例 | 同类型对象全局只有一个实例 |
| 可变性 | 克隆出来的对象可以自由修改 | 享元对象是不可变的(修改会影响所有引用方) |
| 解决的问题 | 构造成本高(复杂初始化、深拷贝) | 内存占用高(大量相似对象重复分配) |
| 典型场景 | 游戏中复制一个带完整属性的角色模板 | 游戏中百万棵树共用同一份纹理数据 |
| 与缓存的关系 | 克隆后对象独立,原型不做缓存 | 工厂维护缓存池,同 key 永远返回同一个对象 |
一句话总结:
- 原型模式:我要一个副本,拿来改。
- 享元模式:我要共享这个对象,谁也别改它
两者可以组合使用——比如用原型快速克隆出一批配置对象,再用享元共享其中不变的部分(如纹理、图标等重型资源)。
七、代理模式(Proxy)
一句话
为另一个对象提供一个替身或占位符,以控制对它的访问。
生活中的例子
- 房产中介:你想买房,不直接找房东,而是找中介。中介帮你筛选、谈价、办手续——你通过中介间接访问房东。
- 明星经纪人:想找明星商演,不能直接联系明星本人,要通过经纪人。经纪人帮你过滤不合理的请求、谈档期和费用。
- VPN:你访问网站不是直接连,而是通过 VPN 服务器中转——VPN 就是一个网络代理。
- 信用卡:信用卡是现金的代理。你不用带着一捆现金出门,刷卡就行。银行在背后处理真正的资金流动。
示意图

代理的种类
| 类型 | 干什么 | 例子 |
|---|---|---|
| 远程代理 | 本地对象代表远程对象 | RPC 调用、Web Service 客户端 |
| 虚拟代理 | 延迟加载重量级对象 | 大图片懒加载、ORM 懒查询 |
| 保护代理 | 控制访问权限 | 权限校验、鉴权 |
| 缓存代理 | 缓存请求结果 | 接口缓存、CDN |
代码实现(缓存代理)
java代码
/**
* 用户服务接口:定义统一的访问契约。
* 真实服务和代理都实现此接口,调用方无需感知背后是哪个实现。
*/
public interface UserService {
User getUser(long id);
}
/**
* 真实服务实现:直接查数据库,每次都有 I/O 开销。
* 在高并发或频繁重复查询的场景下,性能瓶颈就在这里。
*/
public class UserServiceImpl implements UserService {
public User getUser(long id) {
System.out.println("查数据库... id=" + id);
// 实际执行 SQL,返回用户对象
return db.query("SELECT * FROM users WHERE id = ?", id);
}
}
/**
* 缓存代理:在真实服务前面加一层内存缓存。
* 实现了同一个 UserService 接口,对调用方完全透明。
*
* 代理模式的核心价值:在不改动原始类的前提下,
* 通过包装增强其行为(此处是性能增强)。
*/
public class CachingUserServiceProxy implements UserService {
// 持有真实服务的引用,缓存未命中时委托给它处理
private UserServiceImpl real = new UserServiceImpl();
// 线程安全的本地缓存:key 是用户 id,value 是缓存的 User 对象
private Map<Long, User> cache = new ConcurrentHashMap<>();
@Override
public User getUser(long id) {
// computeIfAbsent:若 key 已存在则直接返回缓存值,
// 否则执行 lambda 查库,并将结果存入缓存后返回。
// 一行代码同时完成"查缓存 → 未命中则查库 → 写缓存"三步。
return cache.computeIfAbsent(id, key -> {
System.out.println("缓存未命中,走数据库");
return real.getUser(key);
});
}
}
// ---- 客户端使用 ----
// 调用方只依赖 UserService 接口,不关心背后是真实服务还是代理
UserService service = new CachingUserServiceProxy();
service.getUser(1); // 第一次:缓存无数据 → 查数据库 → 写入缓存
service.getUser(1); // 第二次:缓存命中 → 直接返回,不访问数据库
python代码
class HeavyImage:
"""
真实对象:代表一张需要从磁盘加载的大图片。
构造时立即加载,耗时较长——这正是需要被代理"保护"的代价。
"""
def __init__(self, filename):
self.filename = filename
self._load() # 构造即加载,调用方若直接 new,就无法避免这个开销
def _load(self):
# 模拟耗时的磁盘 I/O(实际场景可能是网络请求、解码等)
print(f"⏳ 从磁盘加载大图片: {self.filename} (耗时3秒)")
def display(self):
print(f"🖼️ 显示图片: {self.filename}")
class ImageProxy:
"""
延迟加载代理(Lazy Loading Proxy):
与 HeavyImage 提供相同的 display() 接口,但构造时不加载图片。
只有在第一次调用 display() 时才真正创建 HeavyImage 并触发加载。
适用场景:需要管理大量资源对象,但实际只会访问其中一小部分时,
用代理延迟开销,按需加载。
"""
def __init__(self, filename):
self.filename = filename
self._real_image = None # 占位符,表示"尚未加载"
def display(self):
# 首次访问:真实对象还不存在,此时才触发加载(懒汉式初始化)
if self._real_image is None:
self._real_image = HeavyImage(self.filename)
# 无论是否刚加载,统一委托给真实对象执行显示
self._real_image.display()
# ---- 客户端使用 ----
# 创建 100 个代理对象——瞬间完成,没有任何 I/O,内存占用极小
images = [ImageProxy(f"photo_{i}.jpg") for i in range(100)]
# 只有真正需要显示的图片才会被加载
images[42].display()
# 输出:
# ⏳ 从磁盘加载大图片: photo_42.jpg (耗时3秒) ← 第一次:触发加载
# 🖼️ 显示图片: photo_42.jpg
images[42].display()
# 输出:
# 🖼️ 显示图片: photo_42.jpg ← 第二次:_real_image 已存在,跳过加载
Spring AOP、MyBatis Mapper、Dubbo RPC、Python 的
@property懒加载,底层全是代理模式。
总结:七种结构型模式对比
| 模式 | 核心意图 | 现实比喻 | 典型场景 |
|---|---|---|---|
| 适配器 | 接口转换 | 电源转换头 | 对接第三方库、老系统改造 |
| 桥接 | 两个维度独立变化 | 遥控器 × 电视品牌 | 消息类型×渠道、形状×颜色 |
| 组合 | 树形结构统一接口 | 文件夹和文件一视同仁 | 菜单树、组织架构、UI 组件 |
| 装饰器 | 动态添加功能 | 奶茶加料 | Java IO 流、中间件管道 |
| 外观 | 简化复杂系统 | 酒店前台一句话搞定 | SDK 封装、微服务聚合层 |
| 享元 | 共享重复数据省内存 | 围棋只有黑白两种子 | 大量相似对象、字符渲染 |
| 代理 | 控制对象访问 | 房产中介 | 缓存、懒加载、权限、RPC |
心法
- 优先组合,少用继承。 结构型模式几乎都在践行这条原则。继承把关系写死了,组合可以运行时灵活切换。
- 装饰器 vs 代理: 装饰器侧重"加功能"(用户主动包装),代理侧重"控制访问"(对用户透明)。
- 适配器是"事后补救",桥接是"事前设计"。 新系统尽量用桥接规划好维度;老系统对接不上了再用适配器补。
- 外观不是万能的。 如果子系统本身设计合理,不需要强行套外观。外观是给"乱但不能重构"的系统兜底用的。

结构型模式关心的核心问题:**类和对象之间怎么组合,才能形成更大、更灵活的结构?** 用组合代替继承,用包装扩展功能,让系统像乐高一样随意拼装。
浙公网安备 33010602011771号