设计模式2-结构型模式

设计模式2-结构型模式

作用:通过合理组合类或对象,优化系统的结构(如类的继承关系、对象的关联关系),实现功能复用、结构灵活或降低复杂度。

2.1 桥接模式(Bridge Pattern)

一、什么是桥接模式?

桥接模式是 ** 结构型模式中用于 “解耦抽象与实现”** 的核心模式,其核心思想是:将抽象部分(Abstraction)与实现部分(Implementation)分离,使它们可以独立地变化,且两者通过 “桥接”(引用关系)连接

简单说:“把两个独立变化的维度拆分开,用‘桥’(关联关系)替代‘继承’,避免类爆炸”

这里的 “抽象” 指定义核心业务逻辑的类(如 “手机”),“实现” 指抽象类所依赖的具体功能(如 “手机软件”);“桥接” 则是抽象类中持有实现类的引用,通过委托调用实现功能,而非通过继承硬编码。

二、为什么需要桥接模式?(作用)

当一个系统存在两个或多个独立变化的维度(如 “手机品牌” 和 “手机软件”,“形状” 和 “颜色”),且维度之间需要组合时,用继承会导致 “类爆炸”(组合数量 = 维度 1 数量 × 维度 2 数量)。桥接模式的核心作用是:

  1. 解决类爆炸问题:将多维度的组合关系从 “继承层级” 转为 “关联关系”,减少类的数量(从M×N减少为M+N)。
  2. 实现维度独立扩展:每个维度可单独扩展(如新增手机品牌或新增手机软件),无需修改原有代码,符合 “开闭原则”。
  3. 分离抽象与实现:抽象部分(如手机)专注于核心逻辑,实现部分(如软件)专注于具体功能,职责更清晰。

三、反例:用继承处理多维度的问题

假设我们要设计一个手机系统,包含 “手机品牌”(如华为、小米)和 “手机软件”(如游戏、通讯录)两个维度,每个品牌的手机都需要支持这些软件。

用继承实现的缺陷:

// 1. 抽象手机类
abstract class Phone {
    public abstract void run(); // 运行软件
}

// 2. 华为手机+具体软件(继承组合)
class HuaweiGamePhone extends Phone {
    @Override
    public void run() {
        System.out.println("华为手机运行游戏");
    }
}
class HuaweiContactsPhone extends Phone {
    @Override
    public void run() {
        System.out.println("华为手机运行通讯录");
    }
}

// 3. 小米手机+具体软件(继承组合)
class XiaomiGamePhone extends Phone {
    @Override
    public void run() {
        System.out.println("小米手机运行游戏");
    }
}
class XiaomiContactsPhone extends Phone {
    @Override
    public void run() {
        System.out.println("小米手机运行通讯录");
    }
}

// 客户端使用
public class Client {
    public static void main(String[] args) {
        Phone huaweiGame = new HuaweiGamePhone();
        huaweiGame.run(); // 华为手机运行游戏
    }
}

问题分析:

  • 类爆炸:若有M个品牌和N个软件,需要创建M×N个子类(如 2 个品牌 ×2 个软件 = 4 个类)。若扩展到 5 个品牌 ×10 个软件,则需要 50 个类,系统极度臃肿;
  • 扩展困难:新增品牌(如苹果)需为每个软件新增子类(AppleGamePhoneAppleContactsPhone...);新增软件(如浏览器)需为每个品牌新增子类(HuaweiBrowserPhoneXiaomiBrowserPhone...),严重违反 “开闭原则”;
  • 职责混乱:子类同时承担 “品牌” 和 “软件” 的职责(如HuaweiGamePhone既管华为的特性,又管游戏的逻辑),耦合过高。
四、正例:用桥接模式解决问题

核心改进:将 “手机品牌”(抽象维度)和 “手机软件”(实现维度)拆分为两个独立层次,通过 “桥接”(品牌持有软件的引用)实现组合,而非继承

桥接模式的实现:

// 1. 定义“实现维度”接口(软件):具体功能的抽象
interface Software {
    void run(); // 软件运行逻辑
}

// 2. 具体实现类(不同软件)
class Game implements Software {
    @Override
    public void run() {
        System.out.println("运行游戏");
    }
}
class Contacts implements Software {
    @Override
    public void run() {
        System.out.println("运行通讯录");
    }
}
// 新增软件:浏览器(无需修改其他类)
class Browser implements Software {
    @Override
    public void run() {
        System.out.println("运行浏览器");
    }
}

// 3. 定义“抽象维度”类(手机):持有实现维度的引用(桥接的核心)
abstract class Phone {
    // 桥接:抽象类关联实现接口(而非继承)
    protected Software software;

    // 通过构造函数注入具体软件
    public Phone(Software software) {
        this.software = software;
    }

    // 抽象方法:手机运行软件(委托给实现维度)
    public abstract void runSoftware();
}

// 4. 扩展抽象类(不同品牌手机)
class HuaweiPhone extends Phone {
    public HuaweiPhone(Software software) {
        super(software);
    }

    @Override
    public void runSoftware() {
        System.out.print("华为手机:");
        software.run(); // 委托给软件的run()方法
    }
}
class XiaomiPhone extends Phone {
    public XiaomiPhone(Software software) {
        super(software);
    }

    @Override
    public void runSoftware() {
        System.out.print("小米手机:");
        software.run();
    }
}
// 新增品牌:苹果手机(无需修改其他类)
class IPhone extends Phone {
    public IPhone(Software software) {
        super(software);
    }

    @Override
    public void runSoftware() {
        System.out.print("苹果手机:");
        software.run();
    }
}

// 5. 客户端:自由组合品牌和软件
public class Client {
    public static void main(String[] args) {
        // 华为手机+游戏
        Phone huaweiGame = new HuaweiPhone(new Game());
        huaweiGame.runSoftware(); // 华为手机:运行游戏

        // 小米手机+通讯录
        Phone xiaomiContacts = new XiaomiPhone(new Contacts());
        xiaomiContacts.runSoftware(); // 小米手机:运行通讯录

        // 苹果手机+浏览器(新增组合,无需修改原有类)
        Phone iPhoneBrowser = new IPhone(new Browser());
        iPhoneBrowser.runSoftware(); // 苹果手机:运行浏览器
    }
}

改进效果:

  1. 解决类爆炸:类数量从M×N减少为M+N(2 个品牌 + 3 个软件 = 5 个类,而非 6 个继承组合类),扩展后优势更明显(5 个品牌 + 10 个软件 = 15 个类,而非 50 个);

  2. 维度独立扩展:

    • 新增品牌(如IPhone):只需继承Phone,无需修改软件类;

    • 新增软件(如Browser):只需实现Software,无需修改品牌类;

      完全符合 “开闭原则”;

  3. 职责清晰:品牌类(HuaweiPhone)专注于品牌特性,软件类(Game)专注于软件逻辑,通过 “桥接”(software引用)协同工作,耦合度低;

  4. 组合灵活:客户端可动态组合品牌和软件(如 “华为 + 浏览器”“苹果 + 游戏”),无需提前定义所有组合类。

五、总结

桥接模式的核心是 “拆分维度,用桥接关联替代继承组合”,通过将抽象层与实现层分离,解决了多维度场景下的类爆炸问题,同时支持各维度独立扩展。

它的关键是准确识别系统中的独立变化维度(如 “品牌” 和 “功能”),并通过抽象类持有实现接口的引用,形成 “桥” 式连接。实际开发中,当发现类的数量随维度组合呈指数增长时,桥接模式往往是最佳解决方案。

记住:桥接模式让多维度的系统 “拆得开、合得拢、扩得展”

2.2 代理模式(Proxy Pattern)

一、什么是代理模式?

代理模式是 ** 结构型模式中用于 “控制对象访问”** 的核心模式,其核心思想是:为某一对象(目标对象)提供一种代理对象,通过代理对象间接访问目标对象,从而在访问前后添加额外操作(如权限校验、日志记录)或控制访问时机(如延迟加载)

简单说:“代理就像‘中介’,你想访问目标对象?先经过我,我帮你处理一些额外的事,再把请求传给目标”

日常生活中,租房中介是房东的 “代理”(租客通过中介找房东租房,中介负责筛选租客、签订合同);明星经纪人是明星的 “代理”(商务合作先经经纪人对接,经纪人负责谈判、筛选资源),都是代理模式的体现。

二、为什么需要代理模式?(作用)

直接访问目标对象可能导致 “访问逻辑与业务逻辑耦合”(如每次调用方法都要手动加日志)、“无法控制访问权限”(如随意调用敏感方法)或 “目标对象创建成本高”(如提前加载大对象)。代理模式的核心作用是:

  1. 控制访问:决定是否允许客户端访问目标对象(如权限校验,无权限则拒绝)。
  2. 增强功能:在访问目标对象的前后添加通用逻辑(如日志记录、性能监控、事务管理),且不修改目标对象代码(符合 “开闭原则”)。
  3. 延迟初始化:延迟创建目标对象(如大对象或耗时对象,直到真正需要时才创建,节省资源)。
  4. 隐藏实现:客户端无需知道目标对象的具体实现,只需与代理交互,降低耦合。

三、反例:没有代理模式的问题

假设我们要设计一个 “图片加载器”,需要在加载图片前后记录日志,且只有管理员有权限加载高清图片。

不使用代理模式的实现:

// 目标对象:图片加载器
class ImageLoader {
    // 加载图片(业务逻辑)
    public void loadHighResolution(String imagePath) {
        // 业务逻辑:实际加载高清图片(耗时操作)
        System.out.println("加载高清图片:" + imagePath);
    }
}

// 客户端:直接访问目标对象(问题核心)
public class Client {
    public static void main(String[] args) {
        ImageLoader loader = new ImageLoader();
        String userRole = "guest"; // 当前用户是游客(无权限)

        // 问题1:权限校验逻辑散落在客户端,重复且难维护
        if ("admin".equals(userRole)) {
            // 问题2:日志逻辑与业务逻辑耦合,每次调用都要写
            System.out.println("日志:开始加载图片");
            loader.loadHighResolution("photo.jpg");
            System.out.println("日志:图片加载完成");
        } else {
            System.out.println("错误:无权限加载高清图片");
        }
    }
}

问题分析:

  • 逻辑耦合:权限校验、日志记录等 “非业务逻辑” 与客户端代码耦合,若多个地方需要调用loadHighResolution,则这些逻辑会重复编写(违反 “DRY 原则”);
  • 修改困难:若需修改日志格式(如添加时间戳)或权限规则(如新增 “VIP” 角色),需修改所有调用处的代码,维护成本高;
  • 目标对象暴露:客户端直接持有ImageLoader实例,可能绕过权限校验直接调用,存在安全风险;
  • 无法延迟加载ImageLoader可能是一个大对象(如初始化时需加载配置),但客户端创建后可能暂时不用,导致资源浪费。

四、正例:用代理模式解决问题

核心改进:创建 “代理类”,由代理类持有目标对象的引用,统一处理权限校验、日志记录等逻辑,客户端只与代理交互

代理模式的基本实现(静态代理)
// 1. 抽象主题:定义目标对象和代理的共同接口(核心,保证代理与目标一致性)
interface Image {
    void loadHighResolution(String imagePath);
}

// 2. 真实主题(目标对象):专注于业务逻辑
class RealImageLoader implements Image {
    @Override
    public void loadHighResolution(String imagePath) {
        // 只负责核心业务:加载图片
        System.out.println("加载高清图片:" + imagePath);
    }
}

// 3. 代理类:持有目标对象,控制访问并增强功能
class ImageProxy implements Image {
    // 持有目标对象的引用(代理的核心)
    private RealImageLoader realLoader; 
    private String userRole; // 当前用户角色(用于权限校验)

    // 构造函数:传入用户角色,延迟初始化目标对象
    public ImageProxy(String userRole) {
        this.userRole = userRole;
        // 目标对象暂不创建,直到真正需要时(延迟加载)
        this.realLoader = null; 
    }

    @Override
    public void loadHighResolution(String imagePath) {
        // 1. 访问前增强:权限校验
        if (!"admin".equals(userRole)) {
            System.out.println("代理拦截:无权限加载高清图片(用户角色:" + userRole + ")");
            return;
        }

        // 2. 延迟初始化目标对象(需要时才创建)
        if (realLoader == null) {
            realLoader = new RealImageLoader();
            System.out.println("代理:初始化图片加载器(延迟加载)");
        }

        // 3. 访问前增强:日志记录
        System.out.println("代理日志:开始加载图片 -> " + imagePath);

        // 4. 调用目标对象的业务方法(核心)
        realLoader.loadHighResolution(imagePath);

        // 5. 访问后增强:日志记录
        System.out.println("代理日志:图片加载完成 -> " + imagePath);
    }
}

// 4. 客户端:只与代理交互,无需关心目标对象
public class Client {
    public static void main(String[] args) {
        // 客户端创建代理(传入用户角色),不直接接触RealImageLoader
        Image proxy = new ImageProxy("admin"); // 管理员角色
        proxy.loadHighResolution("photo.jpg"); 
        // 输出:
        // 代理:初始化图片加载器(延迟加载)
        // 代理日志:开始加载图片 -> photo.jpg
        // 加载高清图片:photo.jpg
        // 代理日志:图片加载完成 -> photo.jpg

        // 测试无权限场景
        Image guestProxy = new ImageProxy("guest");
        guestProxy.loadHighResolution("secret.jpg"); 
        // 输出:代理拦截:无权限加载高清图片(用户角色:guest)
    }
}

改进效果:

  1. 逻辑解耦:权限校验、日志记录等通用逻辑被统一放在代理类中,客户端和目标对象无需关心,避免重复代码;
  2. 控制访问:代理拦截所有对目标对象的访问,确保只有授权用户才能调用,安全性提升;
  3. 延迟加载:目标对象(RealImageLoader)在真正需要时才创建,避免提前占用资源;
  4. 易于维护:修改日志格式或权限规则时,只需修改代理类,客户端和目标对象代码无需变动,符合 “开闭原则”;
  5. 目标对象隐藏:客户端只知道Image接口和代理,不知道RealImageLoader的存在,降低耦合。

五、代理模式的核心结构

代理模式包含 3 个核心角色,通过 “接口” 保证代理与目标的一致性:

  1. 抽象主题(Subject)
    • 定义目标对象和代理类的共同接口(如Image),声明核心业务方法(如loadHighResolution);
    • 客户端通过该接口与代理 / 目标交互,保证 “代理可替代目标”(里氏替换原则)。
  2. 真实主题(Real Subject)
    • 实现Subject接口,是代理所代表的 “目标对象”(如RealImageLoader);
    • 专注于核心业务逻辑(如实际加载图片),不关心访问控制或增强逻辑。
  3. 代理(Proxy)
    • 实现Subject接口,持有Real Subject的引用(如ImageProxy持有RealImageLoader);
    • 负责控制对Real Subject的访问(如权限校验),并在访问前后添加增强逻辑(如日志);
    • 可能延迟初始化Real Subject(需要时才创建)。

六、代理模式的常见类型

根据代理的创建时机和实现方式,代理模式可分为静态代理动态代理

1. 静态代理(如上述示例)
  • 特点:代理类在编译期手动编写,与目标类一一对应(一个目标类对应一个代理类)。
  • 优点:实现简单,逻辑清晰,易于调试;
  • 缺点:代理类需手动编写,若目标类多或方法多,会产生大量重复代码(如为 10 个目标类写代理,每个有 5 个方法,需写 50 个代理方法)。
2. 动态代理(运行时自动生成代理类)

动态代理通过反射在运行时动态生成代理类,无需手动编写代理代码,解决静态代理的 “类爆炸” 问题。常见实现有:

(1)JDK 动态代理(基于接口)

Java 自带的动态代理机制,要求目标类必须实现接口,代理类由java.lang.reflect.Proxy动态生成。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// 1. 抽象主题(接口,JDK代理必须)
interface Image {
    void loadHighResolution(String imagePath);
}

// 2. 真实主题
class RealImageLoader implements Image {
    @Override
    public void loadHighResolution(String imagePath) {
        System.out.println("加载高清图片:" + imagePath);
    }
}

// 3. 动态代理处理器(核心:定义增强逻辑)
class ImageInvocationHandler implements InvocationHandler {
    private Object target; // 目标对象(可通用,不局限于Image)
    private String userRole;

    public ImageInvocationHandler(Object target, String userRole) {
        this.target = target;
        this.userRole = userRole;
    }

    // 所有代理方法的调用都会转发到invoke()
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 1. 访问前增强:权限校验
        if (!"admin".equals(userRole)) {
            System.out.println("动态代理拦截:无权限访问");
            return null;
        }

        // 2. 访问前增强:日志
        System.out.println("动态代理日志:调用方法 " + method.getName() + ",参数:" + args[0]);

        // 3. 调用目标对象的方法
        Object result = method.invoke(target, args);

        // 4. 访问后增强:日志
        System.out.println("动态代理日志:方法 " + method.getName() + " 执行完成");

        return result;
    }
}

// 4. 客户端:通过Proxy动态生成代理
public class Client {
    public static void main(String[] args) {
        // 目标对象
        Image realLoader = new RealImageLoader();
        // 创建处理器(传入目标和用户角色)
        InvocationHandler handler = new ImageInvocationHandler(realLoader, "admin");
        // 动态生成代理类(参数:类加载器、目标接口、处理器)
        Image proxy = (Image) Proxy.newProxyInstance(
            Image.class.getClassLoader(),
            new Class[]{Image.class},
            handler
        );

        // 调用代理方法(实际执行invoke())
        proxy.loadHighResolution("dynamic.jpg");
        // 输出:
        // 动态代理日志:调用方法 loadHighResolution,参数:dynamic.jpg
        // 加载高清图片:dynamic.jpg
        // 动态代理日志:方法 loadHighResolution 执行完成
    }
}
(2)CGLIB 动态代理(基于继承)

第三方库(需引入 CGLIB 依赖),无需目标类实现接口,通过继承目标类动态生成代理(子类代理父类),适用于无接口的类。

动态代理的核心优势:一个代理处理器可代理多个目标类(如ImageInvocationHandler可同时代理ImageFile等不同接口的类),大幅减少代码量,是框架中常用的代理方式(如 Spring AOP 的核心就是动态代理)。

七、总结

代理模式的核心是 “通过代理对象间接访问目标,控制访问并增强功能”,它将 “访问控制 / 通用逻辑” 与 “核心业务逻辑” 分离,既保护了目标对象,又简化了客户端代码。

实际开发中,静态代理适合简单场景(目标类少、方法少),动态代理(JDK/CGLIB)适合复杂场景(需代理多个类或框架开发)。理解代理模式是掌握 Spring AOP 等框架的基础,也是解耦 “非业务逻辑” 与 “业务逻辑” 的重要手段。

记住:代理模式让 “访问” 更可控,让 “增强” 更灵活

2.3 组合模式(Composite Pattern)教程

一、什么是组合模式?

组合模式是 ** 结构型模式中专门处理 “树形结构”** 的核心模式,其核心思想是:将对象组合成树形结构以表示 “部分 - 整体” 的层次关系,并且使客户端对单个对象(叶子节点)和组合对象(容器节点)的使用具有一致性

简单说:“把单个元素和元素的组合当成同一种东西对待,用统一的方式操作整个树形结构”

日常生活中,文件系统(文件夹包含文件和子文件夹)、公司组织结构(部门包含员工和子部门)、菜单系统(主菜单包含子菜单和菜单项)都是 “部分 - 整体” 的树形结构,组合模式正是为这类场景设计的。

二、为什么需要组合模式?(作用)

当系统中存在 “部分 - 整体” 的树形层次关系(如文件夹与文件、部门与员工)时,直接区分 “单个对象” 和 “组合对象” 会导致客户端代码臃肿、扩展性差。组合模式的核心作用是:

  1. 客户端统一处理:客户端无需区分 “叶子节点”(单个对象,如文件)和 “容器节点”(组合对象,如文件夹),只需通过统一接口操作,简化代码。
  2. 简化树形结构操作:对整个树形结构的操作(如遍历、统计、展示)可通过递归在容器节点中实现,无需客户端手动处理层次关系。
  3. 易于扩展:新增叶子节点或容器节点时,只需实现统一接口,客户端代码无需修改(符合 “开闭原则”)。

三、反例:没有组合模式的问题

假设我们要设计一个文件系统,包含 “文件(File)” 和 “文件夹(Folder)”,文件夹可以包含文件或其他文件夹,需要实现 “展示所有内容” 的功能。

不使用组合模式的实现:

// 1. 单个对象:文件
class File {
    private String name;
    public File(String name) { this.name = name; }
    // 展示文件
    public void show() {
        System.out.println("文件:" + name);
    }
}

// 2. 组合对象:文件夹
class Folder {
    private String name;
    private List<File> files = new ArrayList<>(); // 只能包含文件
    private List<Folder> subFolders = new ArrayList<>(); // 只能包含子文件夹

    public Folder(String name) { this.name = name; }

    // 添加文件
    public void addFile(File file) { files.add(file); }
    // 添加子文件夹
    public void addFolder(Folder folder) { subFolders.add(folder); }

    // 展示文件夹内容(需分别处理文件和子文件夹)
    public void show() {
        System.out.println("文件夹:" + name);
        // 展示文件
        for (File file : files) {
            file.show();
        }
        // 展示子文件夹
        for (Folder subFolder : subFolders) {
            subFolder.show();
        }
    }
}

// 客户端:必须区分文件和文件夹,操作复杂
public class Client {
    public static void main(String[] args) {
        // 创建文件
        File file1 = new File("笔记.txt");
        File file2 = new File("图片.jpg");

        // 创建子文件夹
        Folder subFolder = new Folder("资料");
        subFolder.addFile(new File("报告.pdf"));

        // 创建根文件夹
        Folder root = new Folder("我的文档");
        root.addFile(file1);
        root.addFile(file2);
        root.addFolder(subFolder);

        // 问题1:客户端需手动调用不同对象的show(),若结构复杂,逻辑混乱
        root.show(); 
        // 输出:
        // 文件夹:我的文档
        // 文件:笔记.txt
        // 文件:图片.jpg
        // 文件夹:资料
        // 文件:报告.pdf

        // 问题2:若要遍历所有内容,客户端需分别处理File和Folder,代码冗余
        // (例如统计总数量,需写两个遍历逻辑)
    }
}

问题分析:

  • 客户端需区分类型:客户端必须知道当前操作的是File还是Folder,调用不同的方法(虽然示例中都有show(),但本质上是两个独立类),若要新增操作(如 “删除”“重命名”),需在两个类中分别实现,客户端也要分别调用;
  • 扩展性差:若新增 “快捷方式”(另一种叶子节点),Folderadd方法需新增addShortcut,且show方法需新增遍历逻辑,违反 “开闭原则”;
  • 树形结构处理复杂:客户端若要遍历整个树形结构(如统计所有文件数量),需手动判断每个节点是叶子还是容器,编写嵌套循环,代码冗长且易出错。

四、正例:用组合模式解决问题

核心改进:定义统一的 “组件接口”,让叶子节点(File)和容器节点(Folder)都实现该接口,容器节点通过递归管理子组件(无论子组件是叶子还是容器),客户端通过统一接口操作所有节点

组合模式的实现:

import java.util.ArrayList;
import java.util.List;

// 1. 抽象组件(Component):定义叶子和容器的统一接口(核心)
interface FileSystemComponent {
    void show(); // 统一的展示方法
    default void add(FileSystemComponent component) { 
        // 默认实现:叶子节点不支持add,抛出异常
        throw new UnsupportedOperationException("不支持添加操作"); 
    }
    default void remove(FileSystemComponent component) {
        // 默认实现:叶子节点不支持remove
        throw new UnsupportedOperationException("不支持删除操作");
    }
}

// 2. 叶子节点(Leaf):单个对象,不包含子组件
class File implements FileSystemComponent {
    private String name;
    public File(String name) { this.name = name; }

    @Override
    public void show() {
        System.out.println("文件:" + name); // 叶子节点的具体实现
    }
    // 无需重写add/remove,使用默认实现(抛出异常)
}

// 3. 容器节点(Composite):组合对象,可包含子组件(叶子或其他容器)
class Folder implements FileSystemComponent {
    private String name;
    // 存储子组件(统一为FileSystemComponent,无需区分叶子和容器)
    private List<FileSystemComponent> children = new ArrayList<>();

    public Folder(String name) { this.name = name; }

    // 实现add:添加子组件(支持文件或文件夹)
    @Override
    public void add(FileSystemComponent component) {
        children.add(component);
    }

    // 实现remove:删除子组件
    @Override
    public void remove(FileSystemComponent component) {
        children.remove(component);
    }

    // 实现show:递归展示自身及所有子组件
    @Override
    public void show() {
        System.out.println("文件夹:" + name);
        // 遍历所有子组件,统一调用show()(无需区分是File还是Folder)
        for (FileSystemComponent child : children) {
            child.show(); // 递归调用,自动处理层次结构
        }
    }
}

// 4. 客户端:通过统一接口操作所有节点,无需区分类型
public class Client {
    public static void main(String[] args) {
        // 创建叶子节点(文件)
        FileSystemComponent file1 = new File("笔记.txt");
        FileSystemComponent file2 = new File("图片.jpg");

        // 创建容器节点(子文件夹)
        FileSystemComponent subFolder = new Folder("资料");
        subFolder.add(new File("报告.pdf")); // 子文件夹添加文件

        // 创建根容器(根文件夹)
        FileSystemComponent root = new Folder("我的文档");
        root.add(file1);    // 根文件夹添加文件
        root.add(file2);    // 根文件夹添加文件
        root.add(subFolder); // 根文件夹添加子文件夹

        // 客户端统一调用show(),无需关心是文件还是文件夹
        root.show(); 
        // 输出:
        // 文件夹:我的文档
        // 文件:笔记.txt
        // 文件:图片.jpg
        // 文件夹:资料
        // 文件:报告.pdf

        // 新增操作:统计所有文件数量(通过统一接口递归实现)
        System.out.println("总文件数:" + countFiles(root)); // 输出:总文件数:3
    }

    // 工具方法:递归统计所有文件数量(客户端无需区分节点类型)
    private static int countFiles(FileSystemComponent component) {
        if (component instanceof File) {
            return 1; // 叶子节点(文件)计数1
        } else {
            // 容器节点:累加所有子组件的文件数
            Folder folder = (Folder) component;
            int count = 0;
            for (FileSystemComponent child : folder.children) {
                count += countFiles(child); // 递归统计
            }
            return count;
        }
    }
}

改进效果:

  1. 客户端统一处理:客户端通过FileSystemComponent接口操作所有节点(file1.show()root.show()),无需区分是File还是Folder,代码简化;

  2. 树形结构自动处理:容器节点(Folder)通过递归调用子组件的show()方法,自动遍历整个树形结构,客户端无需手动处理层次关系;

  3. 扩展性强:

    • 新增叶子节点(如 “快捷方式Shortcut”):只需实现FileSystemComponent接口,Folderaddshow方法无需修改;

    • 新增容器节点(如 “压缩文件夹ZipFolder”):只需继承Folder或实现FileSystemComponent,客户端可直接使用;

      完全符合 “开闭原则”;

  4. 功能复用:新增操作(如countFiles)时,可通过统一接口递归实现,无需为叶子和容器分别编写逻辑。

五、组合模式的核心结构

组合模式的核心是通过 “抽象组件” 统一叶子和容器的接口,形成三个核心角色:

  1. 抽象组件(Component)
    • 定义叶子节点和容器节点的统一接口(如FileSystemComponentshow()add()remove());
    • 声明所有组件共有的方法(如展示、添加、删除),并可为默认方法提供实现(如叶子节点不支持add,默认抛出异常);
    • 是客户端与组件交互的唯一入口,保证客户端对所有组件的使用一致性。
  2. 叶子节点(Leaf)
    • 实现Component接口,代表树形结构中的最小单元(如File),不包含子组件;
    • 对 “添加 / 删除子组件” 等方法提供默认实现(通常抛出异常,因为叶子不能包含子节点);
    • 专注于自身的业务逻辑(如Fileshow()只展示文件名)。
  3. 容器节点(Composite)
    • 实现Component接口,代表树形结构中的组合单元(如Folder),可以包含子组件(叶子或其他容器);
    • 维护一个子组件集合(如List<FileSystemComponent>),并实现add()remove()等管理子组件的方法;
    • 对核心业务方法(如show())的实现需递归调用所有子组件的对应方法,从而实现 “整体操作”。

六、组合模式的两种形式

根据抽象组件是否声明 “管理子组件” 的方法(add/remove),组合模式分为两种实现形式:

1. 透明模式(Transparent)
  • 特点:抽象组件(Component)中声明所有方法(包括add/remove等管理子组件的方法),叶子节点和容器节点都实现这些方法(叶子节点对管理方法提供默认实现,如抛出异常);
  • 优点:客户端完全不区分叶子和容器,接口统一,符合 “里氏替换原则”(任何组件都可被替换);
  • 缺点:叶子节点实现了无意义的管理方法(如Fileadd方法),可能导致客户端误用(调用叶子的add方法会抛异常)。

上述文件系统示例即为透明模式

2. 安全模式(Safe)
  • 特点:抽象组件(Component)只声明业务方法(如show()),不包含add/remove等管理方法;管理方法仅在容器节点(Composite)中声明和实现;
  • 优点:叶子节点不会有多余的管理方法,避免客户端误用;
  • 缺点:客户端需要区分叶子和容器(调用add方法前需判断是否为容器),破坏了接口的统一性。

安全模式的文件系统示例

// 抽象组件:只声明业务方法
interface FileSystemComponent {
    void show(); 
}

// 叶子节点:只实现业务方法
class File implements FileSystemComponent {
    @Override public void show() { ... }
}

// 容器节点:额外声明管理方法
class Folder implements FileSystemComponent {
    @Override public void show() { ... }
    public void add(FileSystemComponent component) { ... } // 仅容器有
    public void remove(FileSystemComponent component) { ... } // 仅容器有
}

// 客户端:需区分类型才能调用add(安全但不透明)
Folder root = new Folder("我的文档");
root.add(new File("笔记.txt")); // 正确(容器有add)
// file1.add(...) 编译报错(叶子没有add,安全)

七、总结

组合模式的核心是 “统一部分与整体,用树形结构管理层次关系”,通过抽象组件接口将叶子节点和容器节点统一起来,使客户端能以相同的方式操作单个对象和整个组合对象。

实际开发中,透明模式和安全模式的选择需权衡 “接口统一性” 和 “使用安全性”:若客户端完全信任且不会误用,透明模式更简洁;若需严格避免错误调用,安全模式更可靠。

记住:组合模式让树形结构的操作 “一视同仁”,让复杂层次的管理 “化繁为简”

2.4 装饰器模式(Decorator Pattern)

一、什么是装饰器模式?

装饰器模式是 ** 结构型模式中专注于 “动态增强对象功能”** 的核心模式,其核心思想是:在不改变原有对象结构和接口的前提下,通过 “包装”(装饰)的方式给对象动态添加新功能,且多个功能可以灵活组合

简单说:“像叠汉堡一样,在原有对象外面一层层包裹新功能,每层都保留原有的接口,最终得到一个增强版的对象”

日常生活中,给手机贴钢化膜(增强防刮功能)、套手机壳(增强防摔功能)就是装饰器模式的体现 —— 手机本身的功能没变,但通过 “装饰” 获得了新功能,且可以先贴膜再套壳(功能组合)。

二、为什么需要装饰器模式?(作用)

当需要给对象添加功能时,若用继承实现,会导致 “类爆炸”(每增加一个功能就需新增一个子类,多功能组合时子类数量呈指数增长)。装饰器模式的核心作用是:

  1. 动态扩展功能:在运行时为对象添加功能,而非编译时通过继承固定,灵活度更高。
  2. 功能组合灵活:多个装饰器可按任意顺序组合(如先加奶再加糖,或先加糖再加奶),实现多种功能组合效果。
  3. 不修改原有代码:通过 “包装” 而非修改原有类实现增强,符合 “开闭原则”(对扩展开放,对修改关闭)。
  4. 避免继承臃肿:用 “组合” 替代 “继承”,减少子类数量(从2^N减少为N个装饰器,N为功能数)。

三、反例:用继承实现功能扩展的问题

假设我们要设计一个咖啡系统,基础咖啡(如浓缩咖啡、拿铁)可以添加多种配料(如牛奶、糖、巧克力),每种配料会增加价格和描述。

用继承实现的缺陷:

// 1. 基础咖啡类
class Coffee {
    public String getDescription() { return "基础咖啡"; }
    public double getPrice() { return 10.0; }
}

// 2. 具体基础咖啡
class Espresso extends Coffee {
    @Override public String getDescription() { return "浓缩咖啡"; }
    @Override public double getPrice() { return 15.0; }
}

class Latte extends Coffee {
    @Override public String getDescription() { return "拿铁"; }
    @Override public double getPrice() { return 20.0; }
}

// 3. 继承实现功能扩展(问题核心:每加一种配料就需新增子类)
// 浓缩咖啡+牛奶
class EspressoWithMilk extends Espresso {
    @Override public String getDescription() { return super.getDescription() + "+牛奶"; }
    @Override public double getPrice() { return super.getPrice() + 3.0; }
}

// 浓缩咖啡+糖
class EspressoWithSugar extends Espresso {
    @Override public String getDescription() { return super.getDescription() + "+糖"; }
    @Override public double getPrice() { return super.getPrice() + 1.0; }
}

// 浓缩咖啡+牛奶+糖(组合功能需新增子类)
class EspressoWithMilkAndSugar extends EspressoWithMilk {
    @Override public String getDescription() { return super.getDescription() + "+糖"; }
    @Override public double getPrice() { return super.getPrice() + 1.0; }
}

// 拿铁+牛奶(虽然不合理,但继承体系下必须定义)
class LatteWithMilk extends Latte { ... }

// 拿铁+糖+巧克力...(子类数量爆炸)

问题分析:

  • 类爆炸:若有M种基础咖啡和N种配料,需要M×(2^N - 1)个子类(如 2 种咖啡 ×3 种配料 = 14 个子类),系统极度臃肿;
  • 扩展困难:新增一种配料(如 “巧克力”),需为每种基础咖啡及所有现有组合新增子类(如EspressoWithChocolateEspressoWithMilkAndChocolate...),违反 “开闭原则”;
  • 功能组合固定:继承是编译时确定的,无法在运行时动态组合功能(如用户点单时临时选择 “加奶 + 加糖”,无法用继承实现);
  • 代码冗余:每种配料的价格和描述逻辑在多个子类中重复(如 “加牛奶” 的+3.0价格在EspressoWithMilkLatteWithMilk中重复)。

四、正例:用装饰器模式解决问题

核心改进:定义 “装饰器” 类,通过 “包装” 原有对象(咖啡)的方式添加功能(配料),装饰器与被装饰对象实现同一接口,支持多层嵌套组合

装饰器模式的实现:

// 1. 抽象组件(Component):定义被装饰对象和装饰器的统一接口
interface Coffee {
    String getDescription(); // 描述(如“浓缩咖啡+牛奶”)
    double getPrice();       // 价格(基础价+配料价)
}

// 2. 具体组件(Concrete Component):基础对象(被装饰的核心)
class Espresso implements Coffee {
    @Override
    public String getDescription() {
        return "浓缩咖啡";
    }

    @Override
    public double getPrice() {
        return 15.0;
    }
}

class Latte implements Coffee {
    @Override
    public String getDescription() {
        return "拿铁";
    }

    @Override
    public double getPrice() {
        return 20.0;
    }
}

// 3. 抽象装饰器(Decorator):持有被装饰对象,实现统一接口(核心)
abstract class CoffeeDecorator implements Coffee {
    // 持有被装饰的Coffee对象(可以是基础咖啡,也可以是其他装饰器)
    protected Coffee decoratedCoffee;

    // 构造函数:传入被装饰对象
    public CoffeeDecorator(Coffee decoratedCoffee) {
        this.decoratedCoffee = decoratedCoffee;
    }

    // 默认实现:委托给被装饰对象(具体装饰器可重写增强)
    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription();
    }

    @Override
    public double getPrice() {
        return decoratedCoffee.getPrice();
    }
}

// 4. 具体装饰器(Concrete Decorator):实现具体功能扩展
class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }

    // 增强描述:添加“+牛奶”
    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription() + "+牛奶";
    }

    // 增强价格:加3元
    @Override
    public double getPrice() {
        return decoratedCoffee.getPrice() + 3.0;
    }
}

class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }

    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription() + "+糖";
    }

    @Override
    public double getPrice() {
        return decoratedCoffee.getPrice() + 1.0;
    }
}

// 新增装饰器:巧克力(无需修改原有类)
class ChocolateDecorator extends CoffeeDecorator {
    public ChocolateDecorator(Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }

    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription() + "+巧克力";
    }

    @Override
    public double getPrice() {
        return decoratedCoffee.getPrice() + 5.0;
    }
}

// 5. 客户端:动态组合装饰器,实现功能扩展
public class Client {
    public static void main(String[] args) {
        // 基础咖啡:浓缩咖啡
        Coffee espresso = new Espresso();
        System.out.println(espresso.getDescription() + ",价格:" + espresso.getPrice()); 
        // 输出:浓缩咖啡,价格:15.0

        // 装饰1:浓缩咖啡+牛奶(用MilkDecorator包装)
        Coffee espressoWithMilk = new MilkDecorator(espresso);
        System.out.println(espressoWithMilk.getDescription() + ",价格:" + espressoWithMilk.getPrice()); 
        // 输出:浓缩咖啡+牛奶,价格:18.0

        // 装饰2:浓缩咖啡+牛奶+糖(再用SugarDecorator包装)
        Coffee espressoWithMilkAndSugar = new SugarDecorator(espressoWithMilk);
        System.out.println(espressoWithMilkAndSugar.getDescription() + ",价格:" + espressoWithMilkAndSugar.getPrice()); 
        // 输出:浓缩咖啡+牛奶+糖,价格:19.0

        // 装饰3:拿铁+巧克力+糖(动态组合新功能)
        Coffee latteWithChocolateAndSugar = new SugarDecorator(
            new ChocolateDecorator(new Latte())
        );
        System.out.println(latteWithChocolateAndSugar.getDescription() + ",价格:" + latteWithChocolateAndSugar.getPrice()); 
        // 输出:拿铁+巧克力+糖,价格:26.0
    }
}

改进效果:

  1. 解决类爆炸:类数量从M×(2^N - 1)减少为M + N(2 种基础咖啡 + 3 种配料 = 5 个类),扩展后优势更明显(5 种咖啡 + 10 种配料 = 15 个类,而非 5×1023=5115 个);
  2. 动态功能组合:客户端可在运行时按任意顺序组合装饰器(如 “牛奶 + 糖”“糖 + 牛奶”“巧克力 + 糖”),无需提前定义所有组合,灵活性极大提升;
  3. 符合开闭原则:
    • 新增基础咖啡(如 “美式咖啡”):只需实现Coffee接口,不影响现有代码;
    • 新增配料(如 “巧克力”):只需新增ChocolateDecorator,无需修改原有装饰器或基础咖啡;
  4. 功能复用:每种配料的逻辑(如牛奶 + 3 元)集中在一个装饰器中,避免代码重复(如MilkDecorator可同时装饰EspressoLatte);
  5. 不改变原有对象:基础咖啡(Espresso)的代码未被修改,装饰器通过 “包装” 而非修改实现增强,安全性高。

五、装饰器模式的核心结构

装饰器模式通过 “接口统一” 和 “对象包装” 实现功能扩展,包含 4 个核心角色:

  1. 抽象组件(Component)
    • 定义被装饰对象和装饰器的统一接口(如CoffeegetDescription()getPrice());
    • 是客户端与所有组件(基础对象和装饰器)交互的唯一入口,保证 “装饰器可替代被装饰对象”(里氏替换原则)。
  2. 具体组件(Concrete Component)
    • 实现Component接口,是被装饰的核心对象(如EspressoLatte);
    • 提供基础功能,是装饰器的 “包装起点”。
  3. 抽象装饰器(Decorator)
    • 实现Component接口,持有一个Component类型的引用(被装饰对象,如decoratedCoffee);
    • 作为所有具体装饰器的父类,默认实现Component的方法(委托给被装饰对象),为具体装饰器提供扩展基础。
  4. 具体装饰器(Concrete Decorator)
    • 继承Decorator实现具体的功能增强(如MilkDecorator添加牛奶的描述和价格);
    • 重写Component的方法,在调用被装饰对象方法的前后添加自身逻辑(如先获取被装饰对象的价格,再加 3 元)。

六、总结

装饰器模式的核心是 “动态包装,叠加增强”,通过将功能封装在装饰器中,以组合的方式给对象添加功能,解决了继承导致的类爆炸问题,同时保证了扩展的灵活性。

它的关键是抽象组件接口的设计(保证装饰器与被装饰对象的一致性)和装饰器的嵌套组合(实现功能叠加)。实际开发中,当需要灵活扩展对象功能且避免继承臃肿时,装饰器模式是最优解之一。

记住:装饰器模式让功能扩展 “按需组合”,让代码设计 “拥抱变化”

2.5 适配器模式(Adapter Pattern)

一、什么是适配器模式?

适配器模式是结构型模式中专门解决 “接口不兼容” 问题的核心模式,其核心思想是:将一个类的接口转换成客户端期望的另一种接口,使原本因接口不匹配而无法一起工作的类能够协同工作

简单说:“适配器就像‘转接头’,比如手机充电时的电源适配器,将 220V 交流电转换成手机需要的 5V 直流电,让不兼容的设备能正常工作”

日常生活中,HDMI 转 VGA 的转换器(让 HDMI 设备连接 VGA 显示器)、USB-C 转 USB-A 的转接头(让新设备使用旧 U 盘),都是适配器模式的典型体现。

二、为什么需要适配器模式?(作用)

在系统开发或维护中,经常会遇到 “现有类的接口与客户端需求不匹配” 的问题(如旧系统接口与新系统不兼容、第三方库接口与自定义接口不一致)。直接修改现有类或客户端代码可能成本高、风险大(如旧系统代码不能动)。适配器模式的核心作用是:

  1. 解决接口不兼容:让接口不同的类可以一起工作,无需修改原有代码(保护旧系统或第三方库)。
  2. 复用现有功能:在不改变现有类的前提下,通过适配器复用其功能,避免重复开发。
  3. 隔离变化:客户端只依赖目标接口,适配器隔离了现有类与客户端的直接交互,降低耦合。
  4. 平滑过渡:在系统升级时,用适配器兼容新旧接口,实现平滑过渡(如逐步替换旧系统)。

三、反例:接口不兼容的问题

假设我们正在开发一个新的电商支付系统,需要集成一个第三方的 “旧版支付网关”(OldPaymentGateway),但两者的接口不匹配:

  • 新系统期望的支付接口是PaymentProcessor,方法为processPayment(int amount)(金额单位为 “分”,整数类型);
  • 旧支付网关的接口是OldPaymentGateway,方法为pay(double money)(金额单位为 “元”,小数类型)。

不使用适配器模式的问题:

// 1. 新系统期望的目标接口(金额单位:分,整数)
interface PaymentProcessor {
    void processPayment(int amount); // 例如:1000 表示 10元
}

// 2. 第三方旧支付网关(接口不兼容:金额单位为元,小数)
class OldPaymentGateway {
    // 例如:10.0 表示 10元
    public void pay(double money) {
        System.out.println("旧支付网关扣款:" + money + "元");
    }
}

// 3. 客户端:尝试直接使用旧支付网关(编译错误)
public class Client {
    public static void main(String[] args) {
        // 新系统需要调用PaymentProcessor接口
        PaymentProcessor processor;

        // 问题:OldPaymentGateway没有processPayment(int)方法,无法直接赋值
        processor = new OldPaymentGateway(); // 编译报错:类型不兼容

        // 若强行修改客户端代码适配旧接口,会导致客户端与旧系统强耦合:
        OldPaymentGateway oldGateway = new OldPaymentGateway();
        int newAmount = 1000; // 新系统的10元(1000分)
        double oldAmount = newAmount / 100.0; // 手动转换为10.0元
        oldGateway.pay(oldAmount); // 调用旧方法
        // 缺点:若有100处调用,需写100次转换逻辑,代码冗余且耦合高
    }
}

问题分析:

  • 接口不兼容:旧系统的pay(double)与新系统的processPayment(int)接口不匹配(方法名、参数类型、单位都不同),无法直接集成;
  • 代码冗余:客户端需手动处理参数转换(分转元、整数转小数),若多处调用,转换逻辑重复,维护成本高;
  • 耦合度高:客户端直接依赖旧系统的接口(OldPaymentGateway),若旧系统升级(如方法名变更),所有客户端调用处都需修改;
  • 风险大:若旧系统是第三方库或遗留系统,修改其代码可能引入未知风险(如破坏其他依赖它的模块)。

四、正例:用适配器模式解决问题

核心改进:创建 “适配器类”,实现新系统的目标接口(PaymentProcessor),内部持有旧系统对象(OldPaymentGateway),在适配器中完成接口转换(参数类型、单位转换),使客户端通过适配器间接调用旧系统

适配器模式的实现(对象适配器)
// 1. 目标接口(新系统期望的接口)
interface PaymentProcessor {
    void processPayment(int amount); // 金额单位:分(整数)
}

// 2. 适配者(Adaptee):需要被适配的旧系统/第三方类
class OldPaymentGateway {
    public void pay(double money) { // 金额单位:元(小数)
        System.out.println("旧支付网关扣款:" + money + "元");
    }
}

// 3. 适配器(Adapter):实现目标接口,持有适配者引用,完成转换
class PaymentAdapter implements PaymentProcessor {
    // 持有旧系统对象(通过组合实现适配,对象适配器的核心)
    private OldPaymentGateway oldGateway;

    // 构造函数:传入旧系统对象
    public PaymentAdapter(OldPaymentGateway oldGateway) {
        this.oldGateway = oldGateway;
    }

    // 实现目标接口方法:在内部完成转换并调用旧系统方法
    @Override
    public void processPayment(int amount) {
        // 1. 转换参数:分 -> 元(整数 -> 小数)
        double money = amount / 100.0;
        // 2. 调用旧系统的方法
        oldGateway.pay(money);
    }
}

// 4. 客户端:只依赖目标接口,不直接接触旧系统
public class Client {
    public static void main(String[] args) {
        // 创建旧系统对象
        OldPaymentGateway oldGateway = new OldPaymentGateway();
        // 创建适配器(包装旧系统对象)
        PaymentProcessor processor = new PaymentAdapter(oldGateway);

        // 客户端调用目标接口,适配内部自动处理转换
        processor.processPayment(1000); // 新系统传入1000分(10元)
        // 输出:旧支付网关扣款:10.0元

        processor.processPayment(2500); // 25元(2500分)
        // 输出:旧支付网关扣款:25.0元
    }
}

改进效果:

  1. 解决接口不兼容:适配器PaymentAdapter实现了PaymentProcessor接口,同时内部调用OldPaymentGateway的方法,让新旧系统能够协同工作;
  2. 隔离客户端与旧系统:客户端只依赖PaymentProcessor接口,不知道OldPaymentGateway的存在,若旧系统升级,只需修改适配器,客户端代码无需变动;
  3. 消除代码冗余:参数转换逻辑(分转元)集中在适配器中,避免客户端多处重复编写;
  4. 复用旧系统功能:在不修改OldPaymentGateway代码的前提下,复用其支付功能,保护了旧系统的稳定性;
  5. 符合开闭原则:新增其他旧支付网关(如VeryOldPaymentGateway),只需新增对应的适配器(VeryOldPaymentAdapter),无需修改客户端或目标接口。

五、适配器模式的核心结构

适配器模式通过 “转换接口” 连接不兼容的类,包含 3 个核心角色:

  1. 目标接口(Target)
    • 客户端期望的标准接口(如PaymentProcessor),定义了客户端需要的方法(如processPayment);
    • 客户端只与目标接口交互,不关心具体实现。
  2. 适配者(Adaptee)
    • 已存在的接口不兼容的类或对象(如OldPaymentGateway),包含客户端需要的功能,但接口不符合目标要求;
    • 通常是旧系统、第三方库或不可修改的类。
  3. 适配器(Adapter)
    • 实现目标接口,并持有适配者的引用(对象适配器)或继承适配者(类适配器);
    • 核心职责是:将目标接口的方法调用 “转换” 为对适配者的方法调用(如参数转换、方法名映射)。

六、适配器模式的两种实现方式

根据适配器与适配者的关系(组合 vs 继承),适配器模式分为两种形式:

1. 对象适配器(推荐)
  • 实现方式:适配器通过组合(持有适配者对象的引用)实现适配(如上述PaymentAdapter持有OldPaymentGateway对象);
  • 优点:
    • 符合 “组合优于继承” 原则,灵活性更高(一个适配器可适配多个适配者);
    • 适配者无需是具体类(可以是接口),适用范围更广;
    • 不会因适配者的修改影响适配器(松耦合);
  • 缺点:需要为每个适配者创建对应的适配器(若适配者多,可能增加类数量)。
2. 类适配器(通过继承)
  • 实现方式:适配器通过继承适配者,并实现目标接口(Java 中需用多重继承,因此适配者必须是类,不能是接口);
// 类适配器:继承适配者,实现目标接口
class PaymentClassAdapter extends OldPaymentGateway implements PaymentProcessor {
    @Override
    public void processPayment(int amount) {
        double money = amount / 100.0;
        super.pay(money); // 调用父类(适配者)的方法
    }
}

// 客户端使用:
PaymentProcessor processor = new PaymentClassAdapter();
processor.processPayment(1000); // 同样生效
  • 优点:代码更简洁(无需持有适配者引用);
  • 缺点:
    • 适配者必须是类(不能是接口),限制了适用场景;
    • 适配器与适配者强耦合(适配者修改可能影响适配器);
    • 无法适配多个适配者(Java 不支持多重继承)。

实际开发中,对象适配器因灵活性更高,应用更广泛

七、总结

适配器模式的核心是 “接口转换,兼容共存”,通过引入适配器类,在不修改原有代码的前提下,解决了接口不兼容的问题,实现了新旧系统、第三方库与自定义代码的平滑集成。

它的关键是 “组合优于继承”(优先使用对象适配器)和 “隔离变化”(客户端只依赖目标接口)。实际开发中,当遇到 “接口不匹配但需要复用现有功能” 的场景时,适配器模式是最直接有效的解决方案。

记住:适配器模式是系统集成的 “万能转接头”,让不兼容的接口 “握手言和”

2.6 享元模式(Flyweight Pattern)

一、什么是享元模式?

享元模式是 ** 结构型模式中专注于 “复用细粒度对象、减少内存消耗”** 的核心模式,其核心思想是:通过共享技术,复用系统中大量相似的细粒度对象,只保留对象的 “内部状态”(可共享部分),而将 “外部状态”(不可共享部分)通过参数传递,从而减少对象实例的数量,降低内存开销

简单说:“把多个相同对象的‘共性部分’抽出来共享,‘个性部分’单独传入,避免重复创建相同对象浪费内存”

日常生活中,围棋 / 象棋的棋子是典型案例:围棋只有黑白两色,无需为每个棋子创建独立对象,只需共享 “黑色棋子” 和 “白色棋子” 两个实例,通过记录它们的位置(外部状态)即可表示整个棋盘的棋子分布,大幅减少对象数量。

二、为什么需要享元模式?(作用)

当系统中存在大量细粒度、高相似性的对象(如文档中的字符、游戏中的粒子、棋盘上的棋子)时,每个对象都独立创建会导致内存占用过高、系统性能下降。享元模式的核心作用是:

  1. 减少内存消耗:复用相似对象,将对象数量从 “海量” 减少到 “少量可共享实例”(如围棋从 361 个对象减少到 2 个),大幅降低内存占用。
  2. 提高对象复用率:避免重复创建结构相似的对象,减少对象初始化的性能开销(如避免重复加载相同的资源)。
  3. 分离可变与不可变状态:将对象的 “不变部分”(内部状态)共享,“可变部分”(外部状态)动态传入,使对象更灵活。
  4. 优化资源密集型场景:在创建对象成本高(如加载图片、初始化配置)的场景中,复用对象可显著提升系统响应速度。

三、反例:大量相似对象导致的内存浪费

假设我们要设计一个围棋游戏,棋盘上有 361 个交叉点,每个棋子包含 “颜色”(黑 / 白)和 “位置”(x,y 坐标)两个属性。

不使用享元模式的实现:

// 围棋棋子类(未使用享元)
class GoChess {
    private String color; // 棋子颜色(黑/白,本可共享)
    private int x;        // 位置x(不可共享,每个棋子不同)
    private int y;        // 位置y(不可共享)

    // 每个棋子都创建独立对象,包含颜色和位置
    public GoChess(String color, int x, int y) {
        this.color = color;
        this.x = x;
        this.y = y;
        System.out.println("创建棋子:" + color + ",位置(" + x + "," + y + ")");
    }

    public void display() {
        System.out.println("显示棋子:" + color + ",位置(" + x + "," + y + ")");
    }
}

// 客户端:创建棋盘上的棋子(问题核心)
public class Client {
    public static void main(String[] args) {
        // 模拟创建10个黑棋和10个白棋(实际棋盘需361个)
        for (int i = 0; i < 10; i++) {
            // 每个黑棋都创建独立对象,颜色重复存储
            new GoChess("黑色", i, i).display();
            // 每个白棋都创建独立对象,颜色重复存储
            new GoChess("白色", i, 10 - i).display();
        }
    }
}

问题分析:

  • 内存浪费严重:361 个棋子中,只有 “黑色” 和 “白色” 两种类型,但每个棋子都独立存储 “颜色” 属性,导致相同颜色的信息重复存储(361 个对象的颜色字段重复,内存占用翻倍);
  • 对象创建成本高:若棋子还包含其他共享资源(如棋子图片、材质),每个对象都需重复加载这些资源,进一步消耗内存和 CPU;
  • 扩展性差:若需修改棋子颜色(如新增 “灰色” 棋子),需修改所有棋子对象的逻辑,维护成本高;
  • 系统性能下降:大量对象的创建和销毁会增加 JVM 的垃圾回收压力,导致系统响应变慢。

四、正例:用享元模式解决问题

核心改进:分离 “内部状态”(可共享的颜色)和 “外部状态”(不可共享的位置),创建 “享元工厂” 管理共享的棋子实例,客户端通过工厂获取共享实例,并传入外部状态(位置)使用。 享元模式的实现:

import java.util.HashMap;
import java.util.Map;

// 1. 抽象享元(Flyweight):定义棋子的核心行为,包含外部状态的传入方法
interface ChessFlyweight {
    // 显示棋子,位置(x,y)为外部状态,通过参数传入
    void display(int x, int y);
}

// 2. 具体享元(Concrete Flyweight):实现抽象享元,存储内部状态(颜色)
class ConcreteChess implements ChessFlyweight {
    // 内部状态:颜色(可共享,黑/白)
    private String color;

    // 构造函数:初始化内部状态(颜色)
    public ConcreteChess(String color) {
        this.color = color;
        System.out.println("创建享元棋子:" + color + "(仅创建一次)");
    }

    // 实现display方法:结合外部状态(位置)展示棋子
    @Override
    public void display(int x, int y) {
        System.out.println("显示享元棋子:" + color + ",位置(" + x + "," + y + ")");
    }
}

// 3. 享元工厂(Flyweight Factory):管理共享的享元实例,确保复用
class ChessFlyweightFactory {
    // 缓存享元实例(key:内部状态(颜色),value:享元对象)
    private static Map<String, ChessFlyweight> chessMap = new HashMap<>();

    // 获取享元实例:存在则返回,不存在则创建
    public static ChessFlyweight getChess(String color) {
        // 检查缓存中是否已有该颜色的棋子
        if (chessMap.containsKey(color)) {
            return chessMap.get(color);
        } else {
            // 创建新的享元实例并缓存
            ChessFlyweight chess = new ConcreteChess(color);
            chessMap.put(color, chess);
            return chess;
        }
    }
}

// 4. 客户端:通过工厂获取享元实例,传入外部状态使用
public class Client {
    public static void main(String[] args) {
        // 模拟创建10个黑棋和10个白棋(实际棋盘需361个)
        for (int i = 0; i < 10; i++) {
            // 获取黑棋享元实例(仅第一次创建,后续复用)
            ChessFlyweight blackChess = ChessFlyweightFactory.getChess("黑色");
            blackChess.display(i, i); // 传入外部状态(位置)

            // 获取白棋享元实例(仅第一次创建,后续复用)
            ChessFlyweight whiteChess = ChessFlyweightFactory.getChess("白色");
            whiteChess.display(i, 10 - i); // 传入外部状态(位置)
        }
    }
}

改进效果:

  1. 大幅减少内存消耗:无论创建多少棋子,系统中只存在 “黑色” 和 “白色” 两个享元实例(361 个棋子场景下,对象数量从 361 个减少到 2 个),避免重复存储内部状态;
  2. 复用对象资源:享元实例仅创建一次,若棋子包含图片等资源,只需加载一次,降低资源加载成本;
  3. 分离可变与不可变状态:内部状态(颜色)固定共享,外部状态(位置)动态传入,客户端可灵活设置棋子位置,不影响享元实例的复用;
  4. 易于扩展:新增 “灰色” 棋子时,只需通过工厂获取ChessFlyweightFactory.getChess("灰色"),自动创建并缓存新的享元实例,客户端无需修改核心逻辑;
  5. 提升系统性能:减少对象创建和垃圾回收的压力,系统响应速度显著提升。

五、享元模式的核心结构

享元模式通过 “分离状态 + 缓存复用” 实现对象优化,包含 4 个核心角色:

  1. 抽象享元(Flyweight)
    • 定义享元对象的核心行为接口(如ChessFlyweightdisplay方法);
    • 声明外部状态的传入方式(如display方法的x,y参数),明确内部状态与外部状态的分离边界。
  2. 具体享元(Concrete Flyweight)
    • 实现Flyweight接口,存储可共享的内部状态(如ConcreteChesscolor字段);
    • 内部状态独立于环境,不随外部变化(如黑色棋子的颜色始终为黑色);
    • 核心方法结合外部状态完成业务逻辑(如display方法结合位置展示棋子)。
  3. 享元工厂(Flyweight Factory)
    • 负责创建和管理享元实例,通过缓存机制确保相同内部状态的享元只存在一个;
    • 提供get方法(如getChess),根据内部状态从缓存中获取或创建享元实例,是享元模式的核心管理组件。
  4. 外部状态(Extrinsic State)
    • 不可共享的状态,随环境变化而变化(如棋子的x,y位置);
    • 由客户端传入享元对象的方法中,不存储在享元实例内部,避免影响享元的复用。
关键:内部状态 vs 外部状态
  • 内部状态:可共享、不变的属性(如棋子颜色、字符编码、图片资源),存储在享元实例中;

  • 外部状态:不可共享、可变的属性(如位置、时间、用户信息),由客户端传入,不存储在享元中。

    两者的分离是享元模式的核心,决定了对象复用的效率。

六、享元模式的两种形式

根据是否包含外部状态,享元模式分为 “单纯享元模式” 和 “复合享元模式”:

1. 单纯享元模式(如上述示例)
  • 特点:所有享元实例都是 “单纯享元”,仅包含内部状态,外部状态通过方法参数传入;
  • 适用场景:外部状态简单,且与享元实例的关联松散(如棋子位置与棋子本身无强绑定)。
2. 复合享元模式(组合享元)
  • 特点:将多个单纯享元组合成一个 “复合享元”,复合享元本身也可作为享元被缓存;
  • 适用场景:外部状态存在组合关系(如文档中的 “单词” 由多个 “字符” 享元组成,单词可作为复合享元缓存)。

实际开发中,单纯享元模式应用更广泛,复合享元模式因结构复杂,仅在特定场景(如组合对象的复用)中使用。

七、总结

享元模式的核心是 “共享复用细粒度对象,分离内部与外部状态”,通过享元工厂的缓存机制,将对象数量从 “海量” 压缩到 “少量可共享实例”,从而解决内存浪费和性能下降的问题。

它的关键是准确区分 “内部状态”(可共享)和 “外部状态”(不可共享),并通过工厂管理享元实例的创建和复用。实际开发中,当遇到 “大量相似对象导致内存紧张” 的场景时,享元模式是最优解之一。

记住:享元模式让细粒度对象的复用 “化繁为简”,让系统内存的使用 “精打细算”

2.7 外观模式(Facade Pattern)教程

一、什么是外观模式?

外观模式是 结构型模式中专注于 简化复杂系统接口的核心模式,其核心思想是:为一组复杂的子系统提供一个统一的高层接口,客户端通过这个接口与子系统交互,而无需关心子系统的内部细节,从而降低客户端与子系统的耦合度

简单说:“外观就像‘总开关’,比如家庭影院的遥控器,按一个‘观影模式’按钮,自动打开电视、音响、蓝光播放器并调整好设置,无需手动操作每个设备”

日常生活中,电脑的 “开机键” 是外观模式的典型体现:按下开机键,电脑自动完成主板通电、CPU 启动、内存加载、硬盘自检等一系列子系统操作,用户无需关心内部细节;手机的 “飞行模式” 也是如此,一键关闭蓝牙、Wi-Fi、蜂窝网络等多个子系统。

二、为什么需要外观模式?(作用)

当系统包含多个子系统(如家庭影院的电视、音响、播放器),且子系统之间存在复杂交互时,客户端直接操作子系统会导致:

  • 客户端需要了解所有子系统的接口和交互逻辑,学习成本高;
  • 客户端与多个子系统强耦合,子系统的任何修改(如接口变更)都会影响客户端;
  • 代码冗余,相同的子系统交互逻辑在多个客户端中重复编写。

外观模式的核心作用是:

  1. 简化接口:为复杂子系统提供统一入口,客户端只需调用一个接口,无需操作多个子系统;
  2. 降低耦合:客户端与子系统解耦,子系统的内部变化不影响客户端(只需保证外观接口不变);
  3. 隔离复杂性:隐藏子系统的内部细节,客户端无需了解子系统的实现逻辑;
  4. 统一复用:将子系统的交互逻辑集中在外观类中,避免客户端重复编写相同代码。

三、反例:直接操作子系统的问题

假设我们要实现一个 “家庭影院” 系统,包含电视(TV)、音响(SoundSystem)、蓝光播放器(BlueRayPlayer)三个子系统,观看电影需要依次执行:打开电视→打开音响→打开播放器→切换电视输入源→调整音量→播放电影。

不使用外观模式的实现:

// 1. 子系统1:电视
class TV {
    public void turnOn() { System.out.println("电视已打开"); }
    public void turnOff() { System.out.println("电视已关闭"); }
    public void setInput(String input) { System.out.println("电视输入源切换为:" + input); }
}

// 2. 子系统2:音响
class SoundSystem {
    public void turnOn() { System.out.println("音响已打开"); }
    public void turnOff() { System.out.println("音响已关闭"); }
    public void setVolume(int volume) { System.out.println("音响音量调整为:" + volume); }
}

// 3. 子系统3:蓝光播放器
class BlueRayPlayer {
    public void turnOn() { System.out.println("蓝光播放器已打开"); }
    public void turnOff() { System.out.println("蓝光播放器已关闭"); }
    public void play() { System.out.println("蓝光播放器开始播放电影"); }
}

// 客户端:直接操作所有子系统(问题核心)
public class Client {
    public static void main(String[] args) {
        // 1. 客户端需手动创建所有子系统对象
        TV tv = new TV();
        SoundSystem sound = new SoundSystem();
        BlueRayPlayer player = new BlueRayPlayer();

        // 2. 客户端需记住复杂的操作步骤和顺序
        tv.turnOn();
        sound.turnOn();
        player.turnOn();
        tv.setInput("HDMI"); // 必须切换到播放器的输入源
        sound.setVolume(20); // 必须调整音量
        player.play();

        // 问题:若要停止观影,还需手动关闭所有设备,步骤繁琐
        // 问题:若子系统变更(如新增投影仪),客户端代码需大幅修改
    }
}

问题分析:

  • 客户端负担重:客户端必须知道所有子系统的存在,掌握正确的操作顺序(如先开设备再切换输入源),一旦顺序错误(如先播放再开电视),系统无法正常工作;
  • 耦合度极高:客户端直接依赖TVSoundSystemBlueRayPlayer三个子系统,若任何一个子系统的方法名变更(如turnOn改为powerOn),客户端代码必须同步修改;
  • 代码复用性差:若有多个客户端(如手机 APP、遥控器)需要实现 “观影模式”,每个客户端都要重复编写相同的操作步骤,维护成本高;
  • 扩展困难:新增子系统(如投影仪、幕布)时,所有客户端都需修改代码以适配新设备,违反 “开闭原则”。

四、正例:用外观模式解决问题

核心改进:创建 “外观类”(如HomeTheaterFacade),封装子系统的所有交互逻辑,客户端只需调用外观类的简单接口(如watchMovie()),由外观类内部协调各个子系统

外观模式的实现:

// 1. 子系统(与反例相同,无需修改)
class TV {
    public void turnOn() { System.out.println("电视已打开"); }
    public void turnOff() { System.out.println("电视已关闭"); }
    public void setInput(String input) { System.out.println("电视输入源切换为:" + input); }
}

class SoundSystem {
    public void turnOn() { System.out.println("音响已打开"); }
    public void turnOff() { System.out.println("音响已关闭"); }
    public void setVolume(int volume) { System.out.println("音响音量调整为:" + volume); }
}

class BlueRayPlayer {
    public void turnOn() { System.out.println("蓝光播放器已打开"); }
    public void turnOff() { System.out.println("蓝光播放器已关闭"); }
    public void play() { System.out.println("蓝光播放器开始播放电影"); }
}

// 2. 外观类(Facade):封装子系统交互,提供统一接口
class HomeTheaterFacade {
    // 持有子系统的引用(外观类知道所有子系统)
    private TV tv;
    private SoundSystem soundSystem;
    private BlueRayPlayer blueRayPlayer;

    // 构造函数:初始化子系统(也可通过依赖注入传入)
    public HomeTheaterFacade(TV tv, SoundSystem soundSystem, BlueRayPlayer blueRayPlayer) {
        this.tv = tv;
        this.soundSystem = soundSystem;
        this.blueRayPlayer = blueRayPlayer;
    }

    // 外观接口1:观影模式(封装所有启动步骤)
    public void watchMovie() {
        System.out.println("=== 准备观影 ===");
        tv.turnOn();
        soundSystem.turnOn();
        blueRayPlayer.turnOn();
        tv.setInput("HDMI");
        soundSystem.setVolume(20);
        blueRayPlayer.play();
        System.out.println("=== 观影开始 ===");
    }

    // 外观接口2:关闭所有设备(封装所有关闭步骤)
    public void endMovie() {
        System.out.println("=== 结束观影 ===");
        blueRayPlayer.turnOff();
        soundSystem.turnOff();
        tv.turnOff();
        System.out.println("=== 所有设备已关闭 ===");
    }
}

// 3. 客户端:只与外观类交互,无需关心子系统
public class Client {
    public static void main(String[] args) {
        // 1. 创建子系统对象(可由工厂或容器注入)
        TV tv = new TV();
        SoundSystem sound = new SoundSystem();
        BlueRayPlayer player = new BlueRayPlayer();

        // 2. 创建外观类(将子系统交给外观管理)
        HomeTheaterFacade facade = new HomeTheaterFacade(tv, sound, player);

        // 3. 客户端只需调用外观接口,一步完成复杂操作
        facade.watchMovie(); 
        // 输出:
        // === 准备观影 ===
        // 电视已打开
        // 音响已打开
        // 蓝光播放器已打开
        // 电视输入源切换为:HDMI
        // 音响音量调整为:20
        // 蓝光播放器开始播放电影
        // === 观影开始 ===

        // 结束观影
        facade.endMovie();
        // 输出:
        // === 结束观影 ===
        // 蓝光播放器已关闭
        // 音响已关闭
        // 电视已关闭
        // === 所有设备已关闭 ===
    }
}

改进效果:

  1. 客户端操作简化:客户端无需记住复杂的子系统操作步骤,只需调用watchMovie()endMovie()两个接口,学习成本和使用难度大幅降低;
  2. 解耦客户端与子系统:客户端只依赖HomeTheaterFacade,不直接访问TVSoundSystem等子系统,即使子系统的方法名或逻辑变更(如turnOn改为powerOn),只需修改外观类,客户端代码无需变动;
  3. 逻辑集中复用:子系统的交互逻辑(如先开电视再开音响)集中在外观类中,多个客户端(如手机 APP、遥控器)可共用同一套逻辑,避免重复代码;
  4. 易于扩展:新增子系统(如投影仪Projector)时,只需修改外观类(在watchMovie中添加投影仪的启动逻辑),客户端无需任何修改,符合 “开闭原则”;
  5. 隔离复杂性:客户端完全不知道子系统的内部实现(如电视如何切换输入源),只需关注 “观影” 这个最终目标,降低了系统的认知成本。

五、外观模式的核心结构

外观模式通过 “统一接口封装子系统” 实现简化,包含 3 个核心角色:

  1. 外观角色(Facade)
    • 是客户端交互的唯一入口(如HomeTheaterFacade);
    • 持有所有子系统的引用,知道每个子系统的职责;
    • 提供高层接口(如watchMovie()),内部协调多个子系统完成复杂操作,隐藏子系统的交互细节。
  2. 子系统角色(Subsystem)
    • 是系统中完成具体功能的模块(如TVSoundSystem);
    • 子系统之间可能存在交互,但它们不知道外观类的存在,也不依赖外观类;
    • 若没有外观类,子系统可直接被客户端调用(但会导致耦合问题)。
  3. 客户端(Client)
    • 通过外观角色的接口与系统交互,不直接访问子系统;
    • 只关心外观提供的高层功能(如 “观影”),不关心子系统的具体实现。

六、外观模式的工作原理

外观模式的核心是 “封装与委托”:

  1. 封装:外观类将子系统的复杂交互逻辑(如 “开机顺序”“参数设置”)封装在自身方法中,客户端无需感知;
  2. 委托:外观类的方法被调用时,内部会按顺序调用各个子系统的相关方法(如watchMovie()依次调用tv.turnOn()soundSystem.turnOn()等),完成委托执行。

这种机制保证了:

  • 客户端与子系统的解耦(客户端→外观→子系统,而非客户端→子系统);
  • 子系统的独立性(子系统可单独修改或复用,只要外观类适配即可);
  • 系统的易用性(复杂操作简化为一个接口调用)。

七、总结

外观模式的核心是 “封装复杂,提供简单”,通过引入外观类作为统一入口,将客户端与复杂子系统隔离开来,既简化了客户端的使用,又降低了系统的耦合度。

它的关键是合理设计外观类的接口(只暴露必要的高层功能),避免外观类职责过重(可通过多个外观类拆分不同功能模块)。实际开发中,当系统变得复杂难以使用时,外观模式往往是 “化繁为简” 的最佳选择。

记住:外观模式是复杂系统的 “简化器”,让客户端与系统的交互 “一步到位”

posted @ 2025-10-23 14:35  碧水云天4  阅读(3)  评论(0)    收藏  举报