设计模式——迭代器模式
俗话说:术业有专攻。作为一个容器,我只要负责存储对象就好嘛,迭代这种事本来就不该我管。
1.情景举例
最近程序员小C在公司项目中的一个模块。大体的需求就是自己通过封装通过数组实现一个容器类,并在其中放入特定的数据对象,然后暴露出相应的查询接口以便其他模块查询使用。
这种问题对于小C来说小菜一碟,他马上就完成了这个容器类。
public class MyContainer {
private String[] array = new String[7];
public MyContainer() {
for(int i = 0; i < 7; i++) {
array[i] = "幸运数" + i;
}
}
public String getNumber(int i) {
return array[i];
}
public String[] getArray() {
return array;
}
}
其中的getNumber()方法可以通过下标获取相应的值。
而且小C想:每个容器应该都有遍历自己的功能,然如果把遍历的结果直接放入方法返回是不行的,于是他决定设计一个方法得到具体的数组对象,然后由调用者自行决定遍历的方法。然后他设计了getArray()方法。
第二天小C高高兴兴的把设计好的类提交了上去。
但是在投入使用时,总是发生已经写入容器的特定的数据被更改的情况。团队用了很大的精力找到了错误,原来是有很多调用getArray()的代码在使用时不小心对核心数组的数据进行了修改。
团队花了一天的时间还没有把所有的类似的错误修改完毕,于是他们请高级工程师大D来帮忙。
大D看到小C设计的容器类,发现问题的根源是类设计的问题,他决定用迭代器模式重写MyContainer类。
2.使用迭代器模式
迭代器模式的核心就是把容器遍历的动作和容器本身的实现进行分离,给使用者提供一个新的且安全的遍历方法。
首先我们要实现迭代器的接口。
public interface Iterator<V> {
//判断是否还有数据等待遍历
public boolean hasNext();
//获得本次遍历指针指向的数据
public V next();
}
其中主要的方法有hasNext()和next()两个。
具体的实现如下。
public class MyContainer {
private String[] array = new String[7];
public MyContainer() {
for(int i = 0; i < 7; i++) {
array[i] = "幸运数" + i;
}
}
public String getNumber(int i) {
return array[i];
}
//通过该方法获取迭代器
public Iterator Iterator() {
return new MyIterator();
}
private class MyIterator implements Iterator<String>{
int count = 7;
@Override
public boolean hasNext() {
return count > 0 ? true : false;
}
@Override
public String next() {
return array[--count];
}
}
}
具体的实现类可以作为容器的内部类。因为迭代器可以看作是该容器的一个组件,而且迭代器没有迭代的对象单拿出去根本没有意义。
然后在hasNext()方法中通过count变量判断是否已经遍历到数组末尾。
在next()方法中返回遍历过程中的每个对象。
至此,我们已经将容器的实现和容器的遍历操作分类了。MyContainer类专注与容器的实现,内部类MyIterator专注于核心数据结构的遍历操作。
大D在完成了修改后,请其他想要遍历该容器的方法通过新的迭代器重新完成遍历操作。
public class MainTest {
public static void main(String[] args) {
MyContainer mc = new MyContainer();
//获得迭代器
Iterator i1 = mc.Iterator();
//遍历操作
while(i1.hasNext()) {
System.out.println(i1.next());
}
}
}
注:本文代码实现的是逆序遍历,顺序遍历只需更改迭代器的具体实现即可。
至此之后,再也没有发生过元数据篡改的错误,迭代器模式完美的解决了问题。
3.迭代器模式小结
传统的容器遍历,都是通过对容器核心结构(数组、链表等等)进行一个遍历。但是由于每个程序员对遍历的需求不同,无法用统一的遍历的结果满足他们。错误的方法就是想文中小C的做法,把核心数据结构直接暴露,这样可能造成其他开发者对核心数据结构的修改(当然他们不是有意的)。
迭代器模式通过实现一个特定的类去实现整个遍历的过程。把容器的实现和容器遍历的动作进行分离。达到既不暴露元数据,又可以灵活的得到遍历的结果,是最最最常用的设计模式,也就是因为这个模式实在太好了,太常用了,很多编程语言直接内置迭代器模式(例如Java的List实现类的Iterator,其中还支持对元数据的remove操作),反而让我们感觉迭代器模式用的很少。
至于我们为什么要通过一个接口来定义迭代器这个问题,当类似的容器的许许多多时,用一个抽象接口去规范所有迭代器的实现,既可以让迭代器的实现更加规整,更重要的是我们可以通过多态的方式去遍历不同的容器,这个这个容器的迭代器实现了抽象迭代器接口。在Java中的for(xx : y)这种循环方法实际就是在通过迭代器进行遍历,如果没有统一的抽象对所有迭代器进行规范,这个功能是无法实现的。