设计模式<一>:迭代器模式

176 阅读6分钟

前言

我们先看一个最简单的例子

/**
 * 功能描述
 *
 * @author: zgq
 * @date: 2024年10月26日 11:57
 */
public class test {
    public static void main(String[] args) {
        int[] numbers = {
                1, 2, 3, 4, 5, 6, 7, 8, 9, 10
        };

        for (int i = 0; i < numbers.length; i++) {
            System.out.println("numbers[i] = " + numbers[i]);
        }
    }
}

在这个例子中,我们有一个循环遍历i,该变量的初始值为0,然后会通过i++,逐次递增

arr[0]   --> 最开始的元素(第0个元素)
arr[1]   --> 下一个元素(第1个元素)
                .
                .
                .
arr[i]   --> (第i个元素)
arr[arr.length-1]  --> (最后一个元素)    

这样就实现了对一个数组的从头到尾的遍历,将这里的循环变量i抽象化,通用化后形成的模式,就叫做 Iterator(迭代器)模式

UML类图

image-20241026123734971

类和接口一览表

名字说明
Aggregate表示集合的接口
Iterator遍历集合的接口
Book表示书的类
BookShelf表示书架的类
BookShelfIterator遍历书架的类
Main测试类

代码清单

Aggregate接口

/**
 * 功能描述
 * Iterator iterator(); 会生成一个用来遍历集合的迭代器
 * @author: zgq
 * @date: 2024年10月26日 12:10
 */
public interface Aggregate {

    Iterator iterator();
}
  • iterator:想要遍历集合中的元素时,可以调用该方法生成一个实现了Iterator接口的类的实例

Iterator接口

/**
 * 功能描述
 * boolean hasNext(); 检查有没有下一个元素
 * Object next(); 获取下一个元素
 * @author: zgq
 * @date: 2024年10月26日 12:10
 */
public interface Iterator {

    boolean hasNext();

    Object next();
}
  • hasNext: 该方法用于检查是否存在下一个元素
  • next: 该方法负责返回集合中的一个元素,并且包含移动指针指向下一个元素

Book类

/**
 * 功能描述
 * 一个简单的实体类
 * @author: zgq
 * @date: 2024年10月26日 12:12
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Book {

    private String name;
}

BookShelf类

/**
 * 功能描述
 * 这个是书架类,表示书的集合类
 * @author: zgq
 * @date: 2024年10月26日 12:11
 */
public class BookShelf implements Aggregate {

    private Book[] books;
    private int last = 0;


    public BookShelf(int maxsize) {
        this.books = new Book[maxsize];
    }

    public Book getBookAt(int index) {
        return books[index];
    }

    public void appendBook(Book book) {
        this.books[last] = book;
        last++;
    }

    public int getLength() {
        return last;
    }

    @Override
    public Iterator iterator() {
        return new BookShelfIterator(this);
    }
}
  • 内部维护了一个Book类型的数组books,并且该数组的大小在BookShelf被创建的时候就指定了
  • 内部维护了一个int类型的last变量,用来记录当前数组最后一个元素的索引
  • Iterator方法会返回BookShelfIterator类,就是BookShelf对应的Iterator

BookShelfIterator

/**
 * 功能描述
 * BookShelf的迭代器,用来遍历BookShelf
 * @author: zgq
 * @date: 2024年10月26日 12:11
 */
public class BookShelfIterator implements Iterator {

    private BookShelf bookShelf;  //表示需要遍历的书架
    private int index;            //表示当前迭代器指向书架的元素下标

    public BookShelfIterator(BookShelf bookShelf) {
        this.bookShelf = bookShelf;
        this.index = 0;
    }

    @Override
    public boolean hasNext() {
        return index < bookShelf.getLength();   // 检查还有没有下一个元素
    }

    @Override
    public Object next() {
        return bookShelf.getBookAt(index++);    //先取出当前指针所指向的book对象,之后再将指针向后移动
    }
}

Main类

/**
 * 功能描述
 * 程序测试运行类
 * @author: zgq
 * @date: 2024年10月26日 13:10
 */
public class Main {
    public static void main(String[] args) {
        BookShelf bookShelf = new BookShelf(4);
        bookShelf.appendBook(new Book("A"));
        bookShelf.appendBook(new Book("B"));
        bookShelf.appendBook(new Book("C"));
        bookShelf.appendBook(new Book("D"));
        Iterator it = bookShelf.iterator();
        while (it.hasNext()) {
            Book book = (Book) it.next();
            System.out.println(book.getName());
        }

    }
}

运行结果如下:

A
B
C
D

模式回顾

通过示例程序,我们可以总结出如下角色

Iterator(迭代器)

  • 该角色负责定义按顺序逐个遍历元素的接口
  • 示例代码中,Iterator扮演这个角色,定义了hasNextnext方法

ConcreteIterator(具体的迭代器)

  • 该角色负责实现Iterator角色所定义的接口
  • 示例代码中,BookShelfIterator负责扮演这个角色,BookShelf类的示例保存在bookShelf字段中,元素下标保存在index

Aggregate(集合)

  • 该角色负责定义创建Iterator角色的接口
  • 示例代码中,Aggregate接口负责扮演这个角色

ConcreteAggregate(具体的集合)

  • 负责实现Aggregate角色所定义的接口
  • 示例代码中,BookShelf负责扮演这个角色

类图

image-20241026133406213

问题思考

为什么需要使用Iterator(迭代器)?

回顾一下测试程序的代码

    public static void main(String[] args) {
        BookShelf bookShelf = new BookShelf(4);
        bookShelf.appendBook(new Book("A"));
        bookShelf.appendBook(new Book("B"));
        bookShelf.appendBook(new Book("C"));
        bookShelf.appendBook(new Book("D"));
        Iterator it = bookShelf.iterator();
        while (it.hasNext()) {
            Book book = (Book) it.next();
            System.out.println(book.getName());
        }

    }
  • 从这段代码可以看到,我们遍历bookShelf,是通过IteratorhasNextnext方法实现的,并没有调用bookShelf的方法,简单来说就是我们遍历bookShelf对象是不依赖它自身的实现,我们所依赖的是它返回的实现了Iterator的类的实例。
  • 在我们的实例程序中,我们是使用Book[]来存储的数据,假如有一天不使用数组来存储,需要改完List<Book>来实现的话,不用修改我们的while循环测试代码(只要返回的Iterator类的实例没有问题)
  • 因此,一句话说完就是,Iterator的作用就是将遍历和实现分离开来,在设计发生变动的时候,只需要修改实现部分,不需要修改遍历操作部分

Aggregate和Iterator的对应关系

  • BookShelf实现了Aggregate所定义的接口
  • BookShelfIterator实现了Iterator所定义的接口
  • BookShelfIterator需要知道BookShelf的实现才能知道该如何执行遍历操作

容易弄错 "下一个"

  • 容易在next方法出错,搞不清楚应该是返回当前指针所指向的元素还是指向当前元素的下一个元素
  • 更详细的说,next方法的全程应该叫returnCurrentElementAndAdvanceToNextPosition
  • 也就是说,next的方法就是 返回当前元素,并指向下一个元素

容易弄错 "最后一个"

  • hasNext方法需要理解为 接下来是否还可以调用next方法

多个Iterator

  • 如果我们需要不同的遍历逻辑,例如,奇偶遍历等,可以编写不同的ConcreteIterator角色来实现不同的遍历逻辑

多种多样的Iterator

  • 示例代码,只是简单的实现了从前到后的遍历模式,实际还会有各种各样的遍历模式,比如
    • 从后向前遍历
    • 前后双向遍历
    • 从指定位置开始跳跃遍历

习题

修改BookShelf类,将数组替换为 集合

/**
 * 功能描述
 * 这个是书架类,表示书的集合类
 *
 * @author: zgq
 * @date: 2024年10月26日 12:11
 */
public class BookShelf implements Aggregate {

    private List<Book> books;


    public BookShelf(int initialSize) {
        this.books = new ArrayList<>(initialSize);
    }

    public Book getBookAt(int index) {
        return books.get(index);
    }

    public void appendBook(Book book) {
       this.books.add(book);
    }

    public int getLength() {
        return books.size();
    }

    @Override
    public Iterator iterator() {
        return new BookShelfIterator(this);
    }
}

总结

  • 在初次接触迭代器模式时,可能会觉得它相对于直接的 fori 循环来说显得有些繁琐。然而,理解迭代器模式的关键不在于比较遍历集合的便捷性,而在于领悟其背后的设计理念。学习迭代器模式,我们实际上是在探索一种解耦的策略,它将集合的遍历机制与具体的遍历操作分离开来,从而提高代码的灵活性和可维护性。这种设计思想是面向对象编程中的一个重要原则,它有助于构建更加模块化和可扩展的系统。
  • 涉及到的代码,都在我的个人仓库