【NowCoder】Design Pattern

设计模式

“设计模式一般用来解决同一问题的不同表相” 这句话精妙地概括了设计模式的核心价值和应用逻辑。下面我来为你解释这句话的含义,并探讨其背后的思想。

🧠 核心思想解读

这句话的关键在于理解 “同一问题”“不同表相”

  • “同一问题”:指的是在软件设计中反复出现的那些核心的、本质性的设计挑战。例如:
    • 如何灵活、高效地创建对象(创建型模式要解决的问题)。
    • 如何组合类或对象以形成更大、更合理的结构(结构型模式要解决的问题)。
    • 如何高效地分配职责和行为,使对象能够更好地协作(行为型模式要解决的问题)。
  • “不同表相”:指的是这些核心问题在各种各样的具体业务场景中所表现出来的具体形态。例如:
    • 同样是“创建对象”的问题,在快餐店点餐系统里是创建不同套餐,在家具店是创建不同风格的一套家具。
    • 同样是“协调对象行为”的问题,在客服系统是多级投诉处理,在机场是协调航班起降。

因此,设计模式的价值就在于:它提炼出了一套针对这些“同一问题”的、经过验证的解决方案“模板”。当我们在“不同表相”的具体场景中遇到这些问题的本质时,就可以应用相应的设计模式,而不需要每次都从头开始设计。

📊 如何理解与应用

为了更直观地理解,我们来看一个表格,它展示了同一个核心问题(创建对象) 如何在不同场景(表相)下,通过不同的设计模式来解决:

核心问题 (同一问题)场景 (不同表相)适用的设计模式模式如何解决该场景下的问题
如何创建对象需要确保一个类只有一个实例(如打印机服务、配置管理器)单例模式 (Singleton)私有化构造器,提供全局访问点 getInstance(),确保全局唯一实例。
需要创建多种不同类型的对象,但不想依赖具体类(如点餐系统、日志记录器)工厂方法模式 (Factory Method)定义创建对象的接口,让子类决定实例化哪个类。将对象的创建与使用分离。
需要创建一整族相关或依赖的对象(如搭配不同风格的家具)抽象工厂模式 (Abstract Factory)提供一个接口来创建一系列相关或依赖的对象,而不指定它们的具体类。
需要分步骤构建一个复杂对象(如组装电脑)建造者模式 (Builder)将复杂对象的构建过程分解为多个步骤,通过相同的构建过程可以创建不同的表示。
需要高效地创建成本较高的对象实例(如复制复杂对象、游戏中的大量相似对象)原型模式 (Prototype) / 享元模式 (Flyweight)原型模式:通过克隆现有对象来创建新对象,避免重复初始化。享元模式:通过共享相似对象的部分状态,来支持大量细粒度对象。

从这个表格可以看出,“创建对象” 这个核心问题,在不同的业务表相下,衍生出了多种不同的解决方案(模式)。每种模式都提供了解决该特定表相下痛点的最佳实践。

💡 模式选择的关键:识别“同一问题”

选择合适设计模式的关键,在于拨开“不同表相”的迷雾,洞察背后“同一问题”的本质

  1. 不要只看表面:不要因为业务场景是“点餐”就生搬硬套“工厂模式”,而是要看当前代码面临的核心痛点是否是“需要隔离对象的创建与使用,避免对具体类的依赖”。
  2. 理解模式意图:深入学习每个设计模式要解决的本质问题(Intent)。例如,适配器模式解决接口不兼容的问题,观察者模式解决一个对象状态改变需要通知其他对象的问题。
  3. 权衡与选择:通常,同一个问题可能有多个模式看似都可选。这就需要权衡它们的后果(Consequences)。例如,为了扩展对象功能,是选择装饰器模式(动态添加职责)还是继承(静态扩展)?装饰器模式更灵活,但会引入更多小对象。

⚠️ 注意事项

  1. 切忌过度设计:不是所有情况都需要使用设计模式。对于简单、稳定、不会变化的逻辑,直接实现可能是更好的选择。设计模式是用来应对变化的复杂性,如果本身没有变化,引入模式反而会增加不必要的复杂度。
  2. 理解优先于套用:在理解模式背后的思想和原则(如开闭原则、依赖倒置原则等)之前,盲目套用模式名称和代码结构是危险的。
  3. 模式是手段,不是目标:应用的终极目标是写出高内聚、低耦合、可复用、可维护的代码。设计模式只是达成这些目标的一种优秀手段。

💎 总结

“设计模式一般用来解决同一问题的不同表相” 这句话告诉我们:

  • 设计模式是对通用设计问题的抽象和总结,它超越了具体的业务场景。
  • 学习设计模式,重要的是理解每种模式所要解决的核心问题(Intent),而不仅仅是记住它的结构或代码。
  • 在实际应用中,需要透过具体业务的“表相”,识别出深层次的“同一问题”,然后选择合适的模式来解决它。

希望以上的解释能帮助你更好地理解这句充满智慧的话。

动态代理 vs. 装饰器

动态代理模式和装饰器模式都是结构型设计模式,它们都通过包装对象来实现功能的扩展或控制,但它们的核心目的、实现方式和应用场景有显著区别。下面我将从多个维度为你详细对比这两种模式,并提供一个总结表格帮助你快速把握它们的核心差异。

维度动态代理模式 (Dynamic Proxy)装饰器模式 (Decorator)
核心目的控制对对象的访问 (如权限、延迟加载、缓存)动态地为对象添加新的功能或职责 (如日志、加密)
实现方式通常在运行时动态生成代理类,隐藏具体代理逻辑通过实现相同接口或继承,持有被装饰对象引用,可多层嵌套
对象关系代理与目标对象关系通常较固定,一一对应装饰器与被装饰对象关系动态,可递归组合多层功能
生命周期管理可能管理目标对象的创建和销毁 (如延迟加载)不管理被装饰对象的生命周期,仅扩展功能
设计重点访问控制、间接调用、增强非功能性需求功能组合、透明扩展、增强对象本身行为
典型应用场景权限控制、延迟加载、远程调用、缓存、AOP(日志、事务)IO流处理、UI组件增强、动态添加日志或加密等

🎯 核心目的与本质

  • 动态代理模式的核心在于控制对原始对象的访问。它像一个"门卫"或"中介",决定客户端是否可以访问原始对象、何时访问,以及在访问前后执行哪些额外逻辑(如权限检查、延迟初始化、缓存结果等)。动态代理通常不改变原始对象的核心行为
  • 装饰器模式的核心在于动态地为原始对象添加新的功能或职责。它像一个"包装纸",可以一层一层地给原始对象增添新的能力,每一层都可能增加新的行为。装饰器模式关注的是增强对象本身的功能

⚙️ 实现方式与关系

  • 动态代理模式通常在运行时动态生成代理类(如利用 Java 的 ProxyInvocationHandler)。代理类与目标对象实现相同的接口。客户端通常感知不到代理背后的具体目标对象,因为代理对象本身"代表"了目标对象。
  • 装饰器模式则需要显式地创建装饰器类,这些装饰器类实现与被装饰对象相同的接口继承相同的父类,并在内部持有被装饰对象的引用。你可以通过多层嵌套装饰器来组合多种功能。

🚀 典型应用场景

  • 动态代理模式常用于:
    • AOP(面向切面编程):如日志记录、性能监控、事务管理等。
    • 虚拟代理:延迟大开销对象的创建,如图片懒加载。
    • 保护代理:控制访问权限,进行安全审查。
    • 远程代理:隐藏远程方法调用的复杂性,如 RPC。
  • 装饰器模式常用于:
    • Java I/O 流:如 BufferedInputStream包装 FileInputStream添加缓冲功能。
    • 为UI组件动态添加功能:如为文本框添加滚动条、边框等。
    • 动态透明地扩展对象功能:如为业务对象添加日志、加密等功能,且这些功能可以灵活组合。

💡 如何选择

  • 当你需要控制对某个对象的访问(例如,检查权限、延迟加载资源、记录日志、管理事务或提供远程接口)时,应优先考虑动态代理模式
  • 当你需要动态、透明地为一个对象添加一个或多个功能,并且希望这些功能可以灵活地组合使用时,应优先考虑装饰器模式

⚠️ 需要注意的点

  1. 动态代理的局限性:在 Java 中,JDK 动态代理要求目标对象必须实现接口。对于未实现接口的类,可以考虑使用 CGLIB 等字节码增强库。
  2. 装饰器的顺序:多层装饰器包装时,功能的执行顺序可能会因包装顺序不同而产生不同效果。
  3. 性能考量:动态代理通常涉及反射调用,可能带来轻微的性能开销,但在大多数场景下可以忽略不计。装饰器模式由于会增加对象层次,也可能带来轻微的开销。

希望这些详细的对比能帮助你更好地理解动态代理模式和装饰器模式。

动态代理模式

动态代理模式是一种在运行时动态创建代理对象的技术,它允许在不修改原始类代码的情况下,通过代理对象拦截并增强目标对象的方法调用。这种模式主要用于实现横切关注点(如日志、事务、安全等)与业务逻辑的解耦。以下是动态代理模式的详细介绍:

1. 基本概念与核心组件

动态代理模式是一种结构型设计模式,它通过在运行时动态生成代理对象来控制对目标对象的访问。代理对象在客户端和目标对象之间充当中介,用于在方法调用前后添加额外的处理逻辑(如日志记录、事务管理等)。

Java中的动态代理主要依赖于两个核心组件:

  • Proxy:位于 java.lang.reflect包中,用于动态创建代理实例。其 newProxyInstance()方法是创建代理对象的关键,它接收三个参数:类加载器(ClassLoader)、目标对象实现的接口数组(Class[])以及调用处理器(InvocationHandler)实例。
  • InvocationHandler接口:这是实现动态代理必须实现的接口,它包含一个 invoke()方法。当代理对象的方法被调用时,invoke()方法会被自动执行。开发者可以在此方法中添加自定义逻辑来拦截和增强原始方法调用。

2. 实现原理与步骤

实现动态代理通常遵循以下步骤:

  1. 定义业务接口:创建一个接口,声明需要被代理的方法。
  2. 实现真实主题(目标类):创建一个类来实现上述接口,该类包含实际的业务逻辑。
  3. 创建调用处理器(InvocationHandler):实现 InvocationHandler接口,并在其 invoke()方法中编写增强逻辑(如方法调用前后的日志记录、权限检查等)。该方法会通过反射调用目标对象的原始方法。
  4. 生成代理对象:使用 Proxy.newProxyInstance()方法,传入目标类的类加载器、实现的接口以及调用处理器实例,动态创建代理对象。

例如,为一个简单的服务接口创建代理:

// 1. 定义接口
interface Service {
    void execute();
}

// 2. 实现真实主题
class RealService implements Service {
    public void execute() {
        // 业务逻辑
    }
}

// 3. 创建调用处理器
class LogHandler implements InvocationHandler {
    private final Object target;
    public LogHandler(Object target) { this.target = target; }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before method: " + method.getName());
        Object result = method.invoke(target, args); // 调用目标方法
        System.out.println("After method: " + method.getName());
        return result;
    }
}

// 4. 生成代理对象
Service realService = new RealService();
InvocationHandler handler = new LogHandler(realService);
Service proxy = (Service) Proxy.newProxyInstance(
    Service.class.getClassLoader(),
    new Class<?>[]{Service.class},
    handler
);
// 使用代理对象
proxy.execute(); // 输出前置/后置日志并执行业务逻辑

3. 动态代理的实现方式

Java生态中主要有两种动态代理实现方式:

特性JDK动态代理CGLIB动态代理
实现基础基于接口实现基于继承实现
要求目标类必须实现至少一个接口目标类不能是final的,方法不能是final或private的
性能特点反射调用,有一定性能开销通过字节码技术生成子类,通常比JDK动态代理快一些
应用场景适用于接口驱动的场景,如Spring AOP(默认策略)适用于代理没有实现接口的类

JDK动态代理是Java标准库的一部分,它要求目标对象必须实现一个或多个接口。代理对象会实现这些相同的接口,并将方法调用委托给 InvocationHandler

CGLIB动态代理是一个第三方库(如被Spring AOP使用),它通过生成目标类的子类来创建代理。这使得它能够代理那些没有实现接口的类。CGLIB通过方法重写来拦截方法调用,因此无法代理final类或final/private方法。

4. 应用场景

动态代理在许多框架和实际开发中有广泛应用:

  • AOP(面向切面编程):动态代理是实现AOP的基石,用于将横切关注点(如日志记录、事务管理、性能监控)模块化,并与业务逻辑分离。Spring框架的AOP模块就大量使用了动态代理。
  • 远程方法调用(RPC):在分布式系统中,动态代理可以隐藏网络通信的复杂性,使客户端像调用本地方法一样调用远程服务(如Dubbo、gRPC等框架的实现)。
  • 声明式事务管理:Spring框架使用动态代理来实现 @Transactional注解,在方法调用前后自动管理事务的开启、提交或回滚。
  • 延迟加载:Hibernate等ORM框架使用动态代理实现延迟加载,只有当实体类的属性被实际访问时才从数据库加载数据。
  • 安全控制与权限检查:在方法调用前,通过代理进行权限验证,确保调用者拥有足够的权限。
  • 缓存:代理可以拦截方法调用,先检查缓存中是否存在结果,存在则直接返回,不存在则调用目标方法并将结果存入缓存。

5. 优缺点分析

优点:

  • 解耦与增强非侵入性:无需修改目标对象的代码,即可为其添加额外的功能,符合开闭原则。
  • 灵活性与可复用性:代理逻辑(如日志、事务)可以复用于多个目标类,减少了代码重复。
  • 简化系统架构:通过AOP等思想,使业务逻辑与通用横切关注点分离,提高了代码的可读性和可维护性。

缺点与局限性:

  • 性能开销:由于动态代理通常涉及反射调用和字节码生成,其性能通常略低于直接的方法调用(但现代JVM和框架已通过各种优化手段减少了这种开销)。
  • JDK动态代理的接口依赖:JDK原生动态代理只能基于接口创建代理,不能代理未实现任何接口的类。
  • CGLIB的限制:CGLIB无法代理final类或final/private方法,因为这些方法不能被重写。
  • 调试复杂性:由于代理机制引入了额外的间接层,在调试时可能会增加一定的复杂性。

6. 总结

动态代理是一种强大的编程技术,是许多高级特性和框架(如Spring AOP、Hibernate、RPC框架等)的基础。它通过运行时动态生成代理对象,优雅地实现了功能的增强和逻辑的解耦。

掌握动态代理模式,理解其原理、实现方式以及适用场景,对于Java开发者设计和构建更灵活、可维护且扩展性良好的系统至关重要。

桥接模式

桥接模式是一种非常实用的结构型设计模式,它通过“组合优于继承”的思想,巧妙地将抽象部分与它的实现部分分离,使它们可以独立变化,从而有效解决多维度变化带来的类爆炸问题,提升系统的灵活性和可扩展性。

为了让你先快速把握其核心价值,下面这个表格对比了使用传统继承和使用桥接模式的不同。

对比维度传统的多层继承方式使用桥接模式
核心关系继承 (is-a)组合 (has-a)
耦合度高耦合(父类与子类紧密绑定)低耦合(抽象与实现完全分离)
类的数量极易发生 “类爆炸” (产品 × 品牌 × 功能…)类数量线性增长,而非乘数增长
扩展性差。增加一个新维度(如新品牌)需修改所有相关类或创建大量子类。。抽象和实现维度可独立扩展,符合开闭原则
灵活性静态,关系在编译时确定动态,可在运行时切换实现
适用场景变化维度较少、关系稳定的系统多个维度独立变化的复杂系统

🧠 桥接模式的设计思想与结构

桥接模式的核心是解耦。它通过一个组合关系(桥),将“什么”(抽象)和“怎么做”(实现)连接起来,而不是让“什么”直接继承“怎么做”。

🔧 模式结构

桥接模式通常包含以下四个关键角色:

  1. Implementor(实现器接口):定义实现部分的接口。这些接口通常是与抽象部分操作相关的基本操作。
  2. ConcreteImplementor(具体实现器):实现 Implementor接口,给出具体操作的定义。
  3. Abstraction(抽象类):定义抽象部分的接口,并维护一个指向 Implementor类型对象的引用(即那座“桥”)。
  4. RefinedAbstraction(优化的抽象类):扩展 Abstraction定义的接口,通常它不再直接操作实现部分,而是通过 Abstraction定义的高级接口来使用 Implementor

它们之间的关系可以通过下面的 UML 类图来直观展示:

classDiagram
    class Abstraction {
        +implementor: Implementor
        +operation()
    }
    class RefinedAbstraction {
        +operation()
    }
    class Implementor {
        <<interface>>
        +operationImpl()
    }
    class ConcreteImplementorA {
        +operationImpl()
    }
    class ConcreteImplementorB {
        +operationImpl()
    }

    Abstraction <|-- RefinedAbstraction : 继承
    Abstraction o-- Implementor : 组合/桥接
    Implementor <|.. ConcreteImplementorA : 实现
    Implementor <|.. ConcreteImplementorB : 实现

⚖️ 优缺点

✅ 优点

  1. 分离抽象与实现:这是最核心的优点,使得抽象和实现可以独立地扩展和修改,互不影响。
  2. 提高可扩展性:可以独立地对 AbstractionImplementor层次结构进行扩展。
  3. 避免继承爆炸:极大地减少了为处理多维度变化而产生的子类数量。
  4. 符合设计原则:符合“开闭原则”和“合成复用原则”(优先使用组合而非继承)。

❌ 缺点

  1. 增加系统复杂度:引入了额外的类和接口,要求开发者针对抽象进行设计,理解和设计的难度有所增加。
  2. 需要正确识别维度:要求开发者能正确识别出系统中两个独立变化的维度,否则会适得其反。

🛠️ 应用场景

桥接模式在以下情况中非常有用:

  • 当一个类存在多个独立变化的维度,且这些维度都需要进行扩展时。
  • 不希望使用继承或因为多层继承导致系统类的个数急剧增加时。
  • 需要在抽象和实现之间增加更多的灵活性,例如需要在运行时动态切换实现。
  • 常见的具体应用
    • 跨平台应用开发:抽象部分定义通用功能,不同平台提供具体实现。
    • GUI 框架:将窗口抽象与不同操作系统的具体实现分离。
    • 数据库驱动与连接池:JDBC 就是一个经典的桥接模式应用,Connection是抽象,各类数据库的驱动是具体实现。
    • 支持多主题切换的应用程序
    • 消息发送系统:消息类型(普通、加急)和发送方式(短信、邮件、站内信)是两个独立的维度。

📟 代码示例

以下是一个简单的 Java 代码示例,模拟了手机品牌与手机软件两个维度:

// 1. 实现部分接口:手机软件
interface App {
    void open();
}

// 2. 具体实现类:微信、YouTube
class WeChat implements App {
    @Override
    public void open() {
        System.out.println("打开微信");
    }
}

class YouTube implements App {
    @Override
    public void open() {
        System.out.println("打开YouTube");
    }
}

// 3. 抽象类:手机品牌
abstract class Phone {
    protected App app; // 桥接的关键:持有实现部分的引用

    public Phone(App app) {
        this.app = app;
    }

    abstract void run();
}

// 4. 优化的抽象类:具体品牌
class Xiaomi extends Phone {
    public Xiaomi(App app) {
        super(app);
    }

    @Override
    void run() {
        System.out.print("小米手机:");
        app.open(); // 委托给实现部分
    }
}

class Huawei extends Phone {
    public Huawei(App app) {
        super(app);
    }

    @Override
    void run() {
        System.out.print("华为手机:");
        app.open();
    }
}

// 5. 客户端使用
public class Client {
    public static void main(String[] args) {
        // 组合出“小米手机运行微信”
        Phone xiaomiWithWeChat = new Xiaomi(new WeChat());
        xiaomiWithWeChat.run(); // 输出:小米手机:打开微信

        // 组合出“华为手机运行YouTube”
        Phone huaweiWithYouTube = new Huawei(new YouTube());
        huaweiWithYouTube.run(); // 输出:华为手机:打开YouTube

        // 轻松扩展:新增一个抖音App,无需修改任何品牌类
        // class Douyin implements App { ... }
        // Phone xiaomiWithDouyin = new Xiaomi(new Douyin());
    }
}

💎 总结

桥接模式通过解耦抽象和实现,并用组合关系代替继承关系,为我们提供了一种处理多维度变化的优雅方案。虽然它可能会在初期增加一些系统设计的复杂度,但在需求可能变化、需要更多扩展性的场景下,它能有效避免代码僵化,让系统更具灵活性。

希望以上解释能帮助你更好地理解桥接模式。

状态模式

状态模式是一种非常实用的行为型设计模式,它允许一个对象在其内部状态改变时改变它的行为,使得这个对象看起来似乎修改了它的类。其核心思想是将状态抽象为独立的对象,并将与特定状态相关的行为都封装到对应的类中,从而消除了庞大的条件判断语句,使代码更清晰、更易于维护和扩展。

为了让你快速建立整体概念,下方图表展示了状态模式中三个核心角色(环境类、抽象状态、具体状态)如何协作,以及状态如何流转。

flowchart TD
    A[客户端 Client] --> B[环境类 Context<br>维护一个对当前状态对象的引用]
    
    B --> C[抽象状态 State<br>定义状态接口]
    
    C --> D[具体状态 ConcreteStateA<br>实现A状态下的行为<br>并知悉下一个状态]
    C --> E[具体状态 ConcreteStateB<br>实现B状态下的行为<br>并知悉下一个状态]
    C --> F[具体状态 ConcreteStateN<br>...]
    
    B -- 委托请求 --> D
    B -- 委托请求 --> E
    B -- 委托请求 --> F
    
    D --> H[在适当条件下<br>调用Context.setState<br>将状态切换至B或其他状态]
    E --> I[在适当条件下<br>调用Context.setState<br>将状态切换至A或其他状态]

从图表中可以看出,环境类(Context) 是状态模式的“大脑”,它持有一个状态对象的引用,并将所有状态相关的请求委托给当前状态对象处理。而每个具体状态(ConcreteState) 类则不仅知道自身状态下该做什么,还知道接下来应该切换到什么状态。

下面我们来深入看看状态模式的各个组成部分。

🔧 状态模式的核心角色

状态模式通常包含以下三个核心角色,它们共同协作,实现了对象行为的动态变化:

  1. 环境类 (Context)
    • 它定义了客户端需要的接口。
    • 维护一个当前状态对象(State)的实例,这个实例定义了当前的状态。
    • 负责在接收到客户端请求时,将请求委托给当前的状态对象来处理
  2. 抽象状态 (State)
    • 这是一个接口或抽象类。
    • 它定义了所有具体状态类必须实现的方法,这些方法通常对应着对象在该状态下可能的行为。
  3. 具体状态 (Concrete State)
    • 这些类实现了抽象状态接口。
    • 每个具体状态类封装了在特定状态下的行为实现
    • 在处理请求的过程中,具体状态对象可能会根据需要改变环境对象当前的状态(即切换到另一个具体状态对象)。

⚖️ 状态模式的优缺点

✅ 优点

  • 清晰的结构与单一职责:将不同状态的行为分散到不同的状态类中,避免了庞大的条件判断语句(如 if-elseswitch-case),代码更清晰,每个状态类职责明确。
  • 良好的扩展性和可维护性:增加新的状态非常简单,只需创建新的具体状态类即可,通常无需修改现有状态类或上下文,符合“开闭原则”。
  • 状态转换逻辑显式化:状态之间的转换逻辑可以分布在具体状态类中,使得转换的目的和路径更加明确。

❌ 缺点

  • 可能增加类的数量:如果状态非常多,会导致系统中有大量的具体状态类,在一定程度上增加了系统的复杂性。
  • 状态转换分散:状态转换的逻辑分布在各个具体状态类中,而不是集中在一个地方,有时可能使状态转换的逻辑变得分散而不易整体把握。

🛠️ 状态模式的应用场景

状态模式在以下情况下特别有用:

  • 对象的行为严重依赖于它的状态,并且必须在运行时根据状态改变其行为。
  • 一个操作中包含庞大的分支结构,并且这些分支取决于对象的状态。使用状态模式可以消除这些条件分支。
  • 常见的具体应用
    • 订单/工作流系统:如电商订单的“待支付”、“已支付”、“已发货”、“已完成”等状态流转。
    • 游戏开发:管理游戏角色的不同状态(如站立、奔跑、跳跃、攻击),每种状态对应不同的行为和动画。
    • 网络连接/线程管理:管理TCP连接的状态(如建立连接、监听、已关闭)或线程的生命周期状态(新建、就绪、运行、阻塞、死亡)。
    • 用户界面交互:管理UI组件在不同交互状态下的显示和行为(如按钮的禁用、悬停、按下状态)。

📟 代码示例:订单状态流转

以下是一个用 Java 实现的简化版订单状态管理示例,展示了状态模式如何优雅地处理状态转换。

// 1. 抽象状态接口
public interface OrderState {
    void pay(OrderContext context);
    void cancel(OrderContext context);
    // 可根据需要添加其他操作,如 ship(), confirm() 等
}

// 2. 具体状态类:待支付状态
public class UnpaidState implements OrderState {
    @Override
    public void pay(OrderContext context) {
        System.out.println("支付成功,订单状态变为已支付。");
        context.setState(new PaidState()); // 状态转换:待支付 -> 已支付
    }

    @Override
    public void cancel(OrderContext context) {
        System.out.println("取消订单成功,订单已关闭。");
        context.setState(new CancelledState()); // 状态转换:待支付 -> 已取消
    }
}

// 3. 具体状态类:已支付状态
public class PaidState implements OrderState {
    @Override
    public void pay(OrderContext context) {
        System.out.println("订单已支付,请勿重复支付。");
        // 不再进行状态转换
    }

    @Override
    public void cancel(OrderContext context) {
        System.out.println("申请取消已支付的订单,进入退款流程。");
        context.setState(new RefundingState()); // 状态转换:已支付 -> 退款中
    }
}

// 4. 环境类:订单上下文
public class OrderContext {
    private OrderState currentState;

    public OrderContext() {
        this.currentState = new UnpaidState(); // 初始状态为“待支付”
    }

    public void setState(OrderState state) {
        this.currentState = state;
    }

    // 将操作委托给当前状态对象
    public void pay() {
        currentState.pay(this);
    }

    public void cancel() {
        currentState.cancel(this);
    }
}

// 5. 客户端使用
public class Client {
    public static void main(String[] args) {
        OrderContext order = new OrderContext();

        order.pay();    // 输出: 支付成功,订单状态变为已支付。
        order.cancel(); // 输出: 申请取消已支付的订单,进入退款流程。
        // 此时 order 的当前状态是 RefundingState
    }
}

在这个简化的例子中,RefundingState(退款中)和 CancelledState(已取消)等具体状态类需要你根据上述模式自行补充完整。

💡 状态转换的触发方式

状态转换通常由三种方式触发,具体取决于设计:

  1. 由具体状态类触发(推荐):如上例所示,每个具体状态类在处理完自己的行为后,负责指定并切换到下一个状态。这种方式将状态转换逻辑分布到各个状态类中,符合单一职责原则。
  2. 由环境类触发:环境类持有所有状态对象的引用,并根据一定的条件(可能是某些属性值的变化)来决定切换到哪个状态
  3. 由客户端触发(不推荐):客户端直接设置环境对象的状态。这种方式破坏了封装性,因为客户端需要知道具体状态类,并使状态转换逻辑分散在客户端代码中。

🔄 状态模式与策略模式的区别

状态模式和策略模式在结构上非常相似,但它们的目的不同:

特性状态模式 (State Pattern)策略模式 (Strategy Pattern)
目的管理状态流转,行为随内部状态自动变化。灵活替换算法,客户端主动选择或更换策略。
状态/策略知晓性具体状态类通常知道下一个状态是什么策略之间相互独立,不知道其他策略的存在。
转换触发状态转换可由内部状态自动触发(如在具体状态类中切换)。策略通常由客户端在外部显式指定和更换
核心关注点封装与状态相关的行为状态转换逻辑封装可互换的算法,使算法独立于客户端而变化。

简单理解:状态模式是 “状态”驱动“行为”,而策略模式是 “策略”(算法)本身就是“行为”

💎 总结

状态模式通过将状态抽象为对象,并将行为封装到对应的状态类中,巧妙地解决了对象行为依赖于其内部状态的问题。它消除了繁琐的条件判断语句,使代码结构更清晰,提高了系统的可扩展性和可维护性。

当你的对象拥有多个状态,且行为随着状态改变而改变,特别是状态转换逻辑比较复杂时,状态模式是一个非常值得考虑的解决方案。

行为型模式

行为型设计模式主要关注对象之间的职责分配和通信方式,它们帮助对象之间更好地协作,使系统更灵活、可维护。下面我将为你梳理这些模式的核心思想、常见类型及应用场景。

🧠 行为型设计模式概览

行为型设计模式的核心在于管理对象之间的交互、通信和职责分配,而不是聚焦于对象的创建或结构组合。它们通过定义清晰的通信模式,使系统在应对变化时更加灵活。

根据对象间交互方式的不同,行为型模式可大致分为两类:

  • 行为类模式:主要使用继承机制在类间分配行为。
  • 行为对象模式:主要使用对象组合(如一个对象持有其他对象的引用)来进行行为分配。

多数常见的行为型模式属于“行为对象模式”,它们更强调运行时的灵活性。

📊 主要行为型模式速览表

下表汇总了11种经典行为型设计模式的核心思想与典型场景,帮你快速建立整体认知:

模式名称核心思想典型应用场景
模板方法模式定义算法骨架,将步骤实现延迟到子类,避免重复代码多个类有相似行为逻辑;需要子类实现特定步骤
策略模式封装一系列算法,使它们能相互替换,让算法独立于客户端变化需动态选择算法;避免多重条件判断(如支付方式、排序策略)
观察者模式定义一种一对多的依赖关系,主题状态变化时自动通知所有观察者一个对象变化需通知其他多个对象;事件驱动系统(如消息订阅)
状态模式允许对象在内部状态改变时改变行为,将状态封装为独立类对象行为依赖于其状态;需通过状态控制行为(如订单状态流转)
职责链模式为请求创建接收链,多个对象都有机会处理请求,避免发送者与接收者耦合多个对象可处理同一请求;需动态指定处理者(如审批流程、过滤器链)
命令模式将请求封装为对象,从而支持参数化、排队、记录日志及撤销操作需支持操作撤销/重做;需将请求排队或记录日志
解释器模式定义一种语言的文法,并提供一个解释器来解释句子需要解释特定规则或表达式(如正则表达式、简单语法解析)
迭代器模式提供一种方法顺序访问聚合对象元素,而不暴露其内部表示需要遍历聚合对象;需提供多种遍历方式
中介者模式用一个中介对象封装一系列对象的交互,使它们不需要显式相互引用,降低耦合对象间存在复杂引用关系;需统一管理网状资源(如聊天室、GUI组件交互)
备忘录模式在不破坏封装前提下,捕获并外部化对象的内部状态,以便日后恢复需要保存和恢复对象状态;需提供撤销操作(如文本编辑器撤销)
访问者模式将作用于某数据结构中各元素的操作封装起来,允许在不改变数据结构前提下添加新操作需对对象结构中的元素执行许多不相关操作;避免这些操作“污染”元素类

💡 几种重要模式的深入理解

🔗 1. 职责链模式 (Chain of Responsibility)

此模式将多个处理请求的对象连成一条链,请求沿链传递直至被处理。发送者与接收者解耦是其主要优点,每个处理对象只需关注自己后续的处理者,无需知道整条链的结构。它常用于审批流程、异常处理链或过滤器链等场景。

📋 2. 命令模式 (Command)

此模式将请求封装为对象,使你可用不同请求参数化其他对象,并支持可撤销操作、请求排队和日志记录。通过引入Command接口、ConcreteCommandInvokerReceiver角色,它解耦了请求发送者和执行者。

🔄 3. 状态模式 (State)

此模式允许对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。它将特定状态相关的行为局部化,并且将不同状态的行为分割开来,通过定义新的子类可以很容易地增加新的状态和转换。状态模式在订单状态流转、游戏角色状态机等场景中非常实用。

📢 4. 观察者模式 (Observer)

此模式定义了对象间的一种一对多的依赖关系,当一个对象(主题)的状态发生改变时,所有依赖于它的对象(观察者)都得到通知并被自动更新。这种发布-订阅机制实现了主题与观察者之间的解耦,但需注意管理观察者的注册与注销,以防内存泄漏。

🧠 5. 策略模式 (Strategy)

此模式定义了一系列算法,并将每个算法封装起来,使它们可以互相替换。策略模式让算法独立于使用它的客户端而变化。通过定义策略接口、具体策略类和一个持有策略引用的上下文类,它避免了大量的条件判断语句,提高了系统的灵活性和可扩展性。

⚖️ 行为型模式的优缺点

✅ 优点:

  • 解耦对象:有效降低了对象之间的耦合度,提高了系统的灵活性。
  • 职责清晰:明确了对象间的职责分配,每个类或对象专注于特定任务。
  • 易于扩展:许多模式都支持开闭原则,易于增加新的行为或组件。

❌ 缺点:

  • 复杂度增加:引入新模式可能会增加类和对象的数量,使系统结构更复杂。
  • 性能考量:某些模式(如中介者模式)若设计不当,可能会导致性能问题。
  • 理解成本:需要开发者理解模式意图和适用场景,滥用可能适得其反。

💎 如何选择合适的行为型模式

选择哪种行为型模式,取决于你面临的具体问题:

  • 需要动态选择多种算法?→ 考虑策略模式
  • 对象行为随状态改变而改变?→ 状态模式很合适。
  • 需要处理一个可能由多个对象处理的请求?→ 职责链模式能派上用场。
  • 想要实现撤销/重做功能将操作排队?→ 看看命令模式
  • 存在一对多的依赖关系,一对象变更需通知多方?→ 观察者模式是经典选择。

🌟 总结

行为型设计模式通过优化对象间的交互和职责分配,为我们提供了解决复杂协作问题的优雅方案。它们解耦了对象之间相互通信和调用的方式,使系统更易于维护和扩展。

理解这些模式的意图适用场景,比死记硬背其结构更重要。在实际开发中,应根据具体需求灵活选用甚至组合使用多种模式。

希望以上介绍能帮助你更好地理解行为型设计模式。

Licensed under CC BY-NC-SA 4.0
Last updated on Sep 30, 2025 19:46 CST
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy