详细设计——设计模式

AI WARNING

未进行对照审核。

可修改性

在软件设计中,可修改性是一个核心的质量属性,它指的是系统在发生变化时,进行修改的难易程度。良好的可修改性可以降低维护成本,提高系统的生命周期。可修改性通常体现在以下几个方面:

  1. 实现的可修改性(M): 指对系统中已有功能的实现进行调整或修正的能力。
    • 例如:调整现有促销策略的折扣率或生效条件。
  2. 实现的可扩展性(E): 指向系统中添加新功能或新实现的能力,而对现有部分影响较小。
    • 例如:增加一种全新的促销策略,如「满额赠品」。
  3. 实现的灵活性(C): 指系统在运行时动态调整其行为或配置的能力。
    • 例如:根据当前销售情况,动态地为某个商品切换不同的促销策略。

如何实现可修改性:接口与实现的分离

实现可修改性的关键在于接口与实现的分离。这意味着客户端代码(使用方)依赖于一个稳定的接口,而该接口的具体实现可以独立变化,甚至可以被替换,而不影响客户端。

在 Java 等面向对象语言中,主要通过以下方式实现接口与实现的分离:

  1. 通过接口:

    • interface 定义了一组契约(方法签名),规定了实现类必须提供的功能。
    • 具体的 class 实现该 interface,提供功能的具体逻辑。
    • 客户端代码通过接口类型的引用来操作对象,从而与具体实现类解耦。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    // Client.java
    public class Client {
    public static void main(String[] args) {
    // 依赖接口 Interface_A
    Interface_A a = new Class_A1(); // 实际指向 Class_A1 的实例
    a.method_A();

    a = new Class_A2(); // 可以轻松替换为 Class_A2 的实例
    a.method_A();
    }
    }

    // Interface_A.java
    public interface Interface_A {
    void method_A(); // 规约
    }

    // Class_A1.java
    public class Class_A1 implements Interface_A {
    @Override
    public void method_A() { // 实现规约
    System.out.println("Class_A1's method_A()!");
    }
    }

    // Class_A2.java(新增的实现)
    public class Class_A2 implements Interface_A {
    @Override
    public void method_A() {
    System.out.println("Class_A2's method_A()!");
    }
    }
    • 依赖关系Client 依赖于 Interface_AClientClass_A1Class_A2 之间没有直接的编译时依赖(除了在创建对象时)。
  2. 通过继承:

    • 父类(通常是抽象类)可以定义接口(抽象方法)和部分共享的实现(具体方法)。
    • 子类继承父类,并为抽象方法提供具体实现,或重写父类的具体方法。
    • 客户端代码通过父类类型的引用来操作子类对象。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // Client.java
    public class Client {
    public static void main(String[] args) {
    // 依赖父类 Super_A
    Super_A a = new Sub_A1(); // 实际指向 Sub_A1 的实例
    a.method_A();
    }
    }

    // Super_A.java(父类)
    public abstract class Super_A {
    // 可以有具体实现,子类可重用
    public void commonLogic() {
    System.out.println("Super_A common logic");
    }
    public abstract void method_A(); // 规约(抽象方法)
    }

    // Sub_A1.java(子类)
    public class Sub_A1 extends Super_A {
    @Override
    public void method_A() { // 实现规约
    System.out.println("Sub_A1's method_A()!");
    }
    }
    • 依赖关系Client 依赖于 Super_AClientSub_A1 之间没有直接的编译时依赖(除了在创建对象时)。

继承 vs. 组合

虽然继承和接口都可以实现接口与实现的分离,但它们有各自的优缺点。

继承:

  • 优点
    • 代码重用:子类可以直接继承父类的实现。
  • 缺点
    • 紧耦合:父类与子类之间存在较强的耦合。如果父类的接口(或受保护的实现)发生改变,所有子类都可能受到影响。
    • 静态性:一旦一个对象被创建为某个子类的实例,其具体的行为实现在运行时就固定了,难以动态改变(除非子类内部有更复杂的逻辑)。

组合

  • 一个类(前端类/宿主类)包含另一个类(后端类/委托类)的实例,并将部分工作委托给它。
  • 通常前端类依赖后端类的接口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// BackendInterface.java
interface BackendInterface {
int performAction();
}

// ConcreteBackend.java
class ConcreteBackend implements BackendInterface {
public int performAction() { return 42; }
}

// Frontend.java
class Frontend {
private BackendInterface backend; // 依赖接口

// 通过构造函数或 setter 注入依赖
public Frontend(BackendInterface backend) {
this.backend = backend;
}

public void setBackend(BackendInterface backend) { // 允许动态改变行为
this.backend = backend;
}

public int doSomething() {
// ... 其他逻辑 ...
return backend.performAction(); // 委托给 backend
}
}

// Client.java
public class Client {
public static void main(String[] args) {
BackendInterface backend1 = new ConcreteBackend();
Frontend frontend = new Frontend(backend1);
System.out.println(frontend.doSomething()); // 使用 backend1

// 可以动态替换行为
BackendInterface backend2 = new AnotherConcreteBackend();
frontend.setBackend(backend2);
System.out.println(frontend.doSomething()); // 使用 backend2
}
}

组合的优点:

  • 松耦合:前端类仅依赖后端类的接口,后端接口的变化对前端类的影响较小(只要接口契约不变)。
  • 灵活性:后端类的具体实现可以在运行时动态创建、配置和替换,非常灵活。
  • 遵循「组合优于继承」的设计原则。

设计模式入门

设计模式

设计模式是在面向对象软件设计过程中,针对特定问题上下文的、可重用的、经过验证的解决方案。它不是一个可以直接转换成代码的完成设计,而是一个描述在不同情况下如何解决某个问题的模板。

  • 为什么需要设计模式?
    • 设计高质量、可重用的面向对象软件是困难的。
    • 经验丰富的设计师往往不会从头解决每个问题,而是重用已有的、被证明有效的解决方案。
    • 设计模式是这些经验的结晶,有助于使设计更灵活、优雅、易于理解和维护。
  • 设计模式的组成要素(通常包括):
    1. 模式名称:一个词或短语,简洁地描述模式。
    2. 问题:描述了在何时使用该模式。它解释了模式所要解决的问题以及问题存在的环境、限制条件等。
    3. 解决方案:描述了设计的组成部分、它们之间的相互关系以及各自的职责和协作方式。它通常用类图、序列图等来可视化。这部分不描述具体实现,而是抽象模型。
    4. 效果/效果及折衷:描述了模式应用的效果以及可能存在的权衡。

设计模式提供了一套共享的词汇,使得开发者可以更有效地沟通设计思想。

graph LR
    A[典型问题] --> B(设计分析);
    B --> C{解决方案};
    C --> D[案例];
    C --> E[组成与协作];
    C --> F[应用场景];
    C --> G[使用注意点];

策略模式

策略模式

策略模式定义了一系列算法,并将每一个算法封装起来,使它们可以相互替换。策略模式让算法的变化独立于使用算法的客户。

  • 典型问题:在一个系统中,某个对象的行为(算法)有多种变体,并且需要在运行时根据不同情况选择或切换这些行为。例如,一个连锁超市雇员的薪水支付方式有多种:

    • 钟点工:薪水 = 时薪 * 小时数,每周三支付。
    • 月薪制:薪水 = 固定月薪,每月 21 日支付。
    • 提成制:薪水 = 销售额 * 提成比率,每隔一周的周三支付。
      如果将所有这些逻辑都写在一个类中,会导致大量的 if-elseswitch 语句,难以维护和扩展。例如,钟点工的支付周期可能变为两周一次,或者新增一种支付方式。
  • 设计分析与解决方案

    1. 识别变化点:薪水计算方法和支付日期判断是变化的部分。
    2. 封装变化:将每种薪水计算方式和每种支付日期判断逻辑分别封装到独立的策略类中。
    3. 定义统一接口:为这些策略类定义统一的接口(例如 PaymentClassification 接口用于计算薪水,PaymentSchedule 接口用于判断支付日)。
    4. 上下文持有策略:雇员类(上下文 Context)持有对策略接口的引用。
    5. 组合优于继承:上下文类与策略类之间使用组合关系,而不是让上下文类继承多种行为。这使得策略可以在运行时动态配置。
  • 类图

    classDiagram
        class Context {
            - strategy: Strategy
            + contextInterface()
            + setStrategy(Strategy s)
        }
        class Strategy {
            <<Interface>>
            + algorithmInterface()
        }
        class ConcreteStrategyA {
            + algorithmInterface()
        }
        class ConcreteStrategyB {
            + algorithmInterface()
        }
        Context o-- Strategy : uses
        Strategy <|.. ConcreteStrategyA : implements
        Strategy <|.. ConcreteStrategyB : implements
    • 在薪水支付案例中,Context 对应 Employee 类,Strategy 对应 PaymentClassificationPaymentSchedule 两个接口,ConcreteStrategy 对应各种具体的计算和判断类。
  • 参与者

    • Context(上下文):
      • 维护一个对 Strategy 对象的引用。
      • 可以被配置一个具体的 ConcreteStrategy 对象。
      • 通过调用 Strategy 对象的接口来执行算法。
      • (可选)可以向 Strategy 对象提供其自身的数据。
    • Strategy(策略接口):
      • 声明了所有支持的算法的公共接口。Context 使用这个接口来调用由 ConcreteStrategy 定义的算法。
    • ConcreteStrategy(具体策略):
      • 实现了 Strategy 接口,封装了具体的算法或行为。
  • 协作

    • Context 将客户端的请求委托给其 Strategy 对象。
    • 客户端通常创建并向 Context 传递一个 ConcreteStrategy 对象。之后,ContextStrategy 对象交互,而客户端通常不知道具体使用了哪个 ConcreteStrategy
  • 应用场景

    • 当一个类有多种行为,并且这些行为仅在实现上有所不同时。
    • 当需要动态地为一个对象选择或切换行为(算法)时。
    • 当算法需要使用客户不应该知道的数据时(策略模式可以隐藏算法的复杂细节和数据)。
    • 当一个类中包含大量的条件语句(if-elseswitch),其分支对应不同的行为时。
  • 注意点

    • Strategy 可以是接口也可以是抽象类(如果具体策略有共享的实现部分)。
    • 策略模式增加了对象的数量。
    • 客户端可能需要了解不同的策略,以便选择合适的策略配置给上下文(除非有更高级的逻辑来自动选择)。
    • ContextStrategy 之间的通信可能存在开销。如果 Context 需要传递大量数据给 Strategy,或者 Strategy 需要回调 Context 获取数据,会增加耦合。
  • 案例:雇员薪水支付

    • ClientTestDrive 类,创建雇员和各种策略,并配置给雇员。
    • ContextEmployee 类,持有 PaymentClassificationPaymentSchedule 的引用。
    • Strategy 接口:PaymentClassification(如 calculatePayment())和 PaymentSchedule(如 isPayDate())。
    • ConcreteStrategy
      • HourlyClassification, SalariedClassification, CommissionedClassification(实现了 PaymentClassification)。
      • WeeklySchedule, MonthlySchedule, BiweeklySchedule(实现了 PaymentSchedule)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    // Employee.java (Context)
    public class Employee {
    private String name;
    private PaymentClassification classification; // 薪资计算策略
    private PaymentSchedule schedule; // 支付日期策略

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

    public void setClassification(PaymentClassification classification) {
    this.classification = classification;
    }
    public void setSchedule(PaymentSchedule schedule) {
    this.schedule = schedule;
    }

    public void payDay() {
    double pay = classification.calculatePayment(this); // 假设需要 Employee 信息
    if (schedule.isPayDate(java.time.LocalDate.now())) { // 假设需要当前日期
    System.out.println("Paying " + name + ": " + pay);
    }
    }
    // ... 其他方法,如获取销售额等给提成策略使用
    }

    // PaymentClassification.java (Strategy Interface 1)
    public interface PaymentClassification {
    double calculatePayment(Employee emp);
    }

    // HourlyClassification.java (Concrete Strategy 1.1)
    public class HourlyClassification implements PaymentClassification {
    private double hourlyRate;
    private int hoursWorked;
    // constructor, setters...
    @Override
    public double calculatePayment(Employee emp) { return hourlyRate * hoursWorked; }
    }

    // PaymentSchedule.java (Strategy Interface 2)
    public interface PaymentSchedule {
    boolean isPayDate(java.time.LocalDate date);
    }
    // ... 具体 Schedule 实现 ...

抽象工厂模式

抽象工厂模式

抽象工厂模式提供一个接口,用于创建一系列相关或相互依赖的对象,而无需指定它们具体的类。它使得一个「家族」的对象的创建与使用分离。

  • 前置:工厂模式的演进

    • 问题:对象的创建(new ClassA()) 如果散布在代码各处,当需要改变创建逻辑(例如,根据条件创建 ClassA1ClassA2)时,修改会很困难。Client 直接依赖具体类。
    • 简单工厂/工厂方法:引入一个工厂类/方法,专门负责对象的创建。Client 依赖工厂接口/父类,由具体的工厂子类/方法实现来决定创建哪个具体产品。这解决了单个对象的创建问题。
    • 新问题:如果需要创建的是一组相关的对象(一个产品族),例如,一个品牌的电脑需要该品牌的鼠标、键盘、显示器。如果为每种品牌组合都创建一个简单工厂,会导致工厂数量的「组合爆炸」。
  • 设计分析与解决方案

    • 识别产品族:确定哪些对象是作为一个整体系列出现的(例如,华为系列的鼠标和键盘,戴尔系列的鼠标和键盘)。
    • 抽象工厂接口:定义一个抽象工厂接口(AbstractFactory),该接口包含创建产品族中每个产品的方法(例如 createMouse()createKeyboard())。
    • 具体工厂实现:为每个产品族创建一个具体工厂类(ConcreteFactory),实现抽象工厂接口,负责创建该产品族内的所有具体产品。
    • 抽象产品接口:为产品族中的每种产品定义一个抽象产品接口(例如 Mouse 接口,Keyboard 接口)。
    • 具体产品实现:为每个产品族中的每个具体产品创建相应的类,实现对应的抽象产品接口(例如 HuaweiMouse 实现 MouseDellKeyboard 实现 Keyboard)。
    • 客户端使用:客户端代码通过抽象工厂接口和抽象产品接口与系统交互。客户端选择一个具体工厂,然后使用该工厂创建所需的产品对象,而无需关心具体产品的类名。
  • 类图(PC 配件示例):

    classDiagram
        class Client
        class AbstractFactory {
            <<Interface>>
            + createMouse() : Mouse
            + createKeyboard() : Keyboard
        }
        class HuaweiFactory {
            + createMouse() : Mouse
            + createKeyboard() : Keyboard
        }
        class DellFactory {
            + createMouse() : Mouse
            + createKeyboard() : Keyboard
        }
        class Mouse {
            <<Interface>>
            + click()
        }
        class HuaweiMouse {
            + click()
        }
        class DellMouse {
            + click()
        }
        class Keyboard {
            <<Interface>>
            + type()
        }
        class HuaweiKeyboard {
            + type()
        }
        class DellKeyboard {
            + type()
        }
    
        Client --> AbstractFactory : uses
        AbstractFactory <|.. HuaweiFactory : implements
        AbstractFactory <|.. DellFactory : implements
    
        HuaweiFactory ..> HuaweiMouse : creates
        HuaweiFactory ..> HuaweiKeyboard : creates
        DellFactory ..> DellMouse : creates
        DellFactory ..> DellKeyboard : creates
    
        Mouse <|.. HuaweiMouse : implements
        Mouse <|.. DellMouse : implements
        Keyboard <|.. HuaweiKeyboard : implements
        Keyboard <|.. DellKeyboard : implements
  • 参与者

    • AbstractFactory(抽象工厂):声明了创建一系列抽象产品对象的操作接口。
    • ConcreteFactory(具体工厂):实现了 AbstractFactory 的操作接口,负责创建具体的产品族。
    • AbstractProduct(抽象产品):为某一类产品对象声明接口。
    • ConcreteProduct(具体产品):定义了由相应具体工厂创建的产品对象,实现了 AbstractProduct 接口。
    • Client(客户端):仅使用 AbstractFactoryAbstractProduct 接口。
  • 协作

    • 客户端请求 AbstractFactory 创建产品。
    • ConcreteFactory 实例化并返回具体的产品对象。
    • 通常情况下,一个应用在运行时只需要一个 ConcreteFactory 的实例,来创建属于特定产品族的对象。
  • 应用场景

    • 当一个系统需要独立于其产品的创建、组合和表示时。
    • 当一个系统要被配置为使用多个产品族中的一个时。
    • 当你要提供一个产品类库,但只想暴露接口而不是具体实现时。
    • 当一个产品族的产品被设计为需要一起工作时(抽象工厂可以强制这种约束)。
  • 注意点

    • 隔离了具体类:客户端不依赖于具体产品类,只依赖抽象接口。
    • 易于交换产品族:改变具体工厂的实例,就可以改变整个产品族的表现。
    • 保证产品兼容性:由于一个具体工厂创建同一产品族的产品,可以保证这些产品之间的兼容性。
    • 难以支持新种类的产品:如果要在产品族中增加一个新的产品种类(例如,增加 createMonitor() 方法到 AbstractFactory),则需要修改 AbstractFactory 接口及其所有子类,这通常很困难。抽象工厂模式更适合产品种类稳定,而产品族易于扩展的场景。
  • 工厂方法模式
    抽象工厂模式中的每个创建方法(如 createMouse())通常可以用工厂方法模式来实现。工厂方法模式定义一个用于创建对象的接口,让子类决定实例化哪一个类。它将类的实例化延迟到子类。

  • 案例:数据库服务

    • AbstractFactoryDatabaseFactory(提供 getCommodityTable(), getMemberTable() 等方法,返回 DatabaseService 接口)。
    • ConcreteFactoryDatabaseFactoryTxtFileImpl, DatabaseFactorySerializableImpl(分别用文本文件和序列化方式实现数据存储)。
    • AbstractProductDatabaseService(提供数据操作接口)。
    • ConcreteProductDatabaseServiceTxtFileImpl, DatabaseServiceSerializableImpl
    • ClientSalesDataServiceImpl(通过 DatabaseFactory 获取各种 DatabaseService 来操作数据)。

单件模式

单件模式

单件模式,也称单例模式,确保一个类只有一个实例,并提供一个全局访问点来获取这个唯一实例。

  • 典型问题:在某些场景下,需要确保系统中某个类在任何时候最多只有一个实例存在。例如,系统配置管理器、线程池、日志对象、数据库连接池等。无论尝试创建多少次该类的对象,都应该返回同一个实例。

  • 设计分析与解决方案

    1. 私有构造函数:将类的构造函数声明为 private,防止外部代码通过 new 操作符直接创建实例。
    2. 静态私有实例变量:在类内部维护一个静态的、私有的该类类型的实例变量,用于持有唯一的实例。
    3. 静态公有工厂方法:提供一个静态的、公有的方法(通常命名为 getInstance()),作为获取唯一实例的全局访问点。
      • 在该方法内部,检查静态实例变量是否已被初始化。
      • 如果未初始化(通常是 null),则创建新实例并赋值给静态实例变量。
      • 返回该静态实例变量。
  • 类图

    classDiagram
        class Singleton {
            - uniqueInstance: Singleton$
            - singletonData: int
            - Singleton()
            + getInstance(): Singleton$
            + singletonOperation()
            + getSingletonData(): int
        }
        Singleton --|> Singleton : uniqueInstance (static ref)
  • 参与者

    • Singleton(单件类)
      • 定义一个 getInstance() 操作,允许客户从任何地方访问它的唯一实例。
      • 可能负责创建它自己的唯一实例。
  • 协作

    • 客户只能通过 Singleton 类的 getInstance() 方法访问单件的实例。
  • 应用场景

    • 当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时。
    • 当这个唯一实例应该是通过子类化可扩展的,并且客户应该无需更改代码就能使用一个扩展的实例时(较少见,通常通过配置实现)。
  • 注意点

    • 全局访问点:提供了对唯一实例的全局访问,但可能引入全局状态,使测试变困难。
    • 延迟初始化:可以通过在 getInstance() 首次被调用时才创建实例(懒汉式),节省资源。
    • 线程安全:在多线程环境下,懒汉式单例的 getInstance() 方法需要特别处理以确保线程安全(例如使用 synchronized 关键字或双重检查锁定)。饿汉式(类加载时即创建实例)是线程安全的。
    • 子类化限制:由于构造函数私有,直接继承 Singleton 类并期望其行为保持单例特性比较困难。
    • 可测试性:过度使用单例可能导致代码紧密耦合,难以进行单元测试。
  • 案例:DatabaseFactoryTxtFileImpl 作为单例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    public class DatabaseFactoryTxtFileImpl implements DatabaseFactory {
    // 1. 静态私有实例变量
    private static DatabaseFactoryTxtFileImpl instance;

    // 2. 私有构造函数
    private DatabaseFactoryTxtFileImpl() {
    // 初始化操作,例如加载配置等
    System.out.println("DatabaseFactoryTxtFileImpl instance created.");
    }

    // 3. 静态公有工厂方法
    public static synchronized DatabaseFactoryTxtFileImpl getInstance() {
    if (instance == null) {
    instance = new DatabaseFactoryTxtFileImpl();
    }
    return instance;
    }

    // 实现 DatabaseFactory 接口的其他方法…
    @Override
    public DatabaseService getCommodityTable() {
    // ...
    return null; // 示例
    }
    // ...
    }

    // Client code
    DatabaseFactory factory1 = DatabaseFactoryTxtFileImpl.getInstance();
    DatabaseFactory factory2 = DatabaseFactoryTxtFileImpl.getInstance();
    System.out.println(factory1 == factory2); // true

迭代器模式

迭代器模式

迭代器模式提供一种方法来顺序访问一个聚合对象(例如列表、集合)中的各个元素,而又不暴露该对象的内部表示。

  • 典型问题:需要遍历一个集合对象的元素,但希望:

    1. 不关心集合的具体类型(例如,是 ArrayList 还是 HashSet 还是自定义的集合结构)。
    2. 不暴露集合的内部存储细节(例如,数组、链表、哈希表)。
    3. 能够支持多种不同的遍历方式(例如,正序、逆序)。
      如果直接将集合对象传递给处理函数,处理函数会与集合的具体实现耦合。如果集合内部结构改变,处理函数也可能需要修改。
  • 设计分析与解决方案

    1. 抽象遍历行为:将遍历操作(如「是否有下一个元素」「获取下一个元素」)抽象出来,定义一个迭代器接口(Iterator)。
    2. 具体迭代器实现:为每种需要遍历的聚合类提供一个或多个具体的迭代器类(ConcreteIterator),实现迭代器接口。具体迭代器负责维护遍历状态(如当前位置)。
    3. 聚合提供迭代器:聚合类(Aggregate)定义一个创建迭代器对象的方法(例如 createIterator()iterator())。
    4. 客户端使用迭代器:客户端通过聚合对象获取迭代器实例,然后使用迭代器接口来遍历元素,从而与聚合的具体结构解耦。
  • 类图

    classDiagram
        class Client
        class Aggregate {
            <<Interface>>
            + createIterator() : Iterator
        }
        class ConcreteAggregate {
            - items[]
            + createIterator() : Iterator
            + getItem(index)
            + count()
        }
        class Iterator {
            <<Interface>>
            + first()
            + next()
            + isDone() : boolean
            + currentItem()
        }
        class ConcreteIterator {
            - aggregate: ConcreteAggregate
            - currentIndex: int
            + first()
            + next()
            + isDone() : boolean
            + currentItem()
        }
    
        Client ..> Aggregate : uses
        Client ..> Iterator : uses
        Aggregate <|.. ConcreteAggregate : implements
        Iterator <|.. ConcreteIterator : implements
        ConcreteAggregate ..> ConcreteIterator : creates
        ConcreteIterator o-- ConcreteAggregate : references
    • Java 中的 java.util.Iteratorjava.lang.Iterable 就是此模式的体现。Iterable 对应 AggregateIterator 对应 Iterator
  • 参与者

    • Iterator(迭代器接口):定义访问和遍历元素的接口,如 hasNext()next()remove()(可选)。
    • ConcreteIterator(具体迭代器):实现 Iterator 接口,负责跟踪聚合中的当前位置,并能计算出待遍历的后继对象。
    • Aggregate(聚合接口):定义创建相应迭代器对象的接口(例如 iterator() 方法)。
    • ConcreteAggregate(具体聚合):实现 Aggregate 接口,返回一个适当的 ConcreteIterator 实例。
  • 协作

    • ConcreteIterator 跟踪聚合中的当前对象,并知道如何访问下一个元素。
  • 应用场景

    • 需要访问一个聚合对象的内容而无需暴露它的内部表示。
    • 需要支持对聚合对象的多种遍历方式。
    • 需要为遍历不同的聚合结构提供一个统一的接口。
  • 注意点

    • 简化聚合接口:迭代器将遍历的责任从聚合对象中分离出来,简化了聚合的接口。
    • 支持多种遍历:可以为一个聚合提供多种不同的迭代器(例如,正向迭代器、反向迭代器)。
    • 并发修改:如果在遍历过程中,聚合对象被修改(添加或删除元素)而迭代器不知道,可能会导致不可预期的行为(例如 Java 中的 ConcurrentModificationException)。一些迭代器实现会检测这种并发修改。
    • 「值传递」效果:当方法参数是迭代器而非集合本身时,方法内部通过迭代器访问元素,通常不能修改原集合(除非迭代器提供了 remove() 等修改方法并被调用),这在某种程度上模拟了对集合内容的「值传递」访问,减少了副作用。
  • 案例:使用 Java 内建迭代器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    import java.util.ArrayList;
    import java.util.HashSet;
    import java.util.Iterator;
    import java.util.List;
    import java.util.Set;
    import java.util.Collection;

    public class IteratorExample {

    // g() 方法依赖抽象的 Collection 和 Iterator,不关心具体是 ArrayList 还是 HashSet
    public static void processElements(Collection<String> collection) {
    Iterator<String> iterator = collection.iterator(); // 获取迭代器
    while (iterator.hasNext()) {
    String element = iterator.next();
    System.out.println("Processing: " + element);
    // iterator.remove(); // 可以选择性移除元素
    }
    }

    public static void main(String[] args) {
    List<String> arrayList = new ArrayList<>();
    arrayList.add("Apple");
    arrayList.add("Banana");
    System.out.println("--- Processing ArrayList ---");
    processElements(arrayList);

    Set<String> hashSet = new HashSet<>();
    hashSet.add("Cherry");
    hashSet.add("Date");
    System.out.println("--- Processing HashSet ---");
    processElements(hashSet);
    }
    }

    在这个例子中,ArrayListHashSet 都是 ConcreteAggregate,它们都实现了 Iterable (Aggregate) 接口的 iterator() 方法。processElements 方法(Client)使用 Iterator 接口进行遍历,与具体的集合类型解耦。