结构型模式 “全家桶”:适配、装饰、代理…7 种模式让你的代码更 “有章法”

深入解析常见结构型设计模式:原理、应用与对比

在软件开发中,随着系统规模扩大,类与对象的关系会变得复杂,如何合理组织它们以提升复用性、灵活性和可维护性,成为开发者面临的核心问题。结构型设计模式正是为解决这一问题而生,它聚焦于类与对象的组合方式,通过优化结构降低耦合度,让系统更易扩展和维护。

根据组合的核心依赖不同,结构型模式可分为类结构型模式对象结构型模式。类结构型模式依赖继承机制,将多个类的功能组合到一起,典型代表是适配器模式和桥接模式;对象结构型模式则依赖组合或聚合关系,通过对象间的动态关联实现功能扩展,更符合 "合成复用原则",如装饰器模式、代理模式等,也是实际开发中更常用的类型。

一、7 种常见结构型模式详解(如果想要快速了解可以直接看最后的表格)

1. 适配器模式:解决 "接口不兼容" 的桥梁

核心原理

适配器模式的本质是 "转换接口"------ 当现有类的接口与客户端期望的接口不一致时,通过一个 "适配器" 类,将现有类的接口转换成客户端能识别的格式,让原本因接口不兼容无法协作的类可以一起工作。它就像生活中 "电源转换器",让不同插头的设备能在同一插座上使用。

应用场景
  • 集成第三方组件时,第三方接口与项目内部接口不匹配(如调用支付接口,第三方返回格式与项目统一返回格式不同);
  • 维护旧系统时,旧模块接口无法修改,但新模块需要调用旧模块功能;
  • 复用现有类,但该类的接口不符合当前业务需求。
实例说明

假设项目中需要计算 "矩形面积",现有一个LegacyRectangle类(旧系统遗留),它的方法是getLength()和getWidth(),但当前业务代码期望调用getX()(对应长度)和getY()(对应宽度)的接口。此时可创建一个适配器类RectangleAdapter,实现业务期望的接口,并在内部持有LegacyRectangle对象,通过适配器方法将旧接口转换为新接口:

csharp 复制代码
// 旧系统类(接口不兼容)
class LegacyRectangle {
    private int length;
    private int width;
    public int getLength() { return length; }
    public int getWidth() { return width; }
}
// 业务期望的接口
interface TargetShape {
    int getX();
    int getY();
}
// 适配器类
class RectangleAdapter implements TargetShape {
    private LegacyRectangle legacyRect;
    public RectangleAdapter(LegacyRectangle legacyRect) {
        this.legacyRect = legacyRect;
    }
    @Override
    public int getX() {
        return legacyRect.getLength(); // 转换旧接口为新接口
    }
    @Override
    public int getY() {
        return legacyRect.getWidth();
    }
}

2. 装饰器模式:动态给对象 "加功能"

核心原理

装饰器模式的核心是 "动态扩展"------ 在不修改原有对象代码、不改变对象结构的前提下,通过创建 "装饰器" 对象,将新功能附加到原有对象上,且可以灵活组合多个装饰器,实现功能的叠加。它就像给手机 "贴钢化膜、装手机壳",手机本身功能不变,但新增了防摔、防刮的功能,且可以随时更换装饰。

应用场景
  • 需要给对象动态添加功能,且功能可随时撤销(如给文本添加 "加粗""斜体""下划线",可单独添加也可组合,还能取消某一效果);
  • 避免通过继承生成大量子类(如一个文本类,若通过继承实现 "加粗文本""斜体文本""加粗 + 斜体文本",会产生多个子类,装饰器模式可通过组合解决);
  • 扩展的功能需要灵活组合(如日志系统,可动态添加 "时间戳日志""用户 ID 日志""错误级别日志")。
实例说明

以 "文本格式化" 为例,基础类Text实现 "显示文本" 功能,通过装饰器BoldDecorator(加粗)、ItalicDecorator(斜体)动态扩展功能:

scala 复制代码
// 基础文本类(被装饰者)
class Text {
    public String display() {
        return "原始文本";
    }
}
// 装饰器抽象类(持有被装饰者对象)
abstract class TextDecorator extends Text {
    protected Text text;
    public TextDecorator(Text text) {
        this.text = text;
    }
}
// 加粗装饰器
class BoldDecorator extends TextDecorator {
    public BoldDecorator(Text text) {
        super(text);
    }
    @Override
    public String display() {
        return "<b>" + text.display() + "</b>"; // 附加加粗功能
    }
}
// 斜体装饰器
class ItalicDecorator extends TextDecorator {
    public ItalicDecorator(Text text) {
        super(text);
    }
    @Override
    public String display() {
        return "<i>" + text.display() + "</i>"; // 附加斜体功能
    }
}
// 使用时可灵活组合:加粗+斜体
Text text = new ItalicDecorator(new BoldDecorator(new Text()));
System.out.println(text.display()); // 输出:<i><b>原始文本</b></i>

3. 代理模式:给对象 "找个代言人"

核心原理

代理模式的本质是 "控制访问"------ 为某个对象(目标对象)创建一个 "代理对象",客户端不直接访问目标对象,而是通过代理对象间接访问。代理对象可以在访问目标对象前后,添加额外操作(如权限校验、日志记录、缓存处理),从而实现对目标对象的保护或功能增强。它就像 "房产中介",购房者不直接与房东沟通,而是通过中介完成看房、议价、签约等流程,中介还能提供房源筛选、合同审核等额外服务。

应用场景
  • 需要对目标对象进行权限控制(如只有管理员能调用某个接口,代理对象先校验权限);
  • 调用目标对象前需要预处理(如参数校验、日志记录)或调用后需要后处理(如结果缓存、异常处理);
  • 目标对象创建成本高(如数据库连接对象),通过代理实现 "延迟加载"(仅在需要时创建目标对象)。
实例说明

以 "用户查询接口" 为例,目标对象UserService实现查询用户信息的功能,代理对象UserServiceProxy在查询前校验用户是否登录:

typescript 复制代码
// 目标对象接口
interface UserService {
    User queryUser(String userId);
}
// 目标对象实现
class UserServiceImpl implements UserService {
    @Override
    public User queryUser(String userId) {
        // 实际查询数据库的逻辑
        return new User(userId, "张三");
    }
}
// 代理对象
class UserServiceProxy implements UserService {
    private UserService userService;
    private LoginChecker loginChecker; // 权限校验工具
    public UserServiceProxy(UserService userService) {
        this.userService = userService;
        this.loginChecker = new LoginChecker();
    }
    @Override
    public User queryUser(String userId) {
        // 调用目标对象前:校验登录
        if (!loginChecker.isLoggedIn()) {
            throw new RuntimeException("请先登录");
        }
        // 调用目标对象
        User user = userService.queryUser(userId);
        // 调用后:记录查询日志
        System.out.println("用户" + userId + "查询成功");
        return user;
    }
}

4. 门面模式(外观模式):给复杂系统 "搭个简化入口"

核心原理

门面模式的核心是 "简化接口"------ 当一个子系统包含多个复杂的组件和接口时,创建一个 "门面类",将子系统的多个接口封装成一个统一的高层接口,客户端只需调用门面类的方法,无需关心子系统内部的复杂逻辑。它就像 "餐厅服务员",顾客不需要直接与后厨、收银台、配菜间沟通,只需告诉服务员需求,由服务员协调内部流程。

应用场景
  • 子系统结构复杂,客户端需要调用多个接口才能完成一个功能(如 "用户下单" 需要调用 "库存检查""订单创建""支付处理""物流生成" 4 个接口,门面类可封装为createOrder()一个接口);
  • 希望隔离客户端与子系统,降低客户端对於系统的依赖(如更换子系统时,只需修改门面类,客户端代码无需改动);
  • 维护旧系统时,为旧系统提供统一入口,减少新系统与旧系统的耦合。
实例说明

以 "电商下单" 为例,子系统包含InventoryService(库存检查)、OrderService(订单创建)、PaymentService(支付处理),门面类OrderFacade封装下单流程:

arduino 复制代码
// 子系统1:库存服务
class InventoryService {
    public boolean checkStock(String productId, int count) {
        // 检查库存是否充足
        return true;
    }
}
// 子系统2:订单服务
class OrderService {
    public String createOrder(String userId, String productId, int count) {
        // 创建订单,返回订单号
        return "ORDER123456";
    }
}
// 子系统3:支付服务
class PaymentService {
    public boolean pay(String orderId, double amount) {
        // 处理支付
        return true;
    }
}
// 门面类
class OrderFacade {
    private InventoryService inventoryService;
    private OrderService orderService;
    private PaymentService paymentService;
    public OrderFacade() {
        this.inventoryService = new InventoryService();
        this.orderService = new OrderService();
        this.paymentService = new PaymentService();
    }
    // 封装下单流程,提供统一接口
    public String createOrder(String userId, String productId, int count, double amount) {
        // 1. 检查库存
        if (!inventoryService.checkStock(productId, count)) {
            throw new RuntimeException("库存不足");
        }
        // 2. 创建订单
        String orderId = orderService.createOrder(userId, productId, count);
        // 3. 处理支付
        if (!paymentService.pay(orderId, amount)) {
            throw new RuntimeException("支付失败");
        }
        return orderId;
    }
}
// 客户端使用:只需调用门面类的一个方法
OrderFacade facade = new OrderFacade();
String orderId = facade.createOrder("USER001", "PROD001", 2, 299.8);

5. 桥接模式:解决 "多维度变化" 的耦合

核心原理

桥接模式的本质是 "分离抽象与实现"------ 当一个类存在两个或多个独立变化的维度(如 "形状" 和 "颜色",形状可分为圆形、矩形,颜色可分为红色、蓝色),若通过继承组合(如RedCircle、BlueCircle、RedRectangle、BlueRectangle),会产生大量子类(子类数量 = 维度 1 数量 × 维度 2 数量)。桥接模式通过 "组合" 代替 "继承",将两个维度分离为独立的抽象类和实现类,让它们可以各自独立扩展,再通过 "桥接" 关联起来。

应用场景
  • 类存在多个独立变化的维度,且每个维度都可能扩展(如 "操作系统" 和 "软件",操作系统可扩展为 Windows、Linux,软件可扩展为浏览器、办公软件);
  • 避免通过继承产生大量子类,导致系统臃肿;
  • 希望抽象层和实现层可以独立变化(如修改软件的实现逻辑,不影响操作系统的抽象,反之亦然)。
实例说明

以 "图形绘制" 为例,存在 "图形类型"(圆形、矩形)和 "颜色"(红色、蓝色)两个维度,用桥接模式分离:

java 复制代码
// 维度1:颜色(实现层,可独立扩展)
interface Color {
    void fill();
}
class Red implements Color {
    @Override
    public void fill() {
        System.out.print("红色");
    }
}
class Blue implements Color {
    @Override
    public void fill() {
        System.out.print("蓝色");
    }
}
// 维度2:图形(抽象层,持有颜色对象,通过桥接关联)
abstract class Shape {
    protected Color color; // 桥接:抽象层持有实现层对象
    public Shape(Color color) {
        this.color = color;
    }
    abstract void draw();
}
class Circle extends Shape {
    public Circle(Color color) {
        super(color);
    }
    @Override
    void draw() {
        color.fill();
        System.out.println("圆形");
    }
}
class Rectangle extends Shape {
    public Rectangle(Color color) {
        super(color);
    }
    @Override
    void draw() {
        color.fill();
        System.out.println("矩形");
    }
}
// 使用时:灵活组合两个维度
Shape redCircle = new Circle(new Red());
redCircle.draw(); // 输出:红色圆形
Shape blueRectangle = new Rectangle(new Blue());
blueRectangle.draw(); // 输出:蓝色矩形

6. 组合模式:处理 "部分 - 整体" 的树形结构

核心原理

组合模式的本质是 "统一部分与整体"------ 当对象呈现 "部分 - 整体" 的层次结构(如文件夹与文件,文件夹包含子文件夹和文件,子文件夹又可包含更小的文件夹和文件),通过组合模式让 "部分"(如文件)和 "整体"(如文件夹)实现相同的接口,客户端可以用统一的方式处理单个对象和组合对象,无需区分是 "部分" 还是 "整体"。

应用场景
  • 数据结构呈树形结构(如组织架构:公司→部门→小组→员工;文件系统:根目录→子目录→文件);
  • 客户端需要统一处理单个对象和组合对象(如计算 "文件夹大小",客户端只需调用getSize(),无需关心是文件夹还是文件,文件夹会自动递归计算子文件大小);
  • 希望动态添加或删除树形结构中的节点(如给文件夹添加子文件夹或文件)。
实例说明

以 "文件系统" 为例,FileComponent是统一接口,File(部分)和Folder(整体)实现该接口,Folder可包含多个FileComponent对象:

csharp 复制代码
// 统一接口(部分和整体都实现)
interface FileComponent {
    long getSize(); // 计算大小
    String getName();
}
// 部分:文件
class File implements FileComponent {
    private String name;
    private long size;
    @Override
    public long getSize() {
        return size; // 文件大小固定
    }
    @Override
    public String getName() {
        return name;
    }
}
// 整体:文件夹
class Folder implements FileComponent {
    private String name;
    private List<FileComponent> children = new ArrayList<>();
    // 添加子组件(文件或文件夹)
    public void add(FileComponent component) {
        children.add(component);
    }
    // 删除子组件
    public void remove(FileComponent component) {
        children.remove(component);
    }
    @Override
    public long getSize() {
        // 递归计算所有子组件的大小
        return children.stream().mapToLong(FileComponent::getSize).sum();
    }
    @Override
    public String getName() {
        return name;
    }
}
// 客户端使用:统一处理文件夹和文件
FileComponent docFile = new File("文档.txt", 1024);
FileComponent imgFile = new File("图片.png", 2048);
Folder docFolder = new Folder("文档文件夹");
docFolder.add(docFile);
docFolder.add(imgFile);
System.out.println(docFile.getSize()); // 输出:1024
System.out.println(docFolder.getSize()); // 输出:3072(1024+2048)

7. 享元模式:减少对象创建的 "内存优化师"

核心原理

享元模式的本质是 "复用对象"------ 当系统中存在大量相同或相似的对象(如围棋中的棋子,只有 "黑""白" 两种类型,却有上百个棋子对象),通过共享这些对象的 "公共部分"(如棋子颜色),只创建少量不同的对象,减少内存占用和对象创建开销。它将对象的属性分为 "内部状态"(可共享,如棋子颜色)和 "外部状态"(不可共享,如棋子位置),内部状态相同的对象可共享,外部状态通过参数传递。

应用场景
  • 系统中存在大量相同或相似的对象,且对象创建成本高(如数据库连接池,复用连接对象,避免频繁创建和关闭连接);
  • 对象的大部分属性是可共享的内部状态,少量属性是需要动态设置的外部状态;
  • 内存资源紧张,需要减少对象数量以降低内存消耗(如游戏中的粒子效果,大量粒子只需共享 "形状""颜色"

各模式的核心权衡点

设计模式 核心优点 主要缺点 适用场景总结
适配器模式 1. 无需修改原有代码,解决接口不兼容问题,符合 "开闭原则";2. 复用现有类,降低开发成本;3. 灵活切换适配对象,提升系统扩展性。 1. 过多使用会增加系统复杂度,导致调用链路变长;2. 适配复杂接口时,代码逻辑可能繁琐。 集成第三方组件、维护旧系统、接口格式不匹配的场景。
装饰器模式 1. 动态扩展功能,可随时添加 / 移除装饰,灵活性高;2. 避免通过继承生成大量子类,减少系统冗余;3. 装饰器可组合使用,实现功能叠加。 1. 多层装饰会增加对象创建成本,影响性能;2. 调试时需跟踪多层装饰链路,排查问题较复杂。 需动态扩展功能、功能可组合、避免子类膨胀的场景(如文本格式化、日志增强)。
代理模式 1. 隔离客户端与目标对象,保护目标对象;2. 可在访问前后添加额外操作(如权限校验、日志);3. 支持延迟加载,降低初始化成本。 1. 增加代理对象,导致系统层级变多;2. 简单场景下会增加不必要的代码复杂度。 权限控制、日志记录、延迟加载、远程调用(如 RPC 代理)的场景。
门面模式 1. 简化客户端调用,隐藏子系统复杂度;2. 降低客户端与子系统的耦合,便于子系统维护;3. 统一入口,便于监控和管理。 1. 门面类可能成为 "上帝类",职责过重;2. 子系统扩展时,可能需要修改门面类,违反 "开闭原则"。 子系统复杂、客户端需统一调用入口、需隔离子系统的场景(如电商下单流程)。
桥接模式 1. 分离抽象与实现,两者可独立扩展,减少子类数量;2. 灵活切换抽象或实现,适应多维度变化;3. 提高代码复用性,降低耦合。 1. 增加系统设计复杂度,需理解抽象与实现的分离逻辑;2. 简单场景下会过度设计。 类存在多维度变化(如形状 + 颜色、操作系统 + 软件)、需避免子类膨胀的场景。
组合模式 1. 统一处理部分与整体,客户端无需区分单个对象与组合对象;2. 支持动态添加 / 删除树形节点,扩展性强;3. 便于遍历和操作树形结构。 1. 限制节点类型时,需在代码中添加判断,违反 "开闭原则";2. 树形结构过深时,遍历性能可能下降。 数据呈树形结构(如文件系统、组织架构)、需统一处理部分与整体的场景。
享元模式 1. 复用对象,减少内存占用,降低对象创建成本;2. 集中管理共享对象,便于维护;3. 提升系统性能,尤其适合大量相似对象场景。 1. 分离内部 / 外部状态,增加代码复杂度;2. 外部状态需通过参数传递,可能影响使用便捷性。 存在大量相似对象、内存资源紧张、对象可拆分内部 / 外部状态的场景(如围棋棋子、连接池)。
相关推荐
yinke小琪3 小时前
Spring生态全家桶:从基础到微服务的演进与关联是什么?
java·后端·spring
ytadpole4 小时前
揭秘设计模式:从UI按钮到Spring事件的观察者模式
设计模式
AAA修煤气灶刘哥4 小时前
微服务又崩了?5 招 + Sentinel 救场,后端小白也能学会
java·后端·spring cloud
渣哥4 小时前
90% 的 Java 初学者都搞不懂的 List、Set、Map 区别
java
何中应5 小时前
Spring Boot单体项目整合Nacos
java·spring boot·后端
dylan_QAQ5 小时前
Java转Go全过程01-基础语法部分
java·后端·go
Ka1Yan5 小时前
[算法] 双指针:本质是“分治思维“——从基础原理到实战的深度解析
java·开发语言·数据结构·算法·面试
轮子大叔6 小时前
Spark学习记录
java·spark
一语长情6 小时前
RocketMQ 消息队列冷读问题的分析与优化
java·后端·架构