《HeadFirst设计模式》迭代器和组合模式的错误原因详解
下面是本人在阅读HeadFirst 设计模式这本书上讲述的迭代器和组合模式时遇到的问题,该问题产生的原因会在此文中详细的说明,书上例子的详细代码我就不写出来了,大家都懂的。
1、什么样的错误?
当我们运行测试代码后(按照书中的测试用例,自己写也可以,这里的用例是我自己乱编的,意思意思就可以了,都懂的)。
下面是我的用例:
@Test
public void testPrintVegetarian() {
MenuComponent pancakeHouseMenu = new Menu("PANCAKE HOUSE MENU",
"Breakfast");
MenuComponent dinerMenu = new Menu("DINER MENU",
"LUNCH");
MenuComponent cafeMenu = new Menu("CAFE MENU",
"dinner");
MenuComponent dessertMenu = new Menu("DESSERT MENU",
"dessert of course");
MenuComponent allMenus = new Menu("ALL MENU",
"All menu");
allMenus.add(pancakeHouseMenu);
allMenus.add(dinerMenu);
allMenus.add(cafeMenu);
dinerMenu.add(new MenuItem("vegetarian","description",
true, 2.9));
dinerMenu.add(new MenuItem("BLT","description",
true, 2.9));
dinerMenu.add(new MenuItem("Soup of the day","description",
true, 3.59));
dinerMenu.add(new MenuItem("HotDog","description",
true, 3.59));
dinerMenu.add(dessertMenu);
pancakeHouseMenu.add(new MenuItem("K&B's Pancake Breakfast","description",
true, 2.9));
pancakeHouseMenu.add(new MenuItem("Regular Pancake Breakfast","description",
true, 2.9));
pancakeHouseMenu.add(new MenuItem("Blueberry Pancakes","description",
true, 3.59));
pancakeHouseMenu.add(new MenuItem("Waffles","description",
true, 3.59));
cafeMenu.add(new MenuItem("veggie Burger and Air Fries","description",
true, 2.9));
cafeMenu.add(new MenuItem("Soup of the day","description",
true, 2.9));
dessertMenu.add(new MenuItem("Burrito","description2222",
true, 3.59));
Waitress waitress = new Waitress(allMenus);
// waitress.print();
waitress.printVegetarian();
}
控制台执行结果如下:
K&B's Pancake Breakfast(V) , 2.9
-------description
Regular Pancake Breakfast(V) , 2.9
-------description
Blueberry Pancakes(V) , 3.59
-------description
Waffles(V) , 3.59
-------description
vegetarian(V) , 2.9
-------description
BLT(V) , 2.9
-------description
Soup of the day(V) , 3.59
-------description
HotDog(V) , 3.59
-------description
Burrito(V) , 3.59
-------description2222
Burrito(V) , 3.59
-------description2222
veggie Burger and Air Fries(V) , 2.9
-------description
Soup of the day(V) , 2.9
-------description
在这段输出中可以明显看出,Burrito…打印了两次,而我们的测试代码中明显给我们的直觉输出是不应该出现多次打印的,百思不得其解啊!
中午没睡午觉,我想了一下午都没有想通,几经思考无果的情况下,翻了一下百度,我还是没明白(原谅我有点笨~),我完全无法理解那一句话所包含的深刻含义,所以我只能自己继续思考,终于,在下午吃晚饭之前,我想通了,并且在这里把它记录下来,免得今后再看书的时候忘记(记下来将来没想通直接翻……开心~)。
这里的迭代器模式和组合模式的这段代码个人觉得非常厉害,所以我必需要死磕这个问题,
2、错误出现在哪里?
错误出现的代码类是在CompositeIterator类的这段代码中:
@Override
public Object next() {
if(hasNext()) {
Iterator iterator = (Iterator) stack.peek();
MenuComponent menuComponent = (MenuComponent) iterator.next();
if (menuComponent instanceof Menu) {
stack.push(menuComponent.createIterator());
}
return menuComponent;
} else
return null;
}
该代码块中的menuComponent.createIterator()创建了一个组合了stack栈的迭代器,并且将它放在了当前调用者的栈顶(这里的当前调用者其实和menuComponent.createIterator()创建的是同一类型的对象),具体这里说的是什么不在赘述,我想看过书的小伙伴都知道我在说什么,这里应用了组合迭代模式。
3、深入理解为什么出现了问题?
3.1、让我们跟踪一下代码的执行
其中stack1 代表一级列表,stack2 代表二级列表
简单描述下前面几步执行的过程。
程序大概迭代过程:Waitress 获取allMenus的迭代器,如果目标元素是一个Menu ,则获取他的子菜单的迭代器CompositeIterator类型对象,它包装了这个子菜单的一个队列列表;如果目标元素是一个MenuItem对象,则直接返回着个对象,并且打印。上面两种情况都需要直接返回迭代的对象,四人组大佬已经在书中说得很明白了,这里我也就不再多说废话。
关键地方:我们在迭代器遍历到第二层的时候(allMenu创建的迭代器是第零层,依次第一层,第二层……),按照文中示例,下面是我的示意图:
图1简单介绍一下,其中stack1(图片中少打了一个1)第一层中只有一个元素,(index代表着这个栈的下标,栈底—>栈顶,从小到大)allMenus.createIterator()创建的是一个CompositeIterator迭代器,而stack1就是该迭代器中的stack栈。该stack中栈顶是一个allMenus.list.iterator的迭代列表,其中的size是对应位置迭代器的对象数量,cusor表示的是当前迭代位置(该游标从1开始,0表示没有开始迭代),而下面的[0]…代表这个迭代器中的对象。只要你简单跟踪过书中例子的执行过程,应该能够理解我所说的话。
可以看出,图1已经迭代到了第二个元素 [[1]]dinerMenu(图2),接下来看下图。
在图2中,程序调用了dinerMenu.createIterator,创建了一个CompositeIterator迭代器,并且加入了stack中的顶部,我们可以看出多了一个stack2,这个stack2就是调用diner.createIterator创建的迭代器中的stack栈,这里命名为stack2,你可以简单的理解为stack2就是dinerMenu.createIterator迭代器中的stack栈,这里分解出来易于理解。stack2栈顶元素是dinerMenu.list.iterator(这是List列表自己的迭代,看过书中的内容应该可以理解)
继续往下走,stack1继续遍历元素,执行next,如下图。
由代码可以的知道,这时候程序会传递给diner.createIterator的next(请记住diner.createIterator是一个CompositeIterator对象),此时调用获取到dinerMenu.list.iterator迭代器,并且执行next,将会返回一个对象,由代码可以知这是一个dessertMenu,他是一个菜单而不是菜单项,所以此时程序会调用dessertMenu.createItreator,并将该方法返回的对象put到stack2的栈顶位置。
我们继续下一步,请看下图。
现在已经把dessertMenu.createIterator创建的对象放入了stack2的栈顶,我们发现在stack1的index1处的size变成了2,为什么呢?这里再次强调一下,这里的stack2就是dinerMenu.createItreator这个迭代器中的stack栈,而下方的描述信息(例如:size、cusor描述的是该CompositeIterator迭代器下的stack栈中的情况,[1]…..就是栈中的元素)描述的是CompositeIterator迭代器所持有的stack栈中的情况(注意:list用的自己的迭代器而不不是CompositeIterator迭代器这里另当别论,但同样也描述了该list迭代器中的元素列表信息),所以这里的stack2中元素个数改变了,那么对应的迭代器描述信息也发生了变化。
不仅如此,这次操作也把dessertMenu作为返回值返回给了上一级操作,程序回到stack1的层级,通过判断,发现dessertMenu是一个Menu,直接调用了dessertMenu.createItreator(),创建了迭代器对象放在stack1的栈顶。
通过上面的一系列程序跟踪,紧接着下图。
我们再次通过调用peek获取迭代器,使用next()获取元素进行打印,我们就会发现这里的dessertMenu.list.iterator出现了两次,对应的下面的元素就会打印两次(由test测试程序可以看出它下面是一个MenuItem,直接打印了两次)。
让我们来做一下问题总结
通过程序跟踪步骤,我们发现问题出现了,由于使用object.createIterator来创建迭代器并且把它放入stack中会造成二次调用的问题(如果是很多层,调用次数就会成倍增长)。
4、如何解决问题呢?
通过深入理解问题我们已经发现了问题所在,解决问题的方法也是在博客找到的,这里就介绍一种比较简单的解决方案,一句话概括:
如果该菜单存在子菜单/子菜单项,我们通过获取它包含的List成员的迭代器,将它直接push到stack。
我们只需要在接口中声明一个获取List成员的接口,Menu子类实现它:
@Override
public List getMenuComponentList() {
return menuComponentList;
}
同时只需要修改代码(这段代码位于CompositeIterator实现类中),就可以解决问题。
@Override
public Object next() {
if(hasNext()) {
Iterator iterator = (Iterator) stack.peek();
MenuComponent menuComponent = (MenuComponent) iterator.next();
if (menuComponent instanceof Menu) {
//stack.push(menuComponent.createIterator());
//新代码就这一句
stack.push(menuComponent.getMenuComponentList().iterator());
}
return menuComponent;
} else
return null;
}
为什么这样就可以解决问题呢?其实可以很容易的发现,修改代码之后我们每一次遇到了包含子菜单的对象,都只获取该子菜单列表的迭代器并且将它放在了stack1的栈顶,所有的操作都只会在stack1中进行,并不涉及到stack的儿子的儿子…这此方法不会出现原有代码所出现的问题,该方法即简单也实用!
5、总结
该问题是在阅读《HeadFirst设计模式》中迭代器和组合模式时遇到的一个问题,当时觉得特别烧脑,一直纠结为什么在最终的结果中子菜单项打印了两次,几经思考无解的情况下求助互联网,接着在csdn中发现了关于这个问题出现原因以及解决方案的博客文章,但由于自己无法理解作者所要表达的思路(可能作者描述得过于简单,附:此文链接),所以也只能自己摸索这个问题所发生的原因以及过程,最终用一种相对简单思路去认识这些问题并且寻找解决方案(至少我自己知道……),在此记录下整个过程。(内心OS:大脑别打我,我只是在这里做个记录………..)