迭代器模式

假设我们的早餐要在煎饼屋吃,而午餐要在餐厅吃,这是两家不同的餐厅,每一家餐厅的菜单都有各自不同的数据结构实现(但是有一点相同的地方,就是具体的产品上用的同一个数据结构),那么我们该如何设计一个程序来实现这个吃饭的功能呢?

首先我们先看一个煎饼屋的类

可以看到类主要使用了ArrayList的数据结构

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
public class PancakeHouseMenu {
ArrayList menuItems;

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

addItem("K&R C pancake",
"This is C Bible",
true,
1.99);

addItem("Bjarne C++ pancake",
"This is a magic C++ language",
false,
2.99);

addItem("Guido Python pancake",
"This is a simple and useful language",
true,
3.99);

addItem("Gosling java pancake",
"Nothing to say!",
false,
4.99);
}

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

public ArrayList getMenuItems() {
return menuItems;
}
}

然后我们看下午餐餐厅的类

这个类中主要使用了数组作为数据结构

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

public class DinnerMenu {
static final int MAX_VALUE = 4;
int numberOfItem = 0;
MenuItem[] menuItems;

public DinnerMenu() {
menuItems = new MenuItem[MAX_VALUE];

addItem("Tomato",
"Tomato on whole wheat",
true,
5.99);

addItem("Meat",
"Bacon meat",
false,
6.99);

addItem("Soup",
"potato soup",
true,
7.99);

addItem("Hotdog",
"A hot dog",
false,
4.99);
}

public void addItem(String name, String description,
boolean vegetarian, double price) {
MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
if (numberOfItem >= MAX_VALUE) {
System.err.println("Sorry, the menu is full!");
} else {
menuItems[numberOfItem++] = menuItem;
}
}

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

它们为什么不用相同的数据结构呢?可能是不同的人开发的吧,这个我们不能强求啊。不过好歹它们的每个具体项目还是用到的相同的数据结构(其实这里不怕,哪怕它们用的不是相同的结构,我们也可以用适配器模式来解决)。

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
public 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 boolean getVegeratian() {
return vegetarian;
}

public double getPrice() {
return price;
}
}

现在有了菜单的类,我们就需要一个服务员的类来给我们提供相应的餐食服务了,如下所示:

因为早餐和午餐使用了不同的数据结构,所以服务员要分别处理这两种数据结构,进而提供相应的遍历访问服务。

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
public class Waitress {
ArrayList breakfastItems;
MenuItem[] lunchItems;

public Waitress () {
PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
breakfastItems = pancakeHouseMenu.getMenuItems();

DinnerMenu dinnerMenu = new DinnerMenu();
lunchItems = dinnerMenu.getMenuItems();
}
public void printMenu() {
for (int i = 0; i < breakfastItems.size(); i++) {
MenuItem menuItem = (MenuItem)breakfastItems.get(i);
System.out.print(menuItem.getName() + " ");
System.out.println(menuItem.getPrice() + " ");
System.out.println(menuItem.getDescription() + " ");
}

for (int i = 0; i < lunchItems.length; i++) {
MenuItem menuItem = lunchItems[i];
System.out.print(menuItem.getName() + " ");
System.out.println(menuItem.getPrice() + " ");
System.out.println(menuItem.getDescription() + " ");
}
}

public void printVegetarianMenu() {
for (int i = 0; i < breakfastItems.size(); i++) {
MenuItem menuItem = (MenuItem)breakfastItems.get(i);
if (menuItem.getVegeratian()) {
System.out.print(menuItem.getName() + " ");
System.out.println(menuItem.getPrice() + " ");
System.out.println(menuItem.getDescription() + " ");
}
}

for (int i = 0; i < lunchItems.length; i++) {
MenuItem menuItem = lunchItems[i];
if (menuItem.getVegeratian()) {
System.out.print(menuItem.getName() + " ");
System.out.println(menuItem.getPrice() + " ");
System.out.println(menuItem.getDescription() + " ");
}
}
}
}

具体代码归档位置如下:

https://github.com/changyuanchn/DesignPattern/tree/main/src/com/changyuan/IteratorPattern/baseDemo

那么现在问题来了,假设我们需要添加更多的类型,那么Waitress就要适配更多的类型的修改(printMenu函数中),而且这种模式还向Waitress暴露了太多的菜单类的细节(比如一个是ArrayList实现的,一个是数组实现的,在printMenu函数中暴露的一目了然)。那么我们即不想暴露太多细节,又不想让Waitress适配太多,该怎么做呢?

上面的Waitress代码中,如果我们在printMenu函数中能够用一种方式进行遍历就好了。迭代器模式就可以解决这个问题。

迭代器模式提供了一种方法顺序的访问一个聚合对象中的各个元素,而又不暴露其内部实现

迭代器实际上就是封装了上面的遍历,也就是说它依赖于一个名为迭代器的接口。对于每一个数据结构,都有一个迭代器的接口实现。

下面我们通过代码来具体看下:

首先我们需要定义迭代器的接口:
接口主要有两个函数,一个是判断是否有下一个节点,另一个是访问下一个节点

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

然后我们针对每一种餐厅的类型,创建相应的迭代器:

这个迭代器主要实现上面的两个接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class PancakeHouseMenuIterator implements Iterator {
ArrayList menuItems;
int position = 0;

public PancakeHouseMenuIterator(ArrayList menuItems) {
this.menuItems = menuItems;
}

public boolean hasNext() {
if (position >= menuItems.size() || menuItems.get(position) == null) {
return false;
}
return true;
}

public Object next() {
MenuItem menuItem = (MenuItem) menuItems.get(position++);
return menuItem;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class DinnerMenuIterator implements Iterator{
MenuItem[] menuItems;
int position = 0;

public DinnerMenuIterator(MenuItem[] menuItems) {
this.menuItems = menuItems;
}

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

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

然后要修改我么你的餐厅类,之前的实现中,我们直接获取了整个的menuItems,在下面的这个函数中,实际上也暴露了程序内部的实现的细节(比如说我们用数组MenuItem[]来实现的菜单项管理)。

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

现在我们有了迭代器了,我们就不需要这个函数了,我们直接返回迭代器就好了。针对不同的类返回其匹配的迭代器

1
2
3
4
5
6
public class PancakeHouseMenu {
......
public Iterator createIterator() {
return new PancakeHouseMenuIterator(menuItems);
}
}
1
2
3
4
5
6
public class DinnerMenu {
......
public Iterator createIterator() {
return new DinnerMenuIterator(menuItems);
}
}

然后我们在Waitress类中就可以用Iterator来实现对菜单项目的访问了,无论对于PancakeHouseMenu还是DinnerMenu,都可以统一用Iterator来遍历了,就不用在printMenu函数中写两套逻辑了。

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

public class Waitress {
PancakeHouseMenu pancakeHouseMenu;
DinnerMenu dinnerMenu;

public Waitress (PancakeHouseMenu pancakeHouseMenu, DinnerMenu dinnerMenu) {
this.pancakeHouseMenu = pancakeHouseMenu;
this.dinnerMenu = dinnerMenu;
}

public void printMenu() {
Iterator pancakeIterator = pancakeHouseMenu.createIterator();
Iterator dinnerIterator = dinnerMenu.createIterator();
System.out.println("------- Get breakfast -------");
printMenu(pancakeIterator);
System.out.println("------- End -------");
System.out.println("------- Get lunch -------");
printMenu(dinnerIterator);
System.out.println("------- End -------");
}

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

public void printVegetarianMenu() {
Iterator pancakeIterator = pancakeHouseMenu.createIterator();
Iterator dinnerIterator = dinnerMenu.createIterator();
System.out.println("------- Get vegetarian breakfast -------");
printVegetarianMenu(pancakeIterator);
System.out.println("------- End -------");
System.out.println("------- Get vegetarian lunch -------");
printVegetarianMenu(dinnerIterator);
System.out.println("------- End -------");
}

public void printVegetarianMenu(Iterator iterator) {
while(iterator.hasNext()) {
MenuItem menuItem = (MenuItem) iterator.next();
if (menuItem.getVegeratian()) {
System.out.print(menuItem.getName() + " ");
System.out.println(menuItem.getPrice() + " ");
System.out.println(menuItem.getDescription() + " ");
}
}
}
}

具体的代码归档路径如下所示

https://github.com/changyuanchn/DesignPattern/tree/main/src/com/changyuan/IteratorPattern/IteratorDemo

当前的重构实现已经解决了我们之前提出的问题,那么上面的代码是不是就没有问题了呢?我们发现我们的Waitress是基于具体实现在编程,而不是接口,这违背了我们之前的设计原则

针对接口编程,而不是针对实现编程

同时我们也使用java库自带的迭代器来重构下这个需求。

首先我们创建一个menu的接口

1
2
3
public interface Menu {
public Iterator createIterator();
}

我们的餐厅类都要实现这个接口,这样在Waitress中就可以不针对具体的实现了。

而由于我们的ArrayList本身就实现了Iterator接口,所以这里就可以直接使用,而不用对PancakeHouseMenu新创建Iterator接口了。

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
public class PancakeHouseMenu implements Menu {
ArrayList menuItems;

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

addItem("K&R C pancake",
"This is C Bible",
true,
1.99);

addItem("Bjarne C++ pancake",
"This is a magic C++ language",
false,
2.99);

addItem("Guido Python pancake",
"This is a simple and useful language",
true,
3.99);

addItem("Gosling java pancake",
"Nothing to say!",
false,
4.99);
}

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

public Iterator createIterator() {
return menuItems.iterator();
}
}
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
public class DinnerMenu implements Menu {
static final int MAX_VALUE = 4;
int numberOfItem = 0;
MenuItem[] menuItems;

public DinnerMenu() {
menuItems = new MenuItem[MAX_VALUE];

addItem("Tomato",
"Tomato on whole wheat",
true,
5.99);

addItem("Meat",
"Bacon meat",
false,
6.99);

addItem("Soup",
"potato soup",
true,
7.99);

addItem("Hotdog",
"A hot dog",
false,
4.99);
}

public void addItem(String name, String description,
boolean vegetarian, double price) {
MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
if (numberOfItem >= MAX_VALUE) {
System.err.println("Sorry, the menu is full!");
} else {
menuItems[numberOfItem++] = menuItem;
}
}

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

这样我们的waitress类就可以做如下的修改了

1
2
3
4
5
6
7
8
9
10
public class Waitress {
Menu pancakeHouseMenu;
Menu dinnerMenu;

public Waitress(Menu pancakeHouseMenu, Menu dinnerMenu, Menu cafeMenu) {
this.pancakeHouseMenu = pancakeHouseMenu;
this.dinnerMenu = dinnerMenu;
}
......
}

通过上面的重构,当我们需要添加新的餐厅类型,也会变得比较简单了。

具体的代码请参见下面连接,在代码中我们新添加了CafeMenu类

https://github.com/changyuanchn/DesignPattern/tree/main/src/com/changyuan/IteratorPattern/IteratorRefactorDemo

下面就是上面的demo的类图

IteratorPattern

设计原则:

一个类应该只有一个引起变化的原因

内聚

用来度量一个类或模块紧密地达到单一目的或责任。当一个模块或者类被设计成只支持一组相关的功能时,我们说它具有高内聚

显示 Gitment 评论