阅读 86

设计模式之迭代器模式

这是我参与8月更文挑战的第19天,活动详情查看:8月更文挑战

在阎宏博士的《JAVA与模式》一书中开头是这样描述迭代子(Iterator)模式的:迭代子模式又叫游标(Cursor)模式,是对象的行为模式。迭代子模式可以顺序地访问一个聚集中的元素而不必暴露聚集的内部表象(internal representation)。

迭代器模式的定义是通过提供一种方法顺序访问一个聚合对象中的各个元素,而又不必暴露该聚合对象中的内部表示。迭代器模式是针对聚合对象(数组、集合、链表)的“访问”而来。通过定义不同的遍历策略来遍历聚合对象。

应用场景

  1. 如果希望提供访问一个聚合对象的内容,但又不想暴露它的内部表示的时候可使用迭代器模式
  2. 如果希望有多种遍历方式可以访问聚合对象。
  3. 如果希望为遍历不同的聚合对象提供一个统一的接口,可以使用迭代器模式(多态迭代)。

UML图

IteratorPattern.png

迭代器模式中涉及的角色:

  • 抽象迭代器角色(Iterator):此抽象角色定义出遍历元素的接口方法;
  • 具体迭代器角色(ConcreteIterator):实现具体的迭代方法,继承或实现Iterator。
  • 聚集角色(Aggregate):定义聚集对象的抽象,并定义出具体的遍历接口返回Iterator类型
  • 具体聚集角色(ConcreteAggregate):实现了创建迭代子(Iterator)对象的接口,返回一个合适的具体迭代子实例。

案例演示

首先创建迭代器Iterator接口

/**
 * 定义迭代器角色
 * @author Iflytek_dsw
 *
 */
interface Iterator {
	/**
	 * 获取第一个元素
	 * @return
	 */
	String first();
	/**
	 * 获取最后一个元素
	 * @return
	 */
	String last();
	/**
	 * 判断是否有下一个元素
	 * @return
	 */
	boolean hasNext();
	/**
	 * 下一个元素
	 * @return
	 */
	String next();
}
复制代码

在Iterator接口中定义了聚集对象需要的遍历操作。

定义抽象聚集对象

/**
 * 聚集角色,定义聚集角色具备的接口
 * @author Iflytek_dsw
 *
 */
abstract class Aggregate {

	abstract Iterator iterator();
}
复制代码

定义具体聚集对象

class ConcreteAggregate extends Aggregate{
	
	private List<String> names;

	public ConcreteAggregate(List<String> names) {
		super();
		this.names = names;
	}

	@Override
	Iterator iterator() {
		return new AggregateIterator(this);
	}
	
	public String first(){
		return names == null ? null : names.get(0);
	}
	
	public String last(){
		return names == null ? null : names.get(names.size() -1);
	}
	
	public String next(int index){
		return names == null ? null : names.get(index);
	}
	
	/**
	 * 聚集中的元素个数
	 * @return
	 */
	public int size(){
		return names.size();
	}
}
复制代码

在上面的实例中,我们可以看到在具体聚集中,我们定义了与Iterator对象的方法,用来封装具体的操作。以便在Iterator的具体实例中进行调用,同时我们也封装了一个生成Iterator的方法。

具体迭代器

class AggregateIterator implements Iterator{
	private ConcreteAggregate concreteAggregate;
	private int index;
	public AggregateIterator(ConcreteAggregate concreteAggregate) {
		super();
		this.concreteAggregate = concreteAggregate;
		this.index = 0;
	}

	@Override
	public String first() {
		return concreteAggregate.first();
	}

	@Override
	public String last() {
		return concreteAggregate.last();
	}

	@Override
	public String next() {
		return concreteAggregate.next(index -1);
	}

	@Override
	public boolean hasNext() {
		if(index < concreteAggregate.size()){
			index++;
			return true;
		}
	    return false;
	}
}
复制代码

在具体迭代器中定义包含一个聚集对象的实例,即对这个聚集对象进行相应操作的访问。

客户端

public class Client {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		List<String> names = new ArrayList<>(Arrays.asList(new String[]{"James","Lucy","Jack"}));
		Aggregate aggregate = new ConcreteAggregate(names);
		Iterator iterator = aggregate.iterator();
		System.out.println("第一个元素是:" + iterator.first());
		System.out.println("最后一个元素是:" + iterator.last());
		
		while(iterator.hasNext()){
			System.out.println("遍历元素:" + iterator.next());
		}
	}
}
复制代码

在上面的客户端中,首先创建一个聚集类实例,然后调用iterator方法得到一个迭代器角色,得到迭代器角色后,我们就可以进行相关的遍历操作。

执行结果

第一个元素是:James
最后一个元素是:Jack
遍历元素:James
遍历元素:Lucy
遍历元素:Jack
复制代码

通过上面的实例演示,我们可以得到以下几点列:

  1. 迭代器的本质:控制访问聚合对象中的元素。迭代器能实现“无须暴露聚合对象的内部实现,就能够访问到聚合对象的各个元素的功能”,做到“透明”的访问聚合对象中的元素。注意迭代器能够即“透明”访问,又可以“控制访问”聚合对象。
  2. 迭代器的关键思想:把对聚合对象的遍历访问从聚合对象中分离出来,放入单独的迭代器中,这样聚合对象会变得简单,而且迭代器和聚合对象可以独立地变化和发展,提高系统的灵活性。
  3. 迭代器的动机:在软件构建过程中,集合对象内部结构常常变化各异。但对于这些集合对象,我们希望在不暴露其内部结构的同时,可以让外部客户代码透明地访问其中包含的元素;同时这种“透明遍历”也为“同一种算法在多种集合对象上进行操作”提供了可能。将遍历机制抽象为“迭代器对象”为“应对变化中的集合对象”提供了一种优雅的方法

在上面的例子中,既然聚集内部已经提供了相应的操作方法,那么我们还要使用迭代器模式呢?确实,客户端可以根据聚集提供的接口来进行遍历操作,但是,迭代器模式将迭代进行了抽象好,具有更好的扩展性,因为集合对象内部结构多变,使用迭代器模式可将客户端和聚集的责任分割开,使得两者可独立演变,迭代器模式作为中间层,可以吸收变化因素,避免客户端的修改。

内部迭代VS外部迭代

外部迭代器 指的是由客户端来控制迭代下一个元素的步骤,客户端会显式调用迭代器的next()等迭代方法,在遍历过程中向前进行。

内部迭代器 指的是由迭代器自己来控制迭代下一个元素的步骤。因此,如果想要在迭代的过程中完成工作的话,客户端就需要把操作传递给迭代器,迭代器在迭代的时候会在每个元素上执行这个操作,类似于JAVA的回调机制。

总体来说外部迭代器比内部迭代器要灵活一些,因此我们常见的实现多属于主动迭代子。

如果一个聚集的接口没有提供修改聚集元素的方法,这样的接口就是所谓的窄接口。

聚集对象为迭代器提供了一个宽接口,为其它对象提供窄接口来访问。即聚集对象对迭代器是适当公开的,以便迭代器对聚集对象有足够的了解,从而可以进行迭代操作。但是聚集对象应该避免向其它对象提供这些方法,其它对象应该通过迭代器完成这些操作,不能直接操作聚集对象。

在Java中,实现这种双重限制接口的办法就是通过内部类来实现,即将迭代器对象设置为聚集类的内部类,这样迭代器对象可以对聚集类有很好的访问,同时又限制了对外的操作。这种同时保证聚集对象的封装和迭代子功能的实现的方案叫做黑箱实现方案。而这种形式定义的迭代器,又被称为内部迭代器

如下面的例子所示:

public class NameRepository implements Container {
   public String names[] = {"Robert" , "John" ,"Julie" , "Lora"};

   @Override
   public Iterator getIterator() {
      return new NameIterator();
   }

   private class NameIterator implements Iterator {

      int index;

      @Override
      public boolean hasNext() {
         if(index < names.length){
            return true;
         }
         return false;
      }

      @Override
      public Object next() {
         if(this.hasNext()){
            return names[index++];
         }
         return null;
      }        
   }
}
复制代码

补充知识——静态迭代器和动态迭代器

静态迭代器 静态迭代子由聚集对象创建,并持有聚集对象的一份快照(snapshot),在产生后这个快照的内容就不再变化。客户端可以继续修改原聚集的内容,但是迭代子对象不会反映出聚集的新变化。

静态迭代子的好处是它的安全性和简易性,换言之,静态迭代子易于实现,不容易出现错误。但是由于静态迭代子将原聚集复制了一份,因此它的短处是对时间和内存资源的消耗。

动态迭代器 动态迭代子则与静态迭代子完全相反,在迭代子被产生之后,迭代子保持着对聚集元素的引用,因此,任何对原聚集内容的修改都会在迭代子对象上反映出来。

完整的动态迭代子不容易实现,但是简化的动态迭代子并不难实现。大多数JAVA设计师遇到的迭代子都是这种简化的动态迭代子。为了说明什么是简化的动态迭代子,首先需要介绍一个新的概念:Fail Fast

Fail Fast 如果一个算法开始之后,它的运算环境发生变化,使得算法无法进行必需的调整时,这个算法就应当立即发出故障信号。这就是Fail Fast的含义。

如果聚集对象的元素在一个动态迭代子的迭代过程中发生变化时,迭代过程会受到影响而变得不能自恰。这时候,迭代子就应当立即抛出一个异常。这种迭代子就是实现了Fail Fast功能的迭代子。

Fail Fast在JAVA聚集中的使用 JAVA语言以接口java.util.Iterator的方式支持迭代子模式,Collection接口要求提供iterator()方法,此方法在调用时返还一个Iterator类型的对象。而作为Collection接口的子类型,AbstractList类的内部成员类Itr便是实现Iterator接口的类。

优缺点

优点

  1. 更好的封装性,聚集对象不需要暴露内部实现即可实现访问;
  2. 每一个聚集对象都可以有一个或多个迭代子对象,每一个迭代子的迭代状态可以是彼此独立的。因此,一个聚集对象可以同时有几个迭代在进行之中。
  3. 聚集内容和迭代器的分离,提高了扩展性,即可以使用不同的迭代器来遍历聚集内容。

缺点

  1. 迭代器种类过多,会导致类的个数增加,提高了系统的复杂性;
  2. 在动态迭代器中易发生异常。

参考资料

文章分类
后端