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

在软件开发中,随着系统规模扩大,类与对象的关系会变得复杂,如何合理组织它们以提升复用性、灵活性和可维护性,成为开发者面临的核心问题。结构型设计模式正是为解决这一问题而生,它聚焦于类与对象的组合方式,通过优化结构降低耦合度,让系统更易扩展和维护。
根据组合的核心依赖不同,结构型模式可分为类结构型模式 和对象结构型模式。类结构型模式依赖继承机制,将多个类的功能组合到一起,典型代表是适配器模式和桥接模式;对象结构型模式则依赖组合或聚合关系,通过对象间的动态关联实现功能扩展,更符合 "合成复用原则",如装饰器模式、代理模式等,也是实际开发中更常用的类型。
一、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. 外部状态需通过参数传递,可能影响使用便捷性。 | 存在大量相似对象、内存资源紧张、对象可拆分内部 / 外部状态的场景(如围棋棋子、连接池)。 |