Post

设计模式:迭代器模式

应用场景

之前看各种示例代码,iterator 是一个出现频率很高的东西,大多数时候都是在我们需要遍历一个线性的数据结构的时候会出现。当时我很不理解为什么会有这个东西,直接对数据结构使用 for 或 while 循环不是更直接也更省事吗?这个东西到底是解决什么问题呢?

带着这些问题,我们来看一个案例,假设有一家餐厅 A 的菜单,实现如下:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
class RestaurantAMenu {
  static final int MAX_ITEMS = 6;
  int numberOfItems = 0;
  MenuItem[] menuItems;

  public RestaurantAMenu() {
    menuItems = new MenuItem[MAX_ITEMS];

    addItem(
      "Vegetarian BLT",
      "Fakin' Bacon with lettuce & tomato on whole wheat",
      true,
      2.99
    );

    addItem(
      "BLT",
      "Bacon with lettuce & tomato on whole wheat",
      false,
      2.99
    );

    addItem(
      "Soup of the day",
      "Soup of the day, with a side of potato salad",
      false,
      3.29
    );

    addItem(
      "Hotdog",
      "A Hot Dog, with saurkraut, relish, onions, topped with cheese",
      false,
      3.05
    );
  }

  public void addItem(String name, String desc, boolean vegetarian, double price) {
    MenuItem menuItem = new MenuItem(name, desc, vegetarian, price);
    if (numberOfItems >= MAX_ITEMS) {
      System.err.println("Sorry, menu is full! Can't add item to menu");
    } else {
      menuItems[numberOfItems] = menuItem;
      numberOfItems = numberOfItems + 1;
    }
  }

  public MenuItem[] getMenuItems() {
    return menuItems;
  }

  // other menu methods here
}

class MenuItem {
  String name;
  String description;
  boolean vegetarian;
  double price;

  public MenuItem(String name,
                  String description,
                  boolean vegetarian,
                  double price) {
    this.name = name;
    this.description = description;
    this.vegetarian = vegetarian;
    this.price = price;
  }

  public String getName() {
    return name;
  }

  public String getDescription() {
    return description;
  }

  public double getPrice() {
    return price;
  }

  public boolean isVegetarian() {
    return vegetarian;
  }
}

可以看到,菜单的类使用一个静态数组 menuItems 来对菜品 MenuItem 进行存储和管理。假如说,现在有另外一家餐厅 B,也实现了一个菜单类:

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
44
45
46
class RestaurantBMenu {
  ArrayList menuItems;

  public RestaurantBMenu() {
    menuItems = new ArrayList();

    addItem(
      "K&B's Pancake Breakfast",
      "Pancake with scrambled eggs, and toast",
      true,
      2.99
    );

    addItem(
      "Regular Pancake Breakfast",
      "Pancake with fried eggs, sausage",
      false,
      2.99
    );

    addItem(
      "Blueberry Pancake",
      "Pancake made with fresh blueberries",
      true,
      3.49
    );

    addItem(
      "Waffles",
      "Waffles, with your choice of blueberries or strawberries",
      true,
      3.59
    );
  }

  public void addItem(String name, String desc, boolean vegetarian, double price) {
    MenuItem menuItem = new MenuItem(name, desc, vegetarian, price);
    menuItems.add(menuItem);
  }

  public ArrayList getMenuItems() {
    return menuItems;
  }

  // other menu methods here
}

与之前不同的是,这个实现类中的菜单结构使用的是 Java 的集合 ArrayList。这导致的结果就是客户端(假如说是外卖公司)需要用不同的方式去查看这两家餐厅的菜单。你可能会觉得这没什么,不就是多写一个函数的事嘛。其实还真不是这样,我们这就来细说一下上面的代码存在的问题:

  1. 就像我们所提到的,因为数据结构的不同,客户端需要用不同的遍历方式来进行处理,我们更希望的是客户端基于一种方式来处理所有的情况(菜单)。
  2. 上面两个类都是实体类,而客户端依赖于这些实体类,随着新的实体类的不断加入。每次加入,我们都需要去修改客户端的代码,最终导致代码臃肿,逻辑混乱且难以维护,这也违背了我们之前说的 “面向接口和抽象编程” 的原则。
  3. 类中的 getMenuItems 函数直接将自己维护的数据结构返回,这也会带来不少的问题。首先数据结构存在被外界修改的风险,并且外面改了,自己这边还不知情,这会带来一些隐患。此外,这也暴露给外界很多内部实现上面的细节,这些其实都是外界不需要的,只会降低代码的可读性。
  4. 最后一点,有两个因素可能导致 menuItems 发生变化,一个是如何存储,或者说用什么样的数据结构进行存储,另一个是如何遍历,或者说如何获取和查看结构里的元素。而上面的实现类并没有很好地处理好这两个变量,仅仅是简单地将数据结构返回。这样,menuItems 承担了存储职责的同时,外界也需要对其判断,并由它来确定遍历的方式,这违背了 单一职责(Single Responsibility) 的设计原则,随着代码量的增加,这只会带来更加严重的问题。

解决上述问题的办法就是应用迭代器模式,迭代器是如下的一个接口:

1
2
3
4
interface Iterator {
  public boolean hasNext();
  public Object next();
}

接口如同契约,实现这个接口的类需要实现这两个方法。其中 hasNext 方法用于判断遍历是否结束,next 用于获取下一个元素,客户端只需按照这个接口所定义的那样,对所有的实现类采取统一的处理方式即可。把迭代器模式应用到上面的例子就可以得到如下的代码:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
interface Iterator {
  public boolean hasNext();
  public Object next();
}

class RestaurantAIterator implements Iterator {
  MenuItem[] items;
  int position = 0;

  public RestaurantAIterator(MenuItem[] items) {
    this.items = items;
  }

  public Object next() {
    MenuItem menuItem = items[position];
    position = position + 1;
    return menuItem;
  }

  public boolean hasNext() {
    if (position >= items.length || items[position] == null) {
      return false;
    } else {
      return true;
    }
  }
}

class RestaurantBIterator implements Iterator {
  ArrayList items;
  int position = 0;

  public RestaurantBIterator(ArrayList items) {
    this.items = items;
  }

  public Object next() {
    MenuItem menuItem = (MenuItem)items.get(position);
    position = position + 1;
    return menuItem;
  }

  public boolean hasNext() {
    if (position >= items.size()) {
      return false;
    } else {
      return true;
    }
  }
}

interface Menu {
  Iterator createIterator();
}

class RestaurantBMenu implements Menu {
  ArrayList menuItems;

  public RestaurantBMenu() {
    menuItems = new ArrayList();

    addItem(
      "K&B's Pancake Breakfast",
      "Pancake with scrambled eggs, and toast",
      true,
      2.99
    );

    addItem(
      "Regular Pancake Breakfast",
      "Pancake with fried eggs, sausage",
      false,
      2.99
    );

    addItem(
      "Blueberry Pancake",
      "Pancake made with fresh blueberries",
      true,
      3.49
    );

    addItem(
      "Waffles",
      "Waffles, with your choice of blueberries or strawberries",
      true,
      3.59
    );
  }

  public void addItem(String name, String desc, boolean vegetarian, double price) {
    MenuItem menuItem = new MenuItem(name, desc, vegetarian, price);
    menuItems.add(menuItem);
  }

  // public ArrayList getMenuItems() {
  //   return menuItems;
  // }

  public Iterator createIterator() {
    return new RestaurantBIterator(menuItems);
  }

  // other menu methods here
}

class RestaurantAMenu implements Menu {
  static final int MAX_ITEMS = 6;
  int numberOfItems = 0;
  MenuItem[] menuItems;

  public RestaurantAMenu() {
    menuItems = new MenuItem[MAX_ITEMS];

    addItem(
      "Vegetarian BLT",
      "Fakin' Bacon with lettuce & tomato on whole wheat",
      true,
      2.99
    );

    addItem(
      "BLT",
      "Bacon with lettuce & tomato on whole wheat",
      false,
      2.99
    );

    addItem(
      "Soup of the day",
      "Soup of the day, with a side of potato salad",
      false,
      3.29
    );

    addItem(
      "Hotdog",
      "A Hot Dog, with saurkraut, relish, onions, topped with cheese",
      false,
      3.05
    );
  }

  public void addItem(String name, String desc, boolean vegetarian, double price) {
    MenuItem menuItem = new MenuItem(name, desc, vegetarian, price);
    if (numberOfItems >= MAX_ITEMS) {
      System.err.println("Sorry, menu is full! Can't add item to menu");
    } else {
      menuItems[numberOfItems] = menuItem;
      numberOfItems = numberOfItems + 1;
    }
  }

  // public MenuItem[] getMenuItems() {
  //   return menuItems;
  // }

  public Iterator createIterator() {
    return new RestaurantAIterator(menuItems);
  }

  // other menu methods here
}

class MenuItem {
  String name;
  String description;
  boolean vegetarian;
  double price;

  public MenuItem(String name,
                  String description,
                  boolean vegetarian,
                  double price) {
    this.name = name;
    this.description = description;
    this.vegetarian = vegetarian;
    this.price = price;
  }

  public String getName() {
    return name;
  }

  public String getDescription() {
    return description;
  }

  public double getPrice() {
    return price;
  }

  public boolean isVegetarian() {
    return vegetarian;
  }
}

class Client {
  Menu pancakeHouseMenu;
  Menu dinerMenu;

  public Client(Menu pancakeHouseMenu, Menu dinerMenu) {
    this.pancakeHouseMenu = pancakeHouseMenu;
    this.dinerMenu = dinerMenu;
  }

  public void printMenu() {
    Iterator pancakeIterator = pancakeHouseMenu.createIterator();
    Iterator dinerIterator = dinerMenu.createIterator();

    System.out.println("MENU\n---\nBREAKFAST");
    printMenu(pancakeIterator);
    System.out.println("\nLUNCH");
    printMenu(dinerIterator);
  }

  public void printMenu(Iterator iter) {
    while (iter.hasNext()) {
      MenuItem menuItem = (MenuItem) iter.next();
      System.out.print(menuItem.getName() + ", ");
      System.out.print(menuItem.getPrice() + " -- ");
      System.out.println(menuItem.getDescription());
    }
  }

  // other methods here
}

整个代码结构图如下:

回过头去看,之前的问题都得到解决了吗?答案是肯定的

  1. 现在等于说是在客户端和 menuItems 之间加了一层抽象——Iterator,客户端对所有的菜单都采用一样的遍历方式,具体如 printMenu 中所示。
  2. 这两个实体类都依赖于 Menu,其实客户端只需维护一个 Menu 集合(这里偷懒没有加)即可,这样新的实体类加入,客户端的逻辑与不需要改变。
  3. 此时,实体类现在不将自己的内部实现暴露给外界了,外界也不知道实体类具体使用什么数据结构来存储。实体类隐藏了自身的实现细节,外界有方法查看对应的元素,双赢。
  4. menuItems 此时只负责存储元素,而遍历元素的职责转嫁到了 hasNextnext 方法,类中的成员的职责变得更加清晰了。

Java 中的迭代器

迭代器模式是一个应用广泛的模式,Java 自带库默认提供了诸多的实现,很多时候我们不需要自己手动实现。比如 java.util.Collection 接口就已经定义了 iterator 方法,这个方法等同于我们上面的 createIterator,会直接返回一个迭代器,而大部分的 Collection 比如上面的 ArrayList 都是实现了 java.util.Collection 接口的,我们直接拿来用就好。

相比于我们自己实现的接口,Java 自带的 iterator 接口 还提供了一些可选项,如 remove 方法,能将最后返回的元素从集合中删除。

抛开例子不谈,单说迭代器模式的话,我们可以得到下面的类关系图:

可以看到的是,客户端(Client)有两个依赖——数据集合类与迭代器类,数据集合类负责存储数据,迭代器类负责遍历数据。重点是,客户端所依赖的都是抽象/接口,这样只要接口不变,新增一个具体类并不会改变客户端的代码。

此外,Aggregate 类与 Iterator 类将具体的实现都交由子类去处理,这样的一个整体框架是否让你想到我们之前学习的 工厂模式 呢?这里的迭代器模式和工厂模式有着异曲同工之妙。

总的来说,迭代器模式提供了一个访问/遍历数据集合元素但又不暴露具体实现的策略。这个数据集合不一定是我们例子中提到的数组或列表,可以是任意的存放数据的结构,而且通过实现接口中的方法,我们可以自定义元素的遍历顺序等逻辑细节,这提供了很多灵活性。

This post is licensed under CC BY 4.0 by the author.