元数据卡
- 前置知识:第7章(SOLID 原则);第8章(创建型模式)
- 预计时间:55 分钟
- 核心难度:进阶
- 完成标志:能分辨 7 种结构型模式的适用场景,并正确实现
你的进度
你已经能灵活地控制零件如何被制造。但你的机器面临新的问题:已有的齿轮组和新设计的轴承“接口不匹配”、树形连杆结构需要统一计算受力、功能需要在运行时叠加——
你在工匠之都的工具库前站定:“我需要一种万能接头——能把不同规格的零件焊在一起,又不改变它们本身的构造。” 你的任务
结构型模式关注——如何组合类和对象以形成更大的结构。不同的模式解决不同类型的"匹配困难":Adapter 解决接口不兼容,Composite 处理树形结构的统一,Decorator 动态添加功能,Proxy 控制访问,Facade 简化复杂子系统,Bridge 分离抽象与实现,Flyweight 共享细粒度对象。
本章分层
- 必读:Adapter、Decorator、Proxy
- 选读:Composite、Facade
- 进阶:Bridge、Flyweight
本章不会要求你掌握
- 混入(Mixin)模式的跨语言对比
- AOP 与 Decorator 的关系
破局 · 溯源
你的擂台系统对外暴露了一组 API。现在你要接入一个"第三方排名服务",它接受一个 XML 格式的请求。而你的系统只暴露了 JSON 接口。你不想改你自己的系统——那意味着改测试、改文档、影响现有调用方。
你需要的是一种"中间转换层"——把你的 JSON 请求翻译成 XML,把 XML 响应翻译回 JSON。这就是 Adapter 模式。
第一层:Adapter——接口适配器
// ch09/adapter/RankingService.java
// 第三方排名服务的接口(XML 方式)
public interface ExternalRankingService {
String submitResult(String xmlRequest);
}
// 你的系统的接口
public interface InternalRankingClient {
void submitResult(String matchId, String winner, int score);
}
// Adapter:把你的接口翻译成第三方的接口
public class RankingServiceAdapter implements InternalRankingClient {
private final ExternalRankingService externalService;
public RankingServiceAdapter(ExternalRankingService externalService) {
this.externalService = externalService;
}
@Override
public void submitResult(String matchId, String winner, int score) {
// 翻译:内部调用 → XML 请求
String xml = String.format(
"<match><id>%s</id><winner>%s</winner><score>%d</score></match>",
matchId, winner, score
);
String response = externalService.submitResult(xml);
// 如果响应为 error,可以抛异常或记录日志
if (response.contains("error")) {
throw new RuntimeException("Ranking service error: " + response);
}
}
}使用:
// 所有业务代码只依赖 InternalRankingClient
InternalRankingClient client = new RankingServiceAdapter(new ExternalRankingServiceImpl());
client.submitResult("match-1", "steven", 100);你的业务代码毫不知情它在和一个 XML 服务打交道——Adapter 在中间翻译。当你切到另一个排名服务(JSON 或 gRPC),只需写一个新的 Adapter,业务代码一个字符不改。
在 Python 里思路相同:
# ch09/adapter/ranking_adapter.py
class RankingServiceAdapter:
def __init__(self, external_service):
self._external = external_service
def submit_result(self, match_id: str, winner: str, score: int):
xml_data = f"<match><id>{match_id}</id><winner>{winner}</winner><score>{score}</score></match>"
response = self._external.submit(xml_data)
if "error" in response:
raise RuntimeError(f"Ranking service error: {response}")第二层:Decorator——运行时添加功能
你想在原来奖品计算的基础上加一个"获奖记录日志"。最直接的方法:改 PrizeCalculator 类。但这违反了开闭原则——你为了加一个日志去修改一个测试覆盖了的类。
Decorator 让你在不修改原对象的前提下,为它动态添加行为。
// ch09/decorator/PrizeCalculator.java
public interface PrizeCalculator {
int calculate(Match match);
}
// 基础实现
public class BasicPrizeCalculator implements PrizeCalculator {
@Override
public int calculate(Match match) {
return match.getBaseScore() * 2;
}
}
// Decorator 基类
public abstract class PrizeCalculatorDecorator implements PrizeCalculator {
protected final PrizeCalculator wrapped;
public PrizeCalculatorDecorator(PrizeCalculator wrapped) {
this.wrapped = wrapped;
}
@Override
public int calculate(Match match) {
return wrapped.calculate(match);
}
}
// 具体 Decorator:日志
public class LoggingPrizeDecorator extends PrizeCalculatorDecorator {
public LoggingPrizeDecorator(PrizeCalculator wrapped) {
super(wrapped);
}
@Override
public int calculate(Match match) {
int result = super.calculate(match);
System.out.println("Match " + match.getId() + " prize: " + result);
return result;
}
}
// 具体 Decorator:节假日翻倍
public class HolidayPrizeDecorator extends PrizeCalculatorDecorator {
public HolidayPrizeDecorator(PrizeCalculator wrapped) {
super(wrapped);
}
@Override
public int calculate(Match match) {
int base = super.calculate(match);
return match.isHoliday() ? base * 2 : base;
}
}使用——运行时组合:
PrizeCalculator calculator = new BasicPrizeCalculator();
calculator = new LoggingPrizeDecorator(calculator); // 加日志
calculator = new HolidayPrizeDecorator(calculator); // 翻倍
int prize = calculator.calculate(match);执行顺序:HolidayPrizeDecorator -> LoggingPrizeDecorator -> BasicPrizeCalculator。
你不需要改 BasicPrizeCalculator 一行代码。想不要日志?去掉 LoggingPrizeDecorator 那行。想换个顺序?调换包装顺序。
Java I/O 流是 Decorator 的经典例子:
InputStream in = new GZipInputStream(
new BufferedInputStream(
new FileInputStream("data.gz")
)
);每一层包装加一个功能:文件读取、缓冲、解压。
第三层:Proxy——给对象加一个替身
你需要对一个远程服务做限流(rate limiting),防止调用方恶意刷请求。你不想改远程服务的代码——而且你也没权限。
Proxy 模式:给一个对象创建一个代理(同接口),代理控制访问。
// ch09/proxy/RankingClient.java
public interface RankingClient {
void submitResult(String matchId, String winner, int score);
}
// 真实的远程客户端
public class RealRankingClient implements RankingClient {
@Override
public void submitResult(String matchId, String winner, int score) {
// 实际的 HTTP 调用
}
}
// 代理:限流
public class RateLimitingProxy implements RankingClient {
private final RealRankingClient realClient;
private final int maxRequestsPerSecond;
private long lastSecond = 0;
private int requestsThisSecond = 0;
public RateLimitingProxy(RealRankingClient realClient, int maxRequestsPerSecond) {
this.realClient = realClient;
this.maxRequestsPerSecond = maxRequestsPerSecond;
}
@Override
public void submitResult(String matchId, String winner, int score) {
long now = System.currentTimeMillis() / 1000;
if (now != lastSecond) {
lastSecond = now;
requestsThisSecond = 0;
}
if (++requestsThisSecond > maxRequestsPerSecond) {
throw new RuntimeException("Rate limit exceeded");
}
realClient.submitResult(matchId, winner, score);
}
}Proxy 的常见变体:
- 虚拟代理:延迟加载(等对象真要被用了才创建)
- 保护代理:检查权限
- 远程代理:处理 RPC 调用细节
- 缓存代理:缓存结果
// 缓存代理
public class CachingProxy implements RankingClient {
private final RealRankingClient realClient;
private final Map<String, Integer> cache = new ConcurrentHashMap<>();
@Override
public void submitResult(String matchId, String winner, int score) {
realClient.submitResult(matchId, winner, score);
cache.put(matchId + ":" + winner, score); // 缓存最新结果
}
public Integer getCachedScore(String matchId, String player) {
return cache.get(matchId + ":" + player);
}
}第四层:Composite——树形结构的统一接口
你的奖品系统要支持"奖品包"——一个包可能包含多个子奖品,子奖品可能是单件或又是一个包。用户调 getTotalValue() 时,单件返回自己,包递归求和。
Composite 模式让你把单个对象和组合对象统一对待。
// ch09/composite/PrizeComponent.java
public interface PrizeComponent {
String getName();
int getValue();
}
// 叶子节点:单个奖品
public class SinglePrize implements PrizeComponent {
private final String name;
private final int value;
public SinglePrize(String name, int value) {
this.name = name;
this.value = value;
}
@Override
public String getName() { return name; }
@Override
public int getValue() { return value; }
}
// 组合节点:奖品包
public class PrizePackage implements PrizeComponent {
private final String name;
private final List<PrizeComponent> children = new ArrayList<>();
public PrizePackage(String name) {
this.name = name;
}
public void add(PrizeComponent child) {
children.add(child);
}
@Override
public String getName() { return name; }
@Override
public int getValue() {
return children.stream().mapToInt(PrizeComponent::getValue).sum();
}
}使用:
PrizePackage packageA = new PrizePackage("冠军包");
packageA.add(new SinglePrize("金币", 100));
packageA.add(new SinglePrize("治疗药水", 50));
PrizePackage grandPackage = new PrizePackage("超级包");
grandPackage.add(packageA); // 包里有包
grandPackage.add(new SinglePrize("传说武器", 500));
System.out.println(grandPackage.getValue()); // 输出 650调用方不需要知道 grandPackage 是一个包——它和其他 PrizeComponent 用同样的接口。这就是"统一对待"的含义。
第五层:Facade——给复杂子系统一个简单入口
你的擂台系统后台有裁判模块、积分管理、选手匹配、场地调度、通知中心。一个新的调用方只想"报名参赛"——但他需要和五个模块交互。
Facade 模式提供一个统一的接口来访问一群子系统接口。
// ch09/facade/TournamentFacade.java
public class TournamentFacade {
private final MatchService matchService;
private final ScoreService scoreService;
private final NotificationService notificationService;
public TournamentFacade() {
this.matchService = new MatchService();
this.scoreService = new ScoreService();
this.notificationService = new NotificationService();
}
// 简单入口:报名参赛
public RegistrationResult register(String playerId, String tournamentId) {
// 1. 验证选手资格(内部协作)
if (!matchService.canRegister(playerId, tournamentId)) {
return RegistrationResult.failure("不能报名");
}
// 2. 注册选手
matchService.register(playerId, tournamentId);
// 3. 初始化积分
scoreService.initialize(playerId);
// 4. 发通知
notificationService.send(playerId, "报名成功");
return RegistrationResult.success();
}
}调用方只需要知道 TournamentFacade:
TournamentFacade facade = new TournamentFacade();
RegistrationResult result = facade.register("steven", "t-1");每个子系统仍可独立访问——Facade 不是"把子系统藏起来",而是提供一个方便的快捷方式。
第六层:Bridge——抽象与实现分离
你的通知系统需要支持"短信通知"和"邮件通知",并且每个通知可以包含"普通消息"或"紧急消息"。你的第一反应是画矩阵:4 个类(SmsNormal、SmsUrgent、EmailNormal、EmailUrgent)。再加一个通道(App 推送)变 6 个类。这不健康。
Bridge 模式把"抽象"(消息类型)和"实现"(通知通道)分离成两个独立的维度,通过组合连接。
// ch09/bridge/NotificationChannel.java
// 实现维度:通知渠道
public interface NotificationChannel {
void send(String message, String recipient);
}
public class SmsChannel implements NotificationChannel {
@Override
public void send(String message, String recipient) {
System.out.println("SMS to " + recipient + ": " + message);
}
}
public class EmailChannel implements NotificationChannel {
@Override
public void send(String message, String recipient) {
System.out.println("Email to " + recipient + ": " + message);
}
}
// 抽象维度:消息类型
public abstract class Message {
protected final NotificationChannel channel;
protected final String content;
protected final String recipient;
public Message(NotificationChannel channel, String content, String recipient) {
this.channel = channel;
this.content = content;
this.recipient = recipient;
}
public abstract void send();
}
public class NormalMessage extends Message {
public NormalMessage(NotificationChannel channel, String content, String recipient) {
super(channel, content, recipient);
}
@Override
public void send() {
channel.send("[INFO] " + content, recipient);
}
}
public class UrgentMessage extends Message {
public UrgentMessage(NotificationChannel channel, String content, String recipient) {
super(channel, content, recipient);
}
@Override
public void send() {
channel.send("[URGENT] " + content, recipient);
}
}使用:
// 紧急短信
Message msg = new UrgentMessage(new SmsChannel(), "比赛即将开始", "steven");
msg.send();
// 普通邮件
Message msg2 = new NormalMessage(new EmailChannel(), "下周赛程", "all-players");
msg2.send();新增一个通知渠道(App 推送),写一个 AppPushChannel implements NotificationChannel——不碰消息类型代码。新增一个消息类型(静默消息),写一个 SilentMessage extends Message——不碰通知渠道代码。两个维度正交扩展。
第七层:Flyweight——共享细粒度对象
你的比赛场地有"树木"对象,每个树占用几十字节,一场比赛可能有十万棵树。内存不够了。
Flyweight 模式:通过共享细粒度的、不可变的对象来节省内存。不可变部分(树种名称、贴图路径)作为内部状态共享,可变部分(坐标)作为外部状态不共享。
// ch09/flyweight/TreeType.java
// 内部状态:可共享
public class TreeType {
private final String name;
private final String texturePath;
private final String color;
public TreeType(String name, String texturePath, String color) {
this.name = name;
this.texturePath = texturePath;
this.color = color;
}
public void display(int x, int y) {
System.out.println("Drawing " + name + " at (" + x + ", " + y + ")");
}
}
// Flyweight 工厂
public class TreeFactory {
private static final Map<String, TreeType> types = new HashMap<>();
public static TreeType getTreeType(String name, String texture, String color) {
return types.computeIfAbsent(name, k -> new TreeType(name, texture, color));
}
}
// 外部状态:位置不共享
public class Tree {
private final int x;
private final int y;
private final TreeType type;
public Tree(int x, int y, TreeType type) {
this.x = x;
this.y = y;
this.type = type;
}
public void display() {
type.display(x, y);
}
}十万棵松树之前是十万个对象(每棵自带 name/texture/color)。现在是一万个 Tree 对象 + 一个共享的 PineTreeType 对象,内存占用降到十分之一。
常见陷阱
陷阱一:用 Adapter 做不该做的转换。 Adapter 只改接口,不改语义。如果你的转换涉及业务逻辑(比如"把天换算成罚款金额"),那是 Transformer 不是 Adapter。
陷阱二:Decorator 链顺序敏感。 LoggingDecorator > HolidayDecorator > BaseCalculator 和 HolidayDecorator > LoggingDecorator > BaseCalculator 的日志顺序不同。确保团队清楚装饰顺序约定。
陷阱三:Proxy 和 Decorator 长得太像。 结构上几乎一样,但意图不同:Proxy 控制访问(限流、权限、延迟加载),Decorator 添加行为(日志、缓存、加密)。同一段代码结构,但改的动机不同。
陷阱四:Composite 的叶子节点和组合节点接口不一致。 如果 SinglePrize 的 add() 方法应该抛 UnsupportedOperationException——说明接口设计有问题。考虑用安全复合模式,把添加子节点的方法只放在 PrizePackage 中。
通关挑战
- 热身:为一个已有的外部 API 调用写 Adapter,让你的业务代码不用直接依赖第三方库。
- 挑战:为你的奖品计算系统写 LoggingProxy(记录每次调用耗时),而不是 LoggingDecorator。
- 观察:看看你使用的框架中的 Decorator 例子——Java
BufferedInputStream套FileInputStream、Python@lru_cache装饰器。它们解决了什么问题?
旅人笔记
七种结构型模式各自解决一种"组装问题":Adapter 翻译接口,Decorator 叠加功能,Proxy 控制访问,Composite 统一树形对象,Facade 简化调用,Bridge 分离维度,Flyweight 共享细粒度。你的工具箱开始充实了。
下一站预告
对象怎么创建、怎么组装都解决了。接下来是对象之间怎么交互——谁通知谁、谁控制谁、谁给谁发消息。下一章:行为型模式——11 种对象协作的方式。