设计模式--迭代器模式

目录

本文的结构如下:

  • 引言
  • 什么是迭代器模式
  • 模式的结构
  • 典型代码
  • 代码示例
  • 优点和缺点
  • 适用环境
  • 模式应用

一、引言

在平时生活中,可能有这样的场景,一天的高强度敲代码特别疲累,下班后又在十字路口堵了大半天,好不容易回到家中,啥也不想干,就往沙发上一躺,拿起遥控器,打开电视,选了一个爱看的频道,哇,全是美女,好吧,可惜太累了,居然睡着了。

这里的电视就是一个存放频道的容器,而遥控器则方便我们去访问这个电视里的频道,并且压根不知道电视到底是怎么存放频道的,也不需要知道。

在软件开发中,也存在像电视这样的类,它用来存储多个成员对象,这些类称为聚合类(Aggregate Classes),对应的对象称为聚合对象。为了更加方便地操作这些聚合对象,同时可以很灵活地为聚合对象增加不同的遍历方法,可以为这些类提供一个遥控器样的角色,可以访问一个聚合对象中的元素但又不需要暴露它的内部结构,称为迭代器。

这种设计其实就是迭代器模式。

二、什么是迭代器模式

聚合对象用来存储一系列数据。其主要有两个职责:一是存储数据;二是遍历数据。从依赖性来看,前者是聚合对象的基本职责;而后者既是可变化的,又是可分离的。这时,将遍历数据的行为从聚合对象中分离出来,封装在一个被称之为“迭代器”的对象中,由迭代器来提供遍历聚合对象内部数据的行为,这将简化聚合对象的设计,更符合“单一职责原则”的要求。

迭代器模式定义如下:

迭代器模式(Iterator Pattern):提供一种方法来访问聚合对象,而不用暴露这个对象的内部表示,其别名为游标(Cursor)。迭代器模式是一种对象行为型模式。

迭代器模式将为聚合对象提供一个遥控器,通过引入迭代器,客户端无须了解聚合对象的内部结构即可实现对聚合对象中成员的遍历,还可以根据需要很方便地增加新的遍历方式。

三、模式的结构

迭代器模式的UML类图如下:

20171124_iterator01.png

在迭代器模式中包含的角色有:

Aggregate(抽象聚合类):它用于存储和管理元素对象,声明一个createIterator()方法用于创建一个迭代器对象,充当抽象迭代器工厂角色。

ConcreteAggregate(具体聚合类):它实现了在抽象聚合类中声明的createIterator()方法,该方法返回一个与该具体聚合类对应的具体迭代器ConcreteIterator实例。

Iterator(抽象迭代器):它定义了访问和遍历元素的接口,声明了用于遍历数据元素的方法,例如:用于判断是否还有下一个元素的hasNext()方法,用于获取获取下一个元素的next()方法,在具体迭代器中将实现这些方法。

ConcreteIterator(具体迭代器):它依赖具体的聚合对象,实现了抽象迭代器接口,完成对聚合对象的遍历,同时在具体迭代器中通过游标来记录在聚合对象中所处的当前位置,在具体实现时,游标通常是一个表示位置的非负整数。

在迭代器模式中,提供了一个外部的迭代器来对聚合对象进行访问和遍历,迭代器定义了一个访问该聚合元素的接口,并且可以跟踪当前遍历的元素,了解哪些元素已经遍历过而哪些没有。迭代器的引入,将使得对一个复杂聚合对象的操作变得简单。

四、典型代码

抽象聚合类的典型代码如下(这里是抽象类,也可以是接口):

public abstract class Aggregate<T> {
    abstract Iterator<T> createIterator();
}

具体聚合类的典型代码如下:

public class ConcreteAggregate<T> extends Aggregate<T> {

    public ConcreteAggregate(List<T> objects) {
        super(objects);
    }

    Iterator<T> createIterator() {
        return new ConcreteIterator<T>(this);
    }
}

抽象迭代器的典型代码如下:

public interface Iterator<T> {
    boolean hasNext();//判断是否存在下一个元素
    T next();//返回下个元素
}

具体迭代器的典型代码如下:

public class ConcreteIterator<T> implements Iterator<T> {

    private ConcreteAggregate<T> concreteAggregate;//维持一个对具体聚合对象的引用,以便于访问存储在聚合对象中的数据
    private int cursor; //定义一个游标,用于记录当前访问位置

    public ConcreteIterator(ConcreteAggregate<T> concreteAggregate){
        this.concreteAggregate = concreteAggregate;
    }

    public boolean hasNext() {
        return cursor != concreteAggregate.getObjects().size();
    }

    public T next() {
        //toDo
        return null;
    }
}

五、代码示例

假设某个公司需要开发一个设备管理系统,该系统需要对用户数据,设备信息进行遍历,要实现这个功能,怎么做呢?

5.1、继承复用

先看下最常规的做法。

为了复用遍历的功能,一般会建立一个抽象的数据集合类AbstractObjectList,里面实现一些常用的方法用于访问,然后存放用户数据和设备数据的类都从AbstractObjectList继承,则可以复用这些方法。

20171124_iterator02.png

如上图所以,也许还有更多的方法,这里只列举几个。

但是这样设计后,会发现有几个缺点:

  1. addObject()、removeObject()等方法用于管理数据,而next()、isLast()、previous()、isFirst()等方法用于遍历数据。这将导致聚合类的职责过重,它既负责存储和管理数据,又负责遍历数据,违反了“单一职责原则”,由于聚合类非常庞大,实现代码过长,还将给测试和维护增加难度。
  2. 如果将抽象聚合类声明为一个接口,则在这个接口中充斥着大量方法,不利于子类实现,违反了“接口隔离原则”。
  3. 如果将所有的遍历操作都交给子类来实现,将导致子类代码庞大,而且必须暴露AbstractObjectList的内部存储细节,向子类公开自己的私有属性,否则子类无法实施对数据的遍历,这将破坏AbstractObjectList类的封装性。

5.2、迭代器模式

迭代器模式则可以规避继承的缺点,它将聚合类中负责遍历数据的方法提取出来,封装到专门的类中,实现数据存储和数据遍历分离,无须暴露聚合类的内部属性即可对其进行操作。

20171124_iterator03.png

抽象聚合类:

public abstract class AbstractObjectList<T> {
    private List<T> objects;

    public AbstractObjectList(List<T> objects){
        this.objects = objects;
    }

    public void addObject(T e){
        this.objects.add(e);
    }

    public void removeObject(T e){
        this.objects.remove(e);
    }

    public List<T> getObjects(){
        return objects;
    }

    abstract AbstractIterator<T> createIterator();
}

具体聚合类:

public class UserList extends AbstractObjectList<User> {
    public UserList(List<User> users) {
        super(users);
    }

    AbstractIterator<User> createIterator() {
        return new UserIterator(this);
    }
}

抽象迭代器:

public interface AbstractIterator<T> {
    public void next(); //移至下一个元素
    public boolean isLast(); //判断是否为最后一个元素
    public void previous(); //移至上一个元素
    public boolean isFirst(); //判断是否为第一个元素
    public T getNextItem(); //获取下一个元素
    public T getPreviousItem(); //获取上一个元素
}

用户数据迭代类:

public class UserIterator implements AbstractIterator<User> {
    private UserList userList;
    private List<User> users;
    private int cursor1; //定义一个游标,用于记录正向遍历的位置
    private int cursor2; //定义一个游标,用于记录逆向遍历的位置

    public UserIterator(UserList userList) {
        this.userList = userList;
        this.users = userList.getObjects();//获取集合对象
        cursor1 = 0; //设置正向遍历游标的初始值
        cursor2 = users.size() -1; //设置逆向遍历游标的初始值
    }

    public void next() {
        if(cursor1 < users.size()) {
            cursor1++;
        }
    }

    public boolean isLast() {
        return (cursor1 == users.size());
    }

    public void previous() {
        if (cursor2 > -1) {
            cursor2--;
        }
    }

    public boolean isFirst() {
        return (cursor2 == -1);
    }

    public User getNextItem() {
        return users.get(cursor1);
    }

    public User getPreviousItem() {
        return users.get(cursor2);
    }
}

测试:

public class Client {
    public static void main(String[] args) {
        List<User> users = new ArrayList<User>();
        users.add(new User("孙悟空", "男娃"));
        users.add(new User("貂蝉", "女娃"));
        users.add(new User("关二爷", "男娃"));
        users.add(new User("紫霞仙子", "女娃"));
        users.add(new User("勒布朗", "男娃"));
        users.add(new User("胡歌", "男娃"));

        AbstractObjectList<User> list;//创建聚合对象
        AbstractIterator<User> iterator;//创建迭代器对象

        list = new UserList(users);
        iterator = list.createIterator();

        System.out.println("正向遍历:");
        while (!iterator.isLast()){
            System.out.println(iterator.getNextItem() + ",");
            iterator.next();
        }

        System.out.println();
        System.out.println("-----------------------------");
        System.out.println("逆向遍历:");
        while (!iterator.isFirst()){
            System.out.println(iterator.getPreviousItem() + ",");
            iterator.previous();
        }
    }
}

如果需要增加一个新的具体聚合类,如设备数据集合类,并且需要为设备数据集合类提供不同于用户数据集合类的正向遍历和逆向遍历操作,只需增加一个新的聚合子类和一个新的具体迭代器类即可,原有类库代码无须修改,符合“开闭原则”;
如果需要为用户聚合类更换一个迭代器,只需要增加一个新的具体迭代器类作为抽象迭代器类的子类,重新实现遍历方法,原有迭代器代码无须修改,也符合“开闭原则”;
但是如果要在迭代器中增加新的方法,则需要修改抽象迭代器源代码,这将违背“开闭原则”。

5.3、使用内部类实现迭代器

在迭代器模式结构图中,具体迭代器类和具体聚合类之间存在双重关系,其中一个关系为关联关系,在具体迭代器中需要维持一个对具体聚合对象的引用,该关联关系的目的是访问存储在聚合对象中的数据,以便迭代器能够对这些数据进行遍历操作。

除了使用关联关系外,为了能够让迭代器可以访问到聚合对象中的数据,我们还可以将迭代器类设计为聚合类的内部类。

public class UserList extends AbstractObjectList<User> {
    public UserList(List<User> users) {
        super(users);
    }

    AbstractIterator<User> createIterator() {
        return new UserIterator(this);
    }

    private class Itr implements AbstractIterator<User> {
        private int cursor1;
        private int cursor2;

        public Itr(){
            cursor1 = 0;
            cursor2 = getObjects().size() -1;
        }

        public void next() {
            if(cursor1 < getObjects().size()) {
                cursor1++;
            }
        }

        public boolean isLast() {
            return (cursor1 == getObjects().size());
        }

        public void previous() {
            if(cursor2 > -1) {
                cursor2--;
            }
        }

        public boolean isFirst() {
            return (cursor2 == -1);
        }

        public User getNextItem() {
            return getObjects().get(cursor1);
        }

        public User getPreviousItem() {
            return getObjects().get(cursor2);
        }
    }
}

用不用内部类实现,对客户端来说都是一样的,客户端无须关心具体迭代器对象的创建细节,只需通过调用工厂方法createIterator()即可得到一个可用的迭代器对象。

说到底,其实迭代器模式就是让聚合类的管理数据的责任和遍历的责任分离。

六、优点和缺点

6.1、优点

迭代器模式的主要优点如下:

  • 它支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式(比如JDK ArrayList中,有Iterator和ListIterator)。在迭代器模式中只需要用一个不同的迭代器来替换原有迭代器即可改变遍历算法,我们也可以自己定义迭代器的子类以支持新的遍历方式。
  • 迭代器简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据遍历等方法,这样可以简化聚合类的设计,符合“类的职责单一原则”。
  • 在迭代器模式中,由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改原有代码,满足“开闭原则”的要求。

6.2、缺点

迭代器模式的主要缺点如下:

  • 由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。
  • 抽象迭代器的设计难度较大,需要充分考虑到系统将来的扩展,例如JDK内置迭代器Iterator就无法实现逆向遍历,如果需要实现逆向遍历,只能通过其子类ListIterator等来实现,而ListIterator迭代器无法用于操作Set类型的聚合对象。在自定义迭代器时,创建一个考虑全面的抽象迭代器并不是件很容易的事情。

七、适用场景

在以下情况下可以考虑使用迭代器模式:

  • 访问一个聚合对象的内容而无须暴露它的内部表示。将聚合对象的访问与内部数据的存储分离,使得访问聚合对象时无须了解其内部实现细节。
  • 需要为一个聚合对象提供多种遍历方式。
  • 为遍历不同的聚合结构提供一个统一的接口,在该接口的实现类中为不同的聚合结构提供不同的遍历方式,而客户端可以一致性地操作该接口。

迭代器模式是一种使用频率非常高的设计模式,通过引入迭代器可以将数据的遍历功能从聚合对象中分离出来,聚合对象只负责存储数据,而遍历数据 由迭代器来完成。由于很多编程语言的类库都已经实现了迭代器模式,因此在实际开发中,我们只需要直接使用Java、C#等语言已定义好的迭代器即可,迭代器已经成为我们操作聚合对象的基本工具之一。

八、模式应用

JDK内置的迭代器就是一个很好的例子。

在Java集合框架中,常用的List和Set等聚合类都继承(或实现)了java.util.Collection接口,在Collection接口中声明了如下方法(部分):

20171124_iterator04.png

里面有一个iterator方法,返回一个迭代器,用于数据的遍历。

Iterator接口有几个方法:

20171124_iterator05.png

JDK中的模式很复杂,从idea中掏出List的部分类图看看:

20171124_iterator06.png

Collection接口和Iterator接口充当了迭代器模式的抽象层,分别对应于抽象聚合类和抽象迭代器,而Collection接口的子类充当了具体聚合类,Iterator的子类则充当了具体迭代器。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345