良好的管理集合
有许多种方法可以把对象堆起来成为一个集合。你可以把它们放进数据、堆栈、列表或者是散列表中,每一种都有它自己的优点和适合的使用时机。
举个例子
加入现在有两个餐厅,披萨餐厅和煎饼餐厅,它们合并了,他们的菜单就需要合并在一起。
披萨餐厅是用ArrayList对象去存放餐单信息,而煎饼餐厅使用数组。他们两家餐厅都同意让菜单对象实现MenuItem。让我们看看具体的实现:
package cn.net.bysoft.iterator;
// 菜单对象,保存了菜单的信息
public class MenuItem {
public MenuItem(String name, String description, boolean vegetarian, double price) {
super();
this.name = name;
this.description = description;
this.vegetarian = vegetarian;
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public boolean isVegetarian() {
return vegetarian;
}
public void setVegetarian(boolean vegetarian) {
this.vegetarian = vegetarian;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
private String name;
private String description;
private boolean vegetarian;
private double price;
}
import java.util.ArrayList;
// 煎饼餐厅对象,用ArrayList保存了菜单。
public class PancakeHouseMenu {
ArrayList<MenuItem> menuItems;
public PancakeHouseMenu() {
menuItems = new ArrayList<MenuItem>();
addItem("煎饼1号", "牛肉煎饼", false, 2.99);
addItem("煎饼2号", "素食煎饼", true, 1.49);
}
public void addItem(String name, String description, boolean vegetarian, double price) {
MenuItem menu = new MenuItem(name, description, vegetarian, price);
menuItems.add(menu);
}
public ArrayList<MenuItem> getMenuItems() {
return menuItems;
}
}
// 披萨餐厅对象,用数据保存了菜单信息。
public class PizzaHouseMenu {
static final int MAX_ITEMS = 2;
int numberOfItems = 0;
MenuItem[] menuItems;
public PizzaHouseMenu() {
menuItems = new MenuItem[MAX_ITEMS];
}
public void addItem(String name, String description, boolean vegetarian, double price) {
MenuItem menu = new MenuItem(name, description, vegetarian, price);
if (numberOfItems >= MAX_ITEMS)
System.out.println("对不起,菜单数量已满");
else
menuItems[numberOfItems++] = menu;
}
public MenuItem[] getMenuItems() {
return menuItems;
}
}
想了解为什么有两种不同的菜单表现方式会让事情变得复杂化,让我们试着实现一个同事使用者两个菜单的客户代码。创建一个服务员对象。
- printMenu(); 打印出菜单上的每一项
- printBreakfastMenu(); 只打印早餐
- printLunchMenu(); 只打印午餐
- printVegetarianMenu(); 打印所有的素食菜单
- isItemVegetarian(name); 查询指定的菜品是否是素食
先从实现printMenu()方法开始:
import java.util.ArrayList;
public class Client {
public static void main(String[] args) {
// 先获得煎饼餐厅的菜单集合
PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
ArrayList<MenuItem> menusOfPancake = pancakeHouseMenu.getMenuItems();
// 在获得披萨餐厅的菜单数组
PizzaHouseMenu pizzaHouseMenu = new PizzaHouseMenu();
MenuItem[] menusOfPizza = pizzaHouseMenu.getMenuItems();
for (int i = 0; i < menusOfPancake.size(); i++) {
MenuItem menu = menusOfPancake.get(i);
System.out.print(menu.getName() + ",价格:");
System.out.print(menu.getPrice() + ",");
System.out.print(menu.getDescription() + "\n");
}
System.out.println();
for (int i = 0; i < menusOfPizza.length; i++) {
MenuItem menu = menusOfPizza[i];
System.out.print(menu.getName() + ",价格:");
System.out.print(menu.getPrice() + ",");
System.out.print(menu.getDescription() + "\n");
}
}
}
打印每份菜单上的所有项,必须调用getMenuItem()方法,来取得他们各自的菜单。上面的例子中两者的返回类型是不同的。
接着,想打印出菜单需要将集合和数组循环并一一列出菜单。
最后,我们总是需要处理这两个菜单的遍历,如果还有第三家餐厅以不同的方式实现菜单集合,我们就需要有第三个循环。
可以封装遍历吗?
可以封装变化的部分。很明显,这里发生的变化是:由不同的集合类型所造成的遍历。但是,这能够被封装吗?让我们来看看这个想法……
- 要便利煎饼餐厅,我们需要使用ArrayList的size()和get()方法;
- 要便利披萨餐厅,我们需要使用数组的length字段和在中括号中输入索引;
- 现在我们创建一个对象,将它称为迭代器(Iterator),利用它来封装“遍历集合内的每个对象的过程”;
想要在餐厅菜单中加入一个迭代器,我们需要先定义迭代器接口,然后为披萨餐厅创建一个迭代器类:
public interface Iterator {
boolean hasNext();
Object next();
}
public class PizzaIterator implements Iterator {
MenuItem[] items;
int position = 0;
public PizzaIterator(MenuItem[] items) {
this.items = items;
}
// 判断数组下一个索引是否还有元素
public boolean hasNext() {
if(position >= items.length || items[position] == null)
return false;
else return true;
}
// 获得当前索引位置的元素
public Object next() {
MenuItem item = items[position++];
return item;
}
}
import java.util.ArrayList;
public class PancakeIterator implements Iterator {
ArrayList<MenuItem> items;
int position = 0;
public PancakeIterator(ArrayList<MenuItem> items) {
this.items = items;
}
// 判断数组下一个索引是否还有元素
public boolean hasNext() {
if(position >= items.size() || items.get(position) == null)
return false;
else return true;
}
// 获得当前索引位置的元素
public Object next() {
MenuItem item = items.get(position);
return item;
}
}
创建好迭代器后,改写披萨餐厅的代码,创建一个PizzaMenuIterator,并返回给客户:
public class PizzaHouseMenu {
static final int MAX_ITEMS = 2;
int numberOfItems = 0;
MenuItem[] menuItems;
public PizzaHouseMenu() {
menuItems = new MenuItem[MAX_ITEMS];
addItem("披萨1号", "素食披萨", true, 4.99);
addItem("披萨2号", "海鲜蛤蜊披萨", true, 5.99);
}
public void addItem(String name, String description, boolean vegetarian, double price) {
MenuItem menu = new MenuItem(name, description, vegetarian, price);
if (numberOfItems >= MAX_ITEMS)
System.out.println("对不起,菜单数量已满");
else
menuItems[numberOfItems++] = menu;
}
public Iterator createIterator() {
return new PizzaIterator(menuItems);
}
}
import java.util.ArrayList;
public class PancakeHouseMenu {
ArrayList<MenuItem> menuItems;
public PancakeHouseMenu() {
menuItems = new ArrayList<MenuItem>();
addItem("煎饼1号", "牛肉煎饼", false, 2.99);
addItem("煎饼2号", "素食煎饼", true, 1.49);
}
public void addItem(String name, String description, boolean vegetarian, double price) {
MenuItem menu = new MenuItem(name, description, vegetarian, price);
menuItems.add(menu);
}
public Iterator createIterator() {
return new PancakeIterator(menuItems);
}
}
我们不再需要getMenuItems()方法,而是用createIterator()方法代替,用来从菜单项数组创建一个迭代器,并把他返回给客户,返回迭代器接口。客户不需要知道餐厅菜单使如何实现维护的,也不需要知道迭代器是如何实现的。客户只需直接使用这个迭代器遍历菜单即可。下面修改一下客户类的调用:
public class Waitress {
PancakeHouseMenu pancake;
PizzaHouseMenu pizza;
public Waitress(PancakeHouseMenu pancake, PizzaHouseMenu pizza) {
this.pancake = pancake;
this.pizza = pizza;
}
public void printMenu() {
Iterator pizzaIterator = pizza.createIterator();
printMenu(pizzaIterator);
Iterator pancakeIterator = pancake.createIterator();
printMenu(pancakeIterator);
}
private void printMenu(Iterator iterator) {
while(iterator.hasNext()) {
MenuItem menu = (MenuItem)iterator.next();
System.out.print(menu.getName() + ",价格:");
System.out.print(menu.getPrice() + ",");
System.out.print(menu.getDescription() + "\n");
}
}
}
public class Client {
public static void main(String[] args) {
PancakeHouseMenu pancake = new PancakeHouseMenu();
PizzaHouseMenu pizza = new PizzaHouseMenu();
Waitress waitress = new Waitress(pancake, pizza);
waitress.printMenu();
}
}
到目前为止,我们将客户调用与餐厅的菜单数据接口解耦了,客户调用再也不用为每一个不同数据结构的菜单编写一套遍历的代码了。
我们有些什么……
我们现在使用一个共同的迭代器接口(Iteraotr)实现了两个具体类(PizzaIterator和PancakeIterator)。这两个具体类都实现了各自的hasNext()方法和next()方法。
然后再PancakeHouseMenu和PizzaHouseMenu两个类中,创建一个createIterator()方法返回各自的迭代器,在Waitress类中,使用这两个餐厅对象返回的迭代器打印菜单。这时Waitress类和Client类再也不需要关心存放菜单的数据结构,之关心能从迭代器中获得菜单就好。
迭代器模式给你提供了一种方法,可以顺序访问一个聚集对象的元素,而又不用知道内部是如何表示的。你已经在前面的两个菜单实现中看到了这一点。在设计中使用迭代器的影响是明显的:如果你有一个统一的方法访问聚合中的每一个对象,你就可以编写多态的代码和这些聚合搭配,使用如同前面的printMenu()方法一样,只要有了迭代器这个方法根本不用管菜单究竟是由数组还是集合或者其他的数据结构来保存的。
另外一个对你的设计造成重要影响的,是迭代器模式把在元素之间游走的责任交给迭代器,而不是聚合对象。这不仅让聚合的接口和实现变得更简洁,也可以让聚合更专注它所应该专注的事情上面,而不必去理会遍历的事情。
让我们检查类图,将来龙去脉拼凑出来……
先看看Aggregate接口,有一个共同的接口提供所有的聚合使用,这对客户代码是很方便的,将客户代码从集合对象的实现解耦。
接下来看看ConcreteAggregate类,这个具体聚合持有一个对象的集合,并实现一个方法,利用此方法返回集合的迭代器。每一个具体聚合都要负责实例化一个具体的迭代器,次迭代器能够便利对象集合。
接下来是Iterator接口,这是所有迭代器都必须实现的接口,它包含一些方法,利用这些方法可以在集合元素之间游走。你可以自己设计或者使用java.util.Iterator接口。
最后是具体的迭代器,负责遍历集合。
单一职责
如果我们允许我们的聚合实现他们内部的集合,以及相关的操作和遍历的方法,又会如何?我们已经知道这回增加聚合中的方法个数,但又怎么样呢?为什么这么做不好?
想知道为什么,首选需要认清楚,当我们允许一个类不但要完成自己的事情,还同时要负担更多的责任时,我们就给这个类两个变化的原因。如果这个集合变化的话,这个类也必须要改变,如果我们遍历的方式改变的话,这个类也必须跟着改变。所以,引出了设计原则的中心:
单一职责
一个类应该只有一个引起变化的原因。
类的每个责任都有改变的潜在区域。超过一个责任,意味着超过一个改变区域。这个原则告诉我们,尽量让每一个类保持单一责任。
内聚(cohesion)这个术语你应该听过,它用来度量一个类或者模块紧密地达到单一目的或责任。
当一个模块或一个类被设计成只支持一组相关的功能时,我们说它具有高内聚;反之,当被设计成支持一组不相关的功能时,我们说它具有低内聚。
内聚是一个比单一职责更普遍的概念,但两者其实关系是很密切的。遵守这个原则的类更容易有很高的凝聚力,而且比背负许多职责的低内聚类更容易维护。
以上就是迭代器模式的一些内容。