结构型模式:组合与包装的艺术

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


前言

创建型模式管"对象怎么来",结构型模式管"对象怎么搭"。

写代码时经常遇到这类问题:

  • 接口不匹配,两个现成组件对不上——适配器
  • 想给对象加功能,但不想改原来的类——装饰器
  • 系统太复杂,外面用起来太累——外观
  • 对象太重,不想一次性全加载——代理

结构型模式一共 7 种,每种解决一类"搭积木"的问题。


一、适配器模式(Adapter)

一句话

把一个类的接口转换成客户端期望的另一个接口,让原本不兼容的类能一起工作。

生活中的例子

  • 电源转换头:你带了个美标插头的笔记本去欧洲,插不进去。买一个转换头(适配器),插头没变,插座没变,中间加了一层转换。
  • 翻译员:中国老板和外国客户谈生意,语言不通。找个翻译,两边都不需要改变自己的语言,翻译负责转换。
  • 读卡器:电脑只有 USB 口,你的数据在 SD 卡里。读卡器就是适配器——把 SD 卡接口适配成 USB 接口。

场景

你系统里用的是自定义的 Logger 接口,但现在要接入第三方日志库,接口完全不同。第三方库不可能为了你改代码,你的系统已有几百处调用也不想改。

示意图

image

代码实现

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 种组合。桥接让两边独立扩展。

为什么需要它

如果用继承来搞所有组合,类数量是两个维度的乘积。桥接把它降为加法

示例图

image

代码实现

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,其他类零改动
![image](uploading...)

// ═══════════════════════════════════════════════════
// 抽象维度:消息类型
// 这一侧专注"发什么"(内容格式),不关心"怎么发"
// 新增类型只需继承 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() → 里面可能有文件、也可能有子文件夹,但你不关心,它自己会处理。

这就是"组合"——把整体和部分组合成统一的接口,让外部无法也无需区分。

示意图

image

代码实现

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 写好了就不能变
  • 装饰器是运行时组合的:想加就套一层,想去就剥一层,动态组装

示意图

image

代码实现

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

image

代码实现

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。

示意图

image

代码实现

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 就是一个网络代理。
  • 信用卡:信用卡是现金的代理。你不用带着一捆现金出门,刷卡就行。银行在背后处理真正的资金流动。

示意图

image

代理的种类

类型 干什么 例子
远程代理 本地对象代表远程对象 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

心法

  1. 优先组合,少用继承。 结构型模式几乎都在践行这条原则。继承把关系写死了,组合可以运行时灵活切换。
  2. 装饰器 vs 代理: 装饰器侧重"加功能"(用户主动包装),代理侧重"控制访问"(对用户透明)。
  3. 适配器是"事后补救",桥接是"事前设计"。 新系统尽量用桥接规划好维度;老系统对接不上了再用适配器补。
  4. 外观不是万能的。 如果子系统本身设计合理,不需要强行套外观。外观是给"乱但不能重构"的系统兜底用的。

posted @ 2026-06-15 14:21  江鸟Dev  阅读(5)  评论(0)    收藏  举报