Skip to content

元数据卡

  • 前置知识:第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——接口适配器

java
// 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);
        }
    }
}

使用:

java
// 所有业务代码只依赖 InternalRankingClient
InternalRankingClient client = new RankingServiceAdapter(new ExternalRankingServiceImpl());
client.submitResult("match-1", "steven", 100);

你的业务代码毫不知情它在和一个 XML 服务打交道——Adapter 在中间翻译。当你切到另一个排名服务(JSON 或 gRPC),只需写一个新的 Adapter,业务代码一个字符不改。

在 Python 里思路相同:

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 让你在不修改原对象的前提下,为它动态添加行为

java
// 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;
    }
}

使用——运行时组合:

java
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 的经典例子:

java
InputStream in = new GZipInputStream(
    new BufferedInputStream(
        new FileInputStream("data.gz")
    )
);

每一层包装加一个功能:文件读取、缓冲、解压。

第三层:Proxy——给对象加一个替身

你需要对一个远程服务做限流(rate limiting),防止调用方恶意刷请求。你不想改远程服务的代码——而且你也没权限。

Proxy 模式:给一个对象创建一个代理(同接口),代理控制访问。

java
// 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 调用细节
  • 缓存代理:缓存结果
java
// 缓存代理
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 模式让你把单个对象和组合对象统一对待

java
// 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();
    }
}

使用:

java
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 模式提供一个统一的接口来访问一群子系统接口。

java
// 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

java
TournamentFacade facade = new TournamentFacade();
RegistrationResult result = facade.register("steven", "t-1");

每个子系统仍可独立访问——Facade 不是"把子系统藏起来",而是提供一个方便的快捷方式。

第六层:Bridge——抽象与实现分离

你的通知系统需要支持"短信通知"和"邮件通知",并且每个通知可以包含"普通消息"或"紧急消息"。你的第一反应是画矩阵:4 个类(SmsNormal、SmsUrgent、EmailNormal、EmailUrgent)。再加一个通道(App 推送)变 6 个类。这不健康。

Bridge 模式把"抽象"(消息类型)和"实现"(通知通道)分离成两个独立的维度,通过组合连接。

java
// 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);
    }
}

使用:

java
// 紧急短信
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 模式:通过共享细粒度的、不可变的对象来节省内存。不可变部分(树种名称、贴图路径)作为内部状态共享,可变部分(坐标)作为外部状态不共享。

java
// 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 > BaseCalculatorHolidayDecorator > LoggingDecorator > BaseCalculator 的日志顺序不同。确保团队清楚装饰顺序约定。

陷阱三:Proxy 和 Decorator 长得太像。 结构上几乎一样,但意图不同:Proxy 控制访问(限流、权限、延迟加载),Decorator 添加行为(日志、缓存、加密)。同一段代码结构,但改的动机不同。

陷阱四:Composite 的叶子节点和组合节点接口不一致。 如果 SinglePrizeadd() 方法应该抛 UnsupportedOperationException——说明接口设计有问题。考虑用安全复合模式,把添加子节点的方法只放在 PrizePackage 中。


通关挑战

  • 热身:为一个已有的外部 API 调用写 Adapter,让你的业务代码不用直接依赖第三方库。
  • 挑战:为你的奖品计算系统写 LoggingProxy(记录每次调用耗时),而不是 LoggingDecorator。
  • 观察:看看你使用的框架中的 Decorator 例子——Java BufferedInputStreamFileInputStream、Python @lru_cache 装饰器。它们解决了什么问题?

旅人笔记

七种结构型模式各自解决一种"组装问题":Adapter 翻译接口,Decorator 叠加功能,Proxy 控制访问,Composite 统一树形对象,Facade 简化调用,Bridge 分离维度,Flyweight 共享细粒度。你的工具箱开始充实了。


下一站预告

对象怎么创建、怎么组装都解决了。接下来是对象之间怎么交互——谁通知谁、谁控制谁、谁给谁发消息。下一章:行为型模式——11 种对象协作的方式。

Built with VitePress | Software Systems Atlas