设计模式(九)迭代器模式与组合模式
迭代器与组合模式
面对不同的问题,自然会用到不同的数据结构,甚至相同的问题也可以用不同的数据结构来实现。比如数组和ArrayList都可以构造一个列表。
但当想获得集合内的元素时,直接的取用就会涉及到集合内部的具体实现,而这些往往不是我们希望暴露给外部的。因此,可以为集合设计迭代器来遍历集合内元素,随后只需向外部提供迭代器,就可以隐藏内部的实现。
迭代器模式
考虑两个餐厅的菜单的合并,其中一个使用数组保存菜单,而另一个使用ArrayList保存菜单。而两个餐厅都不想修改自己的数据结构,因为会涉及大量的代码改动(这同样适用于现实中的其他问题)。
package collection;
import java.util.Iterator;
public class DinerMenu {
static final int MAX_ITEMS = 6;
int numberOfItems = 0;
MenuItem[] menuItems;
public DinerMenu() {
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 description,
boolean vegetarian,
double price) {
MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
if (numberOfItems >= MAX_ITEMS) {
System.err.println("Sorry, menu is full! Can't add item to menu!");
} else {
menuItems[numberOfItems] = menuItem;
numberOfItems++;
}
}
public MenuItem[] getMenuItems() {
return getMenuItems;
}
}
package collection;
import java.util.ArrayList;
import java.util.Iterator;
public class PancakeHouseMenu {
ArrayList menuItems;
public PancakeHouseMenu() {
menuItems = new ArrayList();
addItem("K&B's Pancake Breakfast",
"Pancakes with scrambled eggs, and toast",
true,
2.99);
addItem("Regular Pancake Breakfast",
"Pancakes with fried eggs, sausage",
true,
2.99);
addItem("Blueberry Pancake Breakfast",
"Pancakes 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 description,
boolean vegetarian,
double price) {
MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
menuItems.add(menuItem);
}
public ArrayList getMenuItems() {
return menuItems;
}
}
如果直接地获取两个餐厅的菜单,就需要用两个循环,在每个循环中针对其中一个餐厅的内部实现来编程。这会造成大量的重复代码。并且,如果某个餐厅改变的内部实现,每个关于其的循环内的代码都需要修改。
为了不暴露内部实现,应该设计迭代器来遍历集合。
package collection;
import java.util.Iterator;
public class DinerMenuIterator implements Iterator {
MenuItem[] items;
int position = 0;
public DinerMenuIterator(MenuItem[] items) {
this.items = items;
}
@Override
public Object next() {
MenuItem menuItem = items[position];
position++;
return menuItem;
}
@Override
public boolean hasNext() {
if (position >= items.length || items[position] == null) {
return false;
} else {
return true;
}
}
}
package collection;
import java.util.ArrayList;
import java.util.Iterator;
public class PancakeHouseIterator implements Iterator {
ArrayList items;
int position = 0;
public PancakeHouseIterator(ArrayList items) {
this.items = items;
}
@Override
public Object next() {
MenuItem menuItem = (MenuItem) items.get(position);
position++;
return menuItem;
}
@Override
public boolean hasNext() {
if (position >= items.size()
|| items.get(position) == null) {
return false;
} else {
return true;
}
}
}
现在可以移除两个餐厅类中直接获取内部集合的方法,用获取迭代器的方法代替。
但实际上,Java中已经提供Iterator接口,因此对于已经存在可用迭代的ArrayList,不必特地为其设计迭代器。而为了满足删除菜单中元素的要求,还需要在数组实现的餐厅的迭代器中实现remove方法。
@Override
public void remove() {
if (position <= 0) {
throw new IllegalStateException(
"You can't remove an item until you've done at least one next()"
);
}
if (items[position-1] != null) {
for (int i = position-1; i < items.length-1; i++) {
items[i] = items[i+1];
}
items[items.length-1] = null;
}
}
现在显示菜单的方法仍然是针对具体的餐厅菜单类实现的,需要设计一个菜单接口类来抽象它们。
package collection;
import java.util.Iterator;
public class Waiter {
Menu pancakeHouseMenu;
Menu dinerMenu;
public Waiter(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);
}
private void printMenu(Iterator iterator) {
while (iterator.hasNext()) {
MenuItem menuItem = (MenuItem) iterator.next();
System.out.print(menuItem.getName() + ", ");
System.out.print(menuItem.getPrice() + " -- ");
System.out.println(menuItem.getDescription());
}
}
}
现在我们成功地把具体的实现和调用的方法分开了,显示菜单的方法不关注餐厅菜单的具体数据结构。
设计原则
单一责任原则:一个类应该只有一个引起变化的原因。
当一个类具有多个改变的原因时,在未来修改该类的代码的概率就会很高,维护成本高。
针对这种问题,应该尽量让每个类保持单一的责任,控制改变的原因。
内聚是一个比单一责任原则更普遍的的概念,当一个模块或一个类被设计成只支持一组相关的功能时,我们说它具有高内聚;反之,说它具有低内聚。高内聚的类更加容易维护。
组合模式
使用迭代器模式的餐厅合并已经有了不错的效率,如果有更多的餐厅想要加入,为了方便,也可以用ArrayList来保存不同餐厅的菜单,然后在先显示菜单的方法中遍历这些菜单即可。
但这样的结构仍然缺乏弹性:无法处理嵌套的子菜单。
因此必须使用树的结构来嵌套菜单。这种层次结构能让客户以一致的方式处理个别对象以及对象组合,称作组合模式。
通过组合模式,对象的组合和个别对象之间的差别就被忽略了。
package collection;
public abstract class MenuComponent {
public void add(MenuComponent menuComponent) {
throw new UnsupportedOperationException();
}
public void remove(MenuComponent menuComponent) {
throw new UnsupportedOperationException();
}
public MenuComponent getChild(int i) {
throw new UnsupportedOperationException();
}
public String getName() {
throw new UnsupportedOperationException();
}
public String getDescription() {
throw new UnsupportedOperationException();
}
public double getPrice() {
throw new UnsupportedOperationException();
}
public boolean isVegetarian() {
throw new UnsupportedOperationException();
}
public void print() {
throw new UnsupportedOperationException();
}
}
首先编写一个树结点的抽象类,该类提供了叶节点和组合节点需要实现的方法。
然后修改MenuItem类,令其继承MenuComponent类,作为叶节点,并额外实现print方法。
@Override
public void print() {
System.out.print(" " + getName());
if (isVegetarian()) {
System.out.print("(v)");
}
System.out.println(", " + getPrice());
System.out.println(" -- " + getDescription());
}
修改Menu类,令其继承MenuComponent类,作为组合节点,并额外实现print方法和getChild方法以及add、remove等方法。此时Menu类不再是接口,而是一种组合节点类。
package collection;
import java.util.ArrayList;
import java.util.Iterator;
public class Menu extends MenuComponent {
ArrayList menuComponents = new ArrayList();
String name;
String description;
public Menu(String name, String description) {
this.name = name;
this.description = description;
}
@Override
public void add(MenuComponent menuComponent) {
menuComponents.add(menuComponent);
}
@Override
public void remove(MenuComponent menuComponent) {
menuComponents.remove(menuComponent);
}
@Override
public MenuComponent getChild(int i) {
return (MenuComponent) menuComponents.get(i);
}
@Override
public String getName() {
return name;
}
@Override
public String getDescription() {
return description;
}
@Override
public void print() {
System.out.print("\n" + getName());
System.out.println(", " + getDescription());
System.out.println("---------------------");
Iterator iterator = menuComponents.iterator();
while (iterator.hasNext()) {
MenuComponent menuComponent = (MenuComponent) iterator.next();
menuComponent.print();
}
}
}
现在实现Waiter类十分简单,只需要保存一个根节点,然后调用根节点的print即可。根节点在打印的自己的过程中会递归地调用子节点的print方法。注意,此时我们不需要原来的餐厅菜单类了,抽取其中的餐品信息即可。
package collection;
public class Waiter {
MenuComponent allMenus;
public Waiter(MenuComponent allMenus) {
this.allMenus = allMenus;
}
public void printMenu() {
allMenus.print();
}
}
虽然组合模式在管理层次的同时也执行了菜单的操作,但换取得到了透明性。现在用户可以将组合节点和叶节点视作相同的节点。
组合迭代器
如果不想每次都输出菜单中所有的内容,而是有条件地筛选,就需要使用迭代器来遍历,组合模式也很好地兼容了迭代器的使用。在组合节点的迭代器中递归地使用子节点的迭代器即可。
package collection;
import java.util.Iterator;
import java.util.Stack;
public class CompositeIterator implements Iterator {
Stack stack = new Stack();
public CompositeIterator(Iterator iterator) {
stack.push(iterator);
}
@Override
public Object next() {
if (hasNext()) {
Iterator iterator = (Iterator) stack.peek();
MenuComponent component = (MenuComponent) iterator.next();
if (component instanceof Menu) {
stack.push(component.createIterator());
}
return component;
} else {
return null;
}
}
@Override
public boolean hasNext() {
if (stack.empty()) {
return false;
} else {
Iterator iterator = (Iterator) stack.peek();
if (!iterator.hasNext()) {
stack.pop();
return hasNext();
} else {
return true;
}
}
}
}
组合节点的迭代器要使用上面这种新类型。这种迭代器深度优先地遍历组合节点内的所有元素。
package collection;
import java.util.Iterator;
public class NullIterator implements Iterator {
@Override
public Object next() {
return null;
}
@Override
public boolean hasNext() {
return false;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
叶节点的迭代器使用上面这种空迭代器,这是为了避免用户需要判断是否真正获取了迭代器。
现在我们来用迭代器显示素食菜单。
public void printVegetarianMenu() {
Iterator iterator = allMenus.createIterator();
System.out.println("\nVEGETARIAN MENU\n----");
while (iterator.hasNext()) {
MenuComponent menuComponent = (MenuComponent) iterator.next();
try {
if (menuComponent.isVegetarian()) {
menuComponent.print();
}
} catch (UnsupportedOperationException e) {}
}
}

浙公网安备 33010602011771号