你将在本章中学习如何能让客户遍历你的对象而又无法窥视你存储对象的方式,也将学习如何创建一些对象超集合(super collection),能够一口气跳过某些让人望而生畏的数据结构,你还将学习一些关于对象职责的知识。
题例:餐厅和煎饼屋合并了
虽然可以在一个地方同时想用煎饼屋的早餐和餐厅的午餐,但是煎饼屋的菜单用用的ArrayList记录菜单的,而餐厅用的是数组,而两家餐馆都不愿意修改自己的实现。毕竟有很多代码依赖它们。
检查菜单项
至少两家餐馆都统一实现了MenuItem:
class MenuIten {
String name;
String description;
String vegetarian;
double price;
public MenuIten(String name,
double price,
String vegetarian,
String description) {
this.name = name;
this.price = price;
this.vegetarian = vegetarian;
this.description = description;
}
public String getName() {
return name;
}
public double getPrice() {
return price;
}
public String getVegetarian() {
return vegetarian;
}
public String getDescription() {
return description;
}
}
两家餐馆的菜单实现
煎饼屋:用的是ArrayList。
class PancakeHouseMenu {
ArrayList menuItems;
public PancakeHouseMenu() {
menuItems = new ArrayList();
//这里是添加菜单的地方
addItem("",0,"","");
}
public void addItem(String name,
double price,
String vegetarian,
String description) {
MenuIten menuIten = new MenuIten(name, price, vegetarian, description);
menuItems.add(menuIten);
}
public ArrayList getMenuItems(){
return menuItems;
}
//这里还有菜单的其他方法,所以不希望改菜单
}
餐厅:用的是数组
class DinerMenu {
static final int MAX_ITEMS = 6;
int numberOfItems = 0;
MenuIten[] menuItens;
public DinerMenu() {
menuItens = new MenuIten[MAX_ITEMS];
}
public void addItem(String name,
double price,
String vegetarian,
String description) {
MenuIten menuIten = new MenuIten(name, price, vegetarian, description);
if (numberOfItems >= MAX_ITEMS) {
System.out.println("Sorry,menu is full!Can't add item to menu");
} else {
menuItens[numberOfItems] = menuIten;
numberOfItems = numberOfItems + 1;
}
}
public MenuIten[] getMenuItens(){
return menuItens;
}
//菜单的其他方法
}
有两种不同的菜单表现方式,这会带来什么问题?
假设你是一个女招待下面是你做的事,你会怎么办?
1:打印出菜单上的每一项
2:只打印早餐项
3:只打印午餐项
4:打印所有的素食菜单项
5:指定项的名称,如果该项是素食的话,返回true,否则返回false
1:打印没分菜单上的所有项,必须调用PancakeHouseMenu和DinerMenu的getMenuItenm()方法,来取得它们各自的菜单项,两者返回类型是不一样的。
PancakeHouseMenu pancakeHouseMenu=new PancakeHouseMenu();
ArrayList breakfastItems=pancakeHouseMenu.getMenuItems();
DinerMenu dinerMenu=new DinerMenu();
MenuIten[] linchItenms=dinerMenu.getMenuItens();
2:现在,想要打印PancakeHouseMenu的项,我们用循环将早餐ArrayList内的项一一列出来。想要打印DinerMenu的项目,我们用循环数组内的项一一列出来。
for(int i=0;i<breakfastItems.size();i++){
MenuIten menuIten= (MenuIten) breakfastItems.get(i);
System.out.println(menuIten.getName()+" ");
System.out.println(menuIten.getPrice()+" ");
System.out.println(menuIten.getDescription()+" ");
}
for(int i=0;i<linchItenms.length;i++){
MenuIten menuIten= linchItenms[i];
System.out.println(menuIten.getName()+" ");
System.out.println(menuIten.getPrice()+" ");
System.out.println(menuIten.getDescription()+" ");
}
3:实现女招待的其他方法,做法都和上面的方法相类似,我们总是需要处理两个菜单,并且用两个循环遍历这些项,如果还有第三家餐厅以不同的实现出现,我们就需要有三个循环。
可以封装遍历吗?
1:要遍历早餐项,我们需要使用ArrayList的size()和get()方法。
2:要遍历午餐项,我们需要使用数组的length字段和中括号的i.
3:现在我们创建一个对象,将它称为迭代器(Iterator),利用它来封装“遍历集合内的每个对象的过程”。先让我们来试试:
Iterator iterator=breakfastMenu.createIterator();//获取菜单项迭代器
while(iterator.hasNext()){
MenuItem menuItem=(MenuItem)iterator.next();
}
会见迭代器模式
我们队封装已经奏效了,这正是迭代器模式。你所需要知道的第一件事情,就是它依赖于一个称为迭代器的接口,这是一个可能的迭代器接口:
interface Iterator{
hasNext();
next();
}
一旦我们有了这个接口,就可以为给种对象集合实现迭代器:数组、列表、散列表。
在餐厅菜单中加入一个迭代器
定义迭代器接口
interface Iterator_{
boolean hasNext();
Object next();
}
餐厅:
//实现迭代器接口
class DinerMenuItertor implements Iterator{
MenuIten[] items;
//position记录当前数组遍历的位置。
int position=0;
//构造器需要被传入一个菜单项的数组当做参数。
public DinerMenuItertor(MenuIten[] items){
this.items=items;
}
/*
检查我们是否已经取得数组内所有的元素,如果还有元素待遍历,则返回true
因为使用的是固定长度的数组,所以我们不但要检查是都超出了数组长度也必须检查是否下一项是null,
如果是null,就表示没有其他项了。
*/
@Override
public boolean hasNext() {
if(position>=items.lengthitems[position]==null){
return false;
}else{
return true;
}
}
//返回数组的下一项,并递增其位置。
@Override
public Object next() {
MenuIten menuIten=items[position];
position=position+1;
return menuIten;
}
}
餐厅菜单:
class DinerMenu {
static final int MAX_ITEMS = 6;
int numberOfItems = 0;
MenuIten[] menuItens;
//这里是构造方法
//addItem在这里
//这里我们就不需要了
// public MenuIten[] getMenuItens() {
// return menuItens;
// }
public Iterator createITERATOR(){
return new DinerMenuItertor(menuItens);
}
//菜单的其他方法
}
这样再去写女招待的代码就不会再重复两遍了。
到目前为止,我们做了些什么?
做一些改良
首先我们采用java的迭代器接口,而且ArrayList本身就有返回返回而迭代器的方法,所以我们就不用再写了,只需修改数组的就好。
class DinerMenuIterator implements Iterator{
MenuItem[] items;
int position=0;
public DinerMenuIterator (MenuItem[] items){
this.ietms=items;
}
public Object next(){
//在这里实现
}
public boolean hasNext(){
//在这里实现
}
@Override
public void remove() {
if(position<=0){
throw new IllegalSignatureException("You can't remove ");
}
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;
}
}
然后提供一个公共的接口,然后稍微改一下女招待。
interfaceMenu{
public Iterator createrator();
}
然后让煎饼屋和餐厅的菜单类都实现Menu接口。
女招待可以利用接口(而不是具体类)引用每一个菜单对象,这样通过“针对接口编程,而不是针对实现编程”我们可以减少女招待和具体类之间的依赖。
定义迭代器模式
迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示
单一责任
设计原则:一个类只有一个引起变化的原因。
我们知道要避免类内的改变,因为修改diamante很容易造成许多潜在的错误,如果有一个类具有两个改变的原因,那么这会使得将来该类的变化机率上升,而当它真的改变时,你的设计中同时又两个方面将会受到影响。
正当我们任我这很安全的时候,现在他们希望能够加上一份餐后甜点“子菜单”。
如果我们能让甜点菜单变成餐厅菜单集合的一个元素,那该有多好,但是根据现在的实现,根本做不到。
我们需要做什么?
1:我们需要某种树形结构,可以容纳菜单、子菜单和菜单项。
2:我们需要确定能够在每个菜单的各个项目之间游走,而且至少要像现在用迭代器一样方便。
3:我们也需要能够更有弹性地在菜单项之间游走。比方说:可能只需要遍历甜点菜单,或者可以遍历餐厅的整个菜单。
定义组合模式
组合模式允许你将对象组合成树形节后来表现“整体/部分”层次结构。组合能让客户以一致的方式处理个别对象以及对象组合。
利用组合这几菜单
实现菜单组件
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 String getPrice(){
throw new UnsupportedOperationException();
}
public boolean isVegetarian(){
throw new UnsupportedOperationException();
}
public void print(){
throw new UnsupportedOperationException();
}
}
实现菜单项
class MenuIten_ extends MenuComponent{
String name;
String description;
boolean vegetarian;
double price;
public MenuIten_ (String name,
double price,
boolean vegetarian,
String description) {
this.name = name;
this.price = price;
this.vegetarian = vegetarian;
this.description = description;
}
@Override
public double getPrice() {
return price;
}
@Override
public String getDescription() {
return description;
}
@Override
public String getName() {
return name;
}
@Override
public boolean isVegetarian() {
return vegetarian;
}
@Override
public void print() {
System.out.println(" "+getName());
if(isVegetarian()){
System.out.println("(v)");
}
System.out.println(" "+getPrice());
System.out.println(" --"+getDescription());
}
}
实现组合菜单
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 MenuComponent getChild(int i) {
return (MenuComponent) menuComponents.get(i);
}
@Override
public void remove(MenuComponent menuComponent) {
menuComponents.remove(menuComponent);
}
@Override
public String getName() {
return name;
}
@Override
public String getDescription() {
return description;
}
@Override
public void print() {
System.out.println(" "+getName());
System.out.println(" "+getDescription());
System.out.println("------------------");
}
}
菜单是一个组合,包含了菜单项和其他的菜单,所以它的print()应该打印出它包含的一切,所以上面组合菜单的print()方法是不对的。
正确的print()方法:
class Menu extends MenuComponent{
ArrayList menuComponents=new ArrayList();
String name;
String description;
//构造方法和其他方法
@Override
public void print() {
System.out.println(" "+getName());
System.out.println(" "+getDescription());
System.out.println("------------------");
Iterator iterator=menuComponents.iterator();
while (iterator.hasNext()){
MenuComponent menuComponent= (MenuComponent) iterator.next();
menuComponent.print();
}
}
}
不是说好的单一责任原则,组合模式不但要管理层次结构,而且还要执行菜单的操作。
组合模式以单一责任设计原则换取透明性(transparency)。什么是透明性?通过让组件的接口同时包含一些管理子节点和叶子节点的操作,客户就可以将组合和已转接点一直同仁,一个元素究竟是组合还是叶节点对客户是透明的。
闪回到迭代器
如果女招待需要,我们也能让她使用迭代器遍历整个组合,比方说,女招待可能想要游走整个菜单,挑选素食项。
想要事项一个组合迭代器,让我们为每个组件都加上createIterator()方法。
在MenuComponent中:
abstract class MenuComponent{
//其他方法
public Iterator createIterator(){
throw new UnsupportedOperationException();
}
}
在Menu 中:
class Menu extends MenuComponent{
//其他方法
@Override
public Iterator createIterator() {
return new CompositrIterator(menuComponents.iterator());
}
}
在MenuIten_ 中:
class MenuIten_ extends MenuComponent{
//其他方法
@Override
public Iterator createIterator() {
return new NullIterator();
}
}
组合迭代器
这个CmpositeIterator是一个不可小觑的迭代器,它的工作是遍历组件内的菜单项,而且确保所有的子菜单(以及子子菜单……)都被包括进来。
//跟所有的迭代器一样,我们实现Iterator接口。
class CompositeIterator implements Iterator {
Stack stack = new Stack();
/*
将我们要遍历的顶层组合的迭代器传入,我们把它抛进一个堆栈数据结构中
*/
public CompositeIterator(Iterator iterator) {
stack.push(iterator);
}
@Override
public boolean hasNext() {
//想要知道是否还有下一个元素,我们检查堆栈是否被清空,如果已经空了,就表示没有下一个元素了
if (stack.empty()) {
return false;
} else {
/*
否则我们就从堆栈的顶层中取出迭代器,看看是否还有下一个元素,
如果它没有元素,我们将它弹出堆栈,然后递归调用hasNext()。
*/
Iterator iterator = (Iterator) stack.peek();
if (!iterator.hasNext()) {
stack.pop();
return hasNext();
} else {
//否则,便是还有下一个元素
return true;
}
}
}
@Override
public Object next() {
//好了,当客户想要取得下一个元素时候,我们先调用hasNext()来确定时候还有下一个。
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 void remove() {
throw new UnsupportedOperationException();
}
}
在我们写MenuComponent类的print方法的时候,我们利用了一个迭代器遍历组件内的每个项,如果遇到的是菜单,我们就会递归地电泳print方法处理它,换句话说,MenuComponent是在“内部”自行处理遍历。
但是在上页的代码中,我们实现的是一个“外部”的迭代器,所以有许多需要追踪的事情。外部迭代器必须维护它在遍历中的位置,以便外部可和可以通过hasNext和next来驱动遍历。在这个例子中,我们的代码也必须维护组合递归结构的位置,这也就是为什么当我们在组合层次结构中上上下下时,使用堆栈来维护我们的位置。
空迭代器
菜单项没什么可以遍历的,那么我们要如何实现菜单项的createIterator()方法呢。
1:返回null。我们可以让createIterator()方法返回null,但是如果这么做,我们的客户代码就需要条件语句来判断返回值是否为null;
2:返回一个迭代器,而这个迭代器的hasNext()永远返回false。这个是更好的方案,客户不用再担心返回值是否为null。我们等于创建了一个迭代器,其作用是“没作用”。
class NullIterator implements Iterator{
@Override
public boolean hasNext() {
return false;
}
@Override
public Object next() {
return null;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}