欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

《编程机制探析》第十二章 Iterator Pattern

程序员文章站 2022-05-17 13:54:48
...
《编程机制探析》第十二章 Iterator Pattern

本章讲解一个极为重要、极为常见的设计模式——Iterator Pattern。关于Iterator的用法,实际上我们在前面的章节中有过一面之缘。Java语言开发包(JDK)中定义了一个Iterator接口,很清楚地描述了Iterator的行为。如果对这一点还不清楚的话,请查阅Iterator接口的相关文档和入门例子。
Iterator Pattern和Visitor Pattern这两个设计模式针对的问题领域是一样的。两者都是针对数据集合的遍历操作。两者面对的共同问题模型的组成部分如下:
(1)首先,有一个集合。这个集合有可能是列表(List),也有可能是树结构(Tree,一种常见的数据结构,关于树结构的基本知识,请读者自己补充),还有可能是更加复杂的数据结构,比如图(Graph,这是一种更加复杂的数据结构,我们几乎用不到,知道有这么个东西就行了)。
(2)其次,有一个针对该集合的算法,比如,排序,统计等。这类算法通常需要遍历集合中的所有元素。因此,为了简化问题的描述,我们就称这种算法为遍历算法(Traversal)。
(3)调用该算法的程序,我们称之为调用者。为了调用该算法获得某种结果,调用者必须访问到集合中的每一个数据元素。
以上是Iterator Pattern和Visitor Pattern两种设计模式的问题模型的共同部分。下面讲述两者之间的不同之处。两个设计模式的分歧点发生在第(3)条中。两者都需要访问集合中的每一个数据元素,但两者访问集合的方案截然不同,甚至是相反的策略,这就形成了两种不同的设计模式。
在Visitor Pattern中,调用者访问集合数据的方案,我们已经知道了。那就是派出一个访问者(Visitor),深入虎穴,亲身进入到集合算法的内部进行近距离的考察。整个访问流程,完全由集合算法方一手安排,访问多少个数据,什么时候访问完,完全由集合算法方决定。客随主便,入乡随俗,这就是Visitor一方面对的境况。
Iterator Pattern则恰好相反。调用者不需要深入到集合算法的内部,而是要求集合提供一个Iterator,作为数据访问接口。通过这个Iterator接口,调用者把访问数据的主动权完全掌握在手中。调用者想访问几个数据,就访问几个数据,想什么时候停止,就什么时候停止。
如果说,Visitor Pattern是一种被动模式,那么,Iterator Pattern则完全是一个主动模式。
Iterator的典型使用方法是:
iterator = traversal.getIterator();
item = iterator.next();
do some thing with item

Vistor的典型使用方法是
visitor = new Visitor(){
  .. visit(item) { do something with item }
};
traversal.traverse(visitor);

我们可以用一个拟人化的例子来进一步说明这两个设计模式之间的差异。
Visitor是调用者一方派出的甲方用户代表,深入到乙方集合算法公司内部。这一去,那可是深入虎穴,身不由己,Visitor完全由乙方(集合算法公司)安排访问行程。
Iterator则相当于乙方(集合算法公司)派出的一名乙方业务代表,来到甲方用户(调用者)的地盘上,听候甲方用户的调遣。甲方用户想要什么,就向Iterator这个乙方业务代表提出要求,然后,乙方业务代表根据要求做出应答。
为了加深读者对这两个设计模式异同点的理解,我们可以把这两种场景推到极端的情况。我们假设这么两个具体的场景。
Iterator的应用场景是这样:
我在商品定购目录上看到一个公司有我感兴趣的产品系列。于是我打电话给该公司,要求派一个销售代表来。销售代表上门之后,从包里拿出一个一个的产品给你看,我看了几个,没什么满意的,于是打了个哈欠说,今天就先到这里吧,下次再说。打发了销售代表,我就转身去做自己的事情了。Iterator可以被召之即来,挥之即去。
我的地盘,我做主。这就是Iterator Pattern的理念。
Vistior的应用场景是这样:
我在商品定购目录上看到一个公司有我感兴趣的产品系列。于是我上门拜访该公司,公司给我安排了一场产品性能展示,我看了几个之后,没有什么满意的,于是我说,我肚子疼,想先回去了。遇到好心的公司代表,当然说,身体要紧,慢走。遇到固执的公司代表,一定会说,对不起,我们公司有自己的工作流程,完成产品演示之前,产品厅的门锁是打不开的。我只好勃然大怒,吵吵嚷嚷(比如,进行抛出异常“throw exception”这个操作),期待能够杀出重围,这时候,假设该公司的保安系统非常严密(捕捉所有的Visitor抛出的异常“try and catch every visitor exception”),就会有几个保安跑过来,把我按在椅子上继续听讲。
入乡随俗,客随主便。别人的地盘,别人做主。这就是Visitor Pattern的理念。
读者可能会说,管它主动还是被动,反正都是遍历集合,有什么区别呢?这里我要说,区别可大着呢。主动与被动,是两种截然不同的地位。我们来看这样一个例子:遍历一棵树,搜集到前5个名字是Apple(苹果)的Node(即树结构中的每一个数据结点);然后返回这5个Node;假设树遍历算法已经有了。
如果用Iterator Pattern来实现的话,非常简单。调用者可以向树结构提供的Iterator对象不断地发出请求,获取数据,发现Apple的时候就记录下来;当记录了五个Apple的时候,就停止向Iterator发出请求。
用Visitor Pattern该如何实现呢?首先,收集到五个Apple是非常简单的。我们可以在Visitor内部开辟一块空间来存放这个五个Apple。问题在于,当我们收集够了五个Apple之后,如何才能让Visitor退出来?当然,Visitor是无法自己退出来的,只能让树结构遍历算法把它放出来。那么,问题就变成,Visitor如何才能够告诉树结构遍历算法把它提前放出来?要知道,树结构遍历算法通常都是从头到尾遍历所有数据的。
如果树结构遍历算法提供了这种“提前结束”的交流机制,那么一切都好说。但大多数情况下,树结构遍历算法并不考虑这种特殊需求。那么,Visitor只好兜着五个已经收集到的Apple,随着树结构遍历算法继续遍历下去,直到最终结束。如果树结构很大的话,这是一种极大的浪费。
如果这个例子还不能让你理解主动权的重要性的话,我们再来看下一个例子:我们有有T1和T2两棵树。首先遍历T1的10个Node,如果发现Apple,那么摘下来,然后继续遍历,如果10步都没有发现Apple,那么切换到T2;遍历T2的规则也是如此,10步之内发现目标,就继续,否则就切换到T1。
Iterator Pattern实现起来很简单。相当于我是买方,情势是买方市场,我可以让两个公司的销售代表同时到我的公司来,我可以同时接待他们,让他们各自按顺序展示自己的产品。
Visitor Pattern怎么做?情势是卖方市场,我巴巴地跑上门去,看T1公司的产品展示,看了10个之后说,“请送我到T2公司的产品展示现场,我看10个之后,再回来”。这可能吗?
可能是可能,只是可能性非常小。我们如果真的遇到这种问题,非要用Visitor Pattern来解决的话,就得设计这样一个Visitor,一次访问来自于两个集合中的两个对应元素。而且,这两个集合的遍历算法也要拼成一个,两个集合之间还得想一个办法控制彼此的遍历步数。因此,这种可能只是理论上的可能。
一个用户可以同时使用多个算法的Iterator;但是用户的一个Visitor只能同时进入一个算法。
这就是两者核心理念的不同。
读者说了,既然Iterator这么好用,那我们都使用Iterator Pattern好了,至于Visitor Pattern,干脆就弃而不用了。
这个理想是好的,但现实是残酷的。Iterator Pattern比Visitor Pattern的实现难度大得多。
无论是Iterator Pattern还是Visitor Pattern,其遍历算法全都是集合一方提供的。如果集合数据结构是数组、列表等线性数据结构,那么,一切都好说,Iterator Pattern很容易实现,只需要提供一个结构体保存当前的遍历步骤(当前数组索引或者当前列表元素)就可以了。当然结构体本身还要包含对该数据集合结构的引用。用户每次调用iterator的next()方法,iterator就把数组索引或列表元素向后移动一下。读者可以自己尝试一下,实现数组结构和列表结构的Iterator,这并不难。
但是,如果集合数据结构是复杂结构的话,那么,Iterator Pattern的实现难度可就成几何级数增长了。我们这里所指的复杂结构,主要就是指树形结构。这是我们在应用开发中最常遇到的复杂结构。至于更复杂的数据结构,比如图(Graph),那几乎与日常开发工作无关。至少我没有遇到过Graph结构。因此,本书提到的复杂数据结构,就专指树形结构。
树形结构的遍历算法,一般的数据结构书籍中都有讲。由于树形结构是一层层结构类似的层次结构,其遍历算法通常都采用递归来实现。因此,树形结构遍历算法实现Visitor Pattern是相当容易的。只需要在原有的遍历算法中多加一个Visitor参数,并在代码中增加一条对Visitor方法的调用就可以了。
但是,树形结构遍历算法实现Iterator Pattern却是相当的难。Iterator必须自己维护一个类似于运行栈的结构,来保存当前的遍历状态。栈结构中需要保存本树枝之前遇到的所有父节点。
为什么Visitor Pattern就可以这么简单呢?因为Visitor Pattern可以很自然地采用递归算法。而递归算法是依靠计算机系统来帮助管理运行栈的。无论是压栈还是出栈,递归算法本身都不需要知道,只需要关心自身程序当前的操作。
Iterator Pattern就不同了。在Iterator Pattern中,你无从使用递归算法,你只能自己管理整个运行栈,所有的压栈、出栈操作都需要自己来管理。这种栈管理的算法,往往比树结构遍历算法本身还要繁琐。这种情况下,强行实现Iterator Pattern是得不偿失的。
千言万语一句话。Iterator是好的,但不是免费的。
我们可以用一个说法来比喻Visitor Pattern和Iterator Pattern的取舍。这个说法叫做“山不就我,我就山。”意思是,如果山不过来我这边,我就过去山那边。如果集合算法能够提供Iterator当然最好。如果集合算法不能提供Iterator Pattern的编程接口,只提供了Visitor Pattern的编程接口,那我们没办法,也只好按照人家的要求老老实实派一个Visitor进入人家的地盘。
计算机界中从来不缺乏聪明人。就有这么一些人不信邪,非要想出一种简单的方法来实现Iterator Pattern。凭什么Visitor Pattern可以自然地在递归算法实现,而Iterator Pattern就不可以呢?凭什么Visitor Pattern可以让系统替自己管理运行栈,而Iterator Pattern就不可以呢?
聪明的人们把目光转向了Coroutine(协程,相互协作的线程)。Coroutine本来是一个通用的概念。表示几个协同工作的程序。比如,消费者/生产者,你走几步,我走几步;下棋对弈,你一步我一步。
由于协同工作的程序通常只有2个,而且这两个程序交换的数据通常只有一个。于是人们就很容易想到用Coroutine来实现Iterator。
不得不说,这是一个非常精当的思路。通过前面对Iterator Pattern的种种描述,我们可以很明显地看出,Iterator Pattern实际上就是一种生产者/消费者模型。Iterator就是生产者,因此Iterator有个别名叫做Generator。而调用Iterator的程序,就是消费者。
每次Iterator生产者程序就是等在那里,一旦用户(消费者角色)调用了iterator的next()方法,Iterator就继续向下执行一步,然后把当前遇到的内部数据的Node放到一个消费者用户能够看到的公用的缓冲区(比如,直接放到消费者线程栈里面的局部变量)里面,然后自己就停下来(wait)。然后消费者用户就从缓冲区里面获得了那个Node。
这样Iterator就可以自顾自地进行递归运算,不需要自己管理一个栈,而是迫使计算机帮助它分配和管理运行栈。
下面是一段来遍历二叉树(Binary Tree,最多只能有两个树枝的树形结构)的递归结构的示意代码。这段代码不是我写的,而是从网上的Coroutine资料中摘取的。
public class TreeWalker : Coroutine {
    private TreeNode _tree;
    public TreeWalker(TreeNode tree) { _tree = tree; }
    protected override Execute() {
        Walk(_tree);
    }
    private void Walk(TreeNode tree) {
        if (tree != null) {
            Walk(tree.Left);
            Yield(tree);
            Walk(tree.Right);
        }
    }
}
其中的Yield指令是关键。意思是,首先把当前Node甩到用户的数据空间,然后自己暂停运行(类似于java的thread yield方法或者object.wait方法),把自己当前运行的线程挂起来,这样虚拟机就为自己保存了当前的运行栈(context)。
有经验的读者可能会说,这不就是continuation吗?对。只是Coroutine这里多了一个产生并传递数据的动作。
网上有具体的Java Coroutine实现,具体代码我也没有看过。我自己设想了一下实现原理,给出以下的示意代码.
public class TreeIterator implements Iterator{
   TreeWalker walker;
    // start the walker thread ..
   Object next(){
     walker.notify();
     // wait for a while so that walker can continue to run
     return walker.currentNode;
   }
}

class TreeWalker implements Runnable{
    TreeNode currentNode;
  
TreeWarker(root){ 
    currentNode = root;
}
void run(){
  walk(curentNode);
}

private void Walk(TreeNode tree) {
        if (tree != null) {
            Walk(tree.Left);
currentNode = tree;
this.wait();
Walk(tree.Right);
        }
    }
}

我们看到,Iterator本身是一个线程,用户也是一个线程。Iterator 线程一步步产生数据,并把数据传递给用户线程。这里,Iterator线程就是生产者线程,而用户线程就是消费者线程。
以上只是示意代码,我并没有深究过。因为,我虽然很欣赏用Corutine实现Iterator Pattern这种有创意的想法,但我本人并不赞同、也不会采用这种做法。因为线程是一种相对昂贵的资源,不应该这么任意地使用。线程同步则更是耗时耗力。我宁可自己维护一个Stack,也不愿意引入Coroutine这类Thread Control的方式来实现Iterator。
前面讲了很多实现原理方面的东西,现在,让我们看一些实际一点的例子。
树形结构是我们在应用开发中经常遇到的数据结构,XML就是最典型的实用例子。同样,关于XML的基本知识,请读者自行解决。
XML是一种层次化、结构化的树形文本结构。XML开发包中通常会提供两套操作XML文档的标准编程接口(即API,Application Programming Interface,应用程序编程接口)。一套叫做SAX,一套叫做DOM。
SAX是Simple API for XML的缩写。在Java语言中,程序员在使用SAX接口的时候,需要实现一个叫做ContentHandler的接口。其定义如下:
package org.xml.sax;
public interface ContentHandler
{
    public void setDocumentLocator (Locator locator);
    public void startDocument ()
throws SAXException;
    public void endDocument()
throws SAXException;
    public void startPrefixMapping (String prefix, String uri)
throws SAXException;
    public void endPrefixMapping (String prefix)
throws SAXException;
    public void startElement (String uri, String localName,
      String qName, Attributes atts)
throws SAXException;
    public void endElement (String uri, String localName,
    String qName)
throws SAXException;
    public void characters (char ch[], int start, int length)
throws SAXException;
    public void ignorableWhitespace (char ch[], int start, int length)
throws SAXException;
    public void processingInstruction (String target, String data)
throws SAXException;
    public void skippedEntity (String name)
throws SAXException;
}
我们可以看到,这个ContentHandler就是一个典型的Visitor,而且是带有甚多Type Dispatch的Visitor,其中定义了很多方法来处理不同的数据节点。比如,遇到文档开头怎么办,遇到文档结束怎么办,遇到文本怎么办,遇到属性(attribute,XML里面的概念)怎么办,等等。整个SAX开发包就是一个典型的Double Dispatch的Visitor Pattern。
关于SAX的具体应用例子,还是请读者自己去查阅。如果有兴趣的话,读者还可以参阅一下SAX开发包内部的具体实现,看看XML的SAX遍历算法是不是采用递归结构来实现的。
除了SAX之外,XML开发包提供的另外一套编程接口叫做DOM,是Document Object Model的缩写。DOM编程接口把整个XML文档作为一个树形数据结构提供给程序员。程序员可以自己实现对这个树形结构的遍历算法。
值得一提的是,W3组织的网站上,DOM Level 2规范专门对DOM Traversal(DOM结构遍历)进行了定义。请参见如下网址:
http://www.w3.org/TR/2000/REC-DOM-Level-2-Traversal-Range-20001113/traversal.html
注:W3组织是一个定义各种互联网格式、规范、协议的网站,其中包括XML、HTML、HTTP等各种重要的规范和协议。对于Web开发人员来说,W3网站极为重要。
DOM Traversal规范中定义了DocumentTraversal, TreeWalker, NodeIterator, NodeFilter等多种接口。这些接口定义很有趣,值得研究一下。我大致看了一下。DOM Traversal主要是一个Iterator Pattern,TreeWalker, NodeIterator都是Iterator;但DOM Traversal同时也是一个简单的Visitor Pattern —— NodeFiter可以作为简单的Visitor被注入到Traversal算法里面,对遇的每个Node进行过滤。这个NodeFiter的方法名比较有意思,叫做accept,其功能类似于前面讲过的FileFilter(文件过滤器)的accept方法,起着过滤DOM Node的作用。
DOM Traversal规范的具体实现,我并没有找到。当然,我也没有花心思去找。我只对其设计思路感兴趣,对于其实现细节和使用方法并无兴趣。不过,我倒是知道另外一个实现了Iterator Pattern的XML编程接口——StAX。
StAX编程接口中,提供了一个叫做XMLStreamReader的接口,其中提供了next()和hasNext()等方法,正是起着Iterator的作用。从这个接口名中,我们可以得知Iterator的另一个别名——Stream(流)。现在,我们已经知道了Iterator的两个别名——Generator和Stream。
StAX并不是标准的XML编程接口,但却是一个真正实现了Iterator Pattern的XML编程开发包。
同样,StAX开发包的具体用法,请读者具体查阅。
StAX的内部源代码,我并没有看过。想来应该就是在XMLStreamReader的实现类中维护了一个栈结构,用来存放之前遍历过的所有父节点。至于用Corutine实现的可能性不大,毕竟,线程和线程同步的开销是比较大的。
有兴趣的读者可以自行去研究StAX的实现源码。
相关标签: 设计模式