遍历“容器”的优雅方法——总结迭代器模式

久未见 提交于 2021-02-09 10:03:48

前言

本文主要是读书笔记的整理,自己总结的倒不多,做个记录

聚集(集合)的概念

如果能把多个普通类的对象聚在一起形成一个总体,这个总体就被称之为聚集(Aggregate),举例子:

1、在任何编程语言中:数组都是最基本的聚集,在Java中,数组也是其他的 JAVA 聚集对象的设计基础。

2、在Java里,JAVA聚集对象都是实现了 java.util.Collection 接口的对象,是 JAVA 对聚集概念的直接支持。从 JDK 1.2 开始,JAVA 提供了多种现成的聚集 API,包括 Vector、ArrayList、HashSet、HashMap、Hashtable、ConcurrentHashMap 等。

自定义容器的封闭需求

假如因业务需要,RD 定义了专属的数据元素的聚集,还要把它提供给客户端,让其调用(不特别强调,也包括其他依赖服务)。但是有时候为了安全,RD 不想让客户端看到聚集的内部实现,只是能让她们访问就可以了,比如遍历等操作。还有的时候,客户端不需要了解具体实现,能否让客户端跳开复杂的数据结构?因为调用者们不需要了解实现方式,只要能开箱即用即可。

为了解决这个问题,那么就需要有一种策略能让客户端遍历这个聚集体的时候,无法窥破RD存储对象的方式,无需了解内部的复杂数据结构。

迭代器的引出——茶餐厅和煎饼铺子合并的经典案例

有两个遗留的点餐系统,包括一套餐厅点餐系统——专门提供正餐,和一个煎饼铺子点餐系统(不要纠结为啥煎饼摊也有点餐系统。。。)——专门提供早餐(除了早餐,其他时间不开放)。

现状

餐厅里有很多卖饭的窗口,它们的业务是一块单独的实现,隔壁煎饼铺的业务,也是一块单独的实现。现在有个老板想把它们收购并合并,让客户能在一个地方,一个时间段内,同时吃煎饼和餐厅的各种菜。目前餐厅内有至少两家餐馆都统一实现了 MenuItem 类——菜单子系统的菜单类。

问题

但是煎饼的菜单系统用的 ArrayList 记录菜单,而餐厅的 RD 用的是数组实现了菜单系统,双方的RD,都不愿意花费时间修改自己的实现。毕竟有很多其他服务依赖了菜单子系统,如下 MenuItem 代码:

/**
 * 餐厅的菜单都是午餐项目,煎饼的菜单,都是早餐项目,但是它们都属于菜单,即:
 * 都有菜品名称,描述,是否是素的,价格等
 * 故设计这样一个类作为菜单项目类
 */
public class MenuItem {
    String name;
    String description;
    public MenuItem(String name,
                    String description) {
        this.name = name;
        this.description = description;
    }

    public String getName() {
        return name;
    }

    public String getDescription() {
        return description;
    }
}

不同的餐厅使用了这个 MenuItem 类

/**
 * 煎饼窗口的菜单
 */
public class PancakeHouseMenu {
    private List<MenuItem> menuItems; // menuItems 使用 ArrayList 存储菜单的项目,动态数组,使其很容易扩大菜单规模

    /**
     * 在构造菜单的时候,把菜单加入到 ArrayList menuItems
     */
    public PancakeHouseMenu() {
        menuItems = new ArrayList<>();
        addItem("K&B's Pancake Breakfast",
                "Pancakes with scrambled eggs, and toast");

        addItem("Regular Pancake Breakfast",
                "Pancakes with fried eggs, sausage");
    }

    public void addItem(String name, String description) {
        MenuItem menuItem = new MenuItem(name, description);
        menuItems.add(menuItem);
    }

    public List<MenuItem> getMenuItems() {
        return menuItems;
    }
}
 
///////////////////////////////////////////////////////////////
/**
 * 餐厅的菜单
 */
public class DinerMenu {
    private static final int MAX_ITEMS = 6;
    private int numberOfItems = 0;
    private MenuItem[] menuItems; // 使用了真正的数组实现菜单项的存储

    public DinerMenu() {
        menuItems = new MenuItem[MAX_ITEMS];
        addItem("Vegetarian BLT", "(Fakin') Bacon with lettuce & tomato on whole wheat");
        addItem("BLT", "Bacon with lettuce & tomato on whole wheat");
        addItem("Soup of the day", "Soup of the day, with a side of potato salad");
    }

    public void addItem(String name, String description) {
        MenuItem menuItem = new MenuItem(name, description);
        if (numberOfItems >= MAX_ITEMS) {
            System.err.println("Sorry, menu is full!  Can't add item to menu");
        } else {
            menuItems[numberOfItems] = menuItem;
            numberOfItems = numberOfItems + 1;
        }
    }

    public MenuItem[] getMenuItems() {
        return menuItems;
    }
}

两种不同的菜单表现方式,会给客户端调用带来很多问题,假设客户端是服务员类——Waitress,下面是客户端的业务:

  • 打印出菜单上的每一项:打印每份菜单上的所有项,必须调用 PancakeHouseMenu 和 DinerMenu 的 getMenuItem 方法,来取得它们各自的菜单项,但是两者返回类型是不一样的

  • 只打印早餐项(PancakeHouseMenu 的菜单)或者只打印午餐项(DinerMenu 的菜单)

    • 想要打印 PancakeHouseMenu 的项,我们用循环将早餐 ArrayList 内的项列出来

    • 想要打印 DinerMenu 的项目,我们用循环将数组内的项一一列出来

  • 打印所有的素食菜单项

  • 指定项的名称,如果该项是素食的话,返回true,否则返回false

实现 Waitress 的其他方法,做法都和上面的方法类似,发现 Waitress 处理两个菜单时,总是需要写两个形式相似的循环,去遍历这些菜单,而且一旦外部菜单的数据结构变了,客户端也得跟着修改。

再有,如果还有第三家餐厅合并,而且坑爹的是,它以完全不同的实现方式实现了菜单……那怎么办?此时难道还继续写第三个循环么……

以后,这样甚至能发展到 N 个不同形式的循环……

这显然是非常不好的设计,直接导致后期系统的大量垃圾代码和日益艰巨的维护任务。

为什么出现这种结局?

封装特性

面向接口编程

代码冗余

Waitress (也就是客户端)竟然能非常清晰的,

而且是必须清晰的熟悉服务端的实现,这是很不科学的

PancakeHouseMenu 和 DinerMenu 都没有面向接口编程,

而直接实现了具体业务,导致扩展困难

DinerMenu和PancakeHouseMenu都有很大重复代码,

没有抽象共享

那么可以解决么?

解决方法

1、Waitress 要遍历早餐项,需要使用 ArrayList 的 size() 和 get() 方法

2、Waitress 遍历午餐项,需要使用数组的 length 字段和中括号

现在创建一个新的对象,将它称为迭代器(Iterator),利用它来封装“遍历集合内的每个对象的过程”,下面对其抽象、封装。

原则:只封装变化的部分

案例中变化的部分:因为不同的集合实现,导致的不同的遍历方式。将其封装即可,其实,这正是迭代器模式的应用。迭代器 Iterator,是面向接口编程,故它依赖于一个称为迭代器的接口:

/**
 * 迭代器的接口,一旦有了这个接口,就可以为给种对象集合实现迭代器:数组、列表、散列表等等
 */
public interface Iterator {
    /**
     * 聚集中,是否还有元素
     */
    boolean hasNext();

    /**
     * 返回聚集中的下一个元素
     */
    Object next();
}

让餐厅实现迭代器接口 —— Iterator,打造一个餐厅菜单迭代器——DinerMenuIterator

/**
 * 餐厅的迭代器
 */
public class DinerMenuIterator implements Iterator {
    private MenuItem[] items;
    private int position = 0;

    public DinerMenuIterator(MenuItem[] items) {
        this.items = items;
    }

    public Object next() {
        MenuItem menuItem = items[position];
        position = position + 1;
        return menuItem;
    }

    public boolean hasNext() {
        return position < items.length && items[position] != null;
    }
}

改造具体餐厅的菜单旧实现,把之前的如下代码删掉,因为它会暴露餐厅菜单的内部数据结构 menuItems

public MenuItem[] getMenuItems() {
    return menuItems;
}

下面是改造之后的餐厅菜单实现,PancakeHouseMenu 实现类似。

public class DinerMenu {
    private static final int MAX_ITEMS = 6;
    private int numberOfItems = 0;
    private MenuItem[] menuItems;

    // 实现方式不变
    public DinerMenu() {
        menuItems = new MenuItem[MAX_ITEMS];
        addItem("Vegetarian BLT", "(Fakin') Bacon with lettuce & tomato on whole wheat");
        addItem("BLT", "Bacon with lettuce & tomato on whole wheat");
        addItem("Soup of the day", "Soup of the day, with a side of potato salad");
    }

    // 实现方式不变
    public void addItem(String name, String description) {
        MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
        if (numberOfItems >= MAX_ITEMS) {
            System.err.println("Sorry, menu is full!  Can't add item to menu");
        } else {
            menuItems[numberOfItems] = menuItem;
            numberOfItems = numberOfItems + 1;
        }
    }

    // 不需要 getMenuItems 方法,因为它会暴露内部实现,返回的直接是菜单的数据结构
    // 这个新方法代替 getMenuItems,createIterator 返回的是迭代器接口
    public Iterator createIterator() {
        return new DinerMenuIterator(menuItems);
    }
}

这样写客户端的代码就不会重复两遍,如下,把迭代器的代码整合到 Waitress,改掉之前冗余的循环遍历代码,只需要传入一个迭代器作为遍历方法的参数,把遍历聚集的工作,委托给迭代器实现。既能保护内部实现,也能抽象遍历形式,精简代码。也符合了开闭原则——以后菜单的实现逻辑修改了,客户端也不用修改调用的代码。

public class Waitress {
    // 服务员依赖的菜单系统
    private PancakeHouseMenu pancakeHouseMenu;
    private DinerMenu dinerMenu;

    public Waitress(PancakeHouseMenu pancakeHouseMenu, DinerMenu dinerMenu) {
        this.pancakeHouseMenu = pancakeHouseMenu;
        this.dinerMenu = dinerMenu;
    }

    /**
     * 遍历全部菜单,无需在客户端里积压多个重复的循环代码,也符合了开闭原则——以后修改遍历逻辑,客户端不需要修改
     */
    public void printMenu() {
        // 为每个菜单系统,创建一个迭代器 Iterator
        Iterator pancakeIterator = pancakeHouseMenu.createIterator();
        Iterator dinerIterator = dinerMenu.createIterator();// 把迭代器子类型,传入
        printMenu(pancakeIterator);// 把迭代器子类型,传入
        printMenu(dinerIterator);
    }

    /**
     * 接口的用法,向上转型
     */
    private void printMenu(Iterator iterator) {
        // 先判断是否还能继续迭代
        while (iterator.hasNext()) {
            // Iterator 接口里 next 返回的是 Object 对象,故需要强制转换
            MenuItem menuItem = (MenuItem) iterator.next();
            System.out.print(menuItem.getName() + ", ");
            System.out.println(menuItem.getDescription());
        }
    }
}
 
//////
public class MenuTestDrive {
    public static void main(String args[]) {
        PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
        DinerMenu dinerMenu = new DinerMenu();
        Waitress waitress = new Waitress(pancakeHouseMenu, dinerMenu);
        waitress.printMenu();
    }
}

到底解决了什么问题

经迭代器模式对菜单系统进行封装,使得各个餐厅的菜单系统能维持不变,磨平了实现的差别,减少了重写的工作量。

旧版代码的客户端

基于迭代器模式封装服务后,重写的客户端

遍历:需要多个代码重复度较高的循环来实现,代码冗余度很高,加大无意义的工作量

只需要增加类,去实现各个菜单系统的迭代器,客户端只需要一个循环就能搞定所有的菜单服务调用

各个菜单系统的具体实现,封装的不行,对客户端暴露了数据结构,这是没有任何必要的

菜单的具体实现被封装,对外只公开迭代器,客户端不知道,也不需要知道具体菜单的实现

客户端被捆绑到了多个菜单实现类,牵一发动全身

客户端可以只用 iterator 接口做参数,通过向上转型,摆脱多个具体实现的捆绑,实现解耦

继续发现问题

客户端 Waitress 组合了多个具体实现类,仍然会牵一发动全身,比如修改了菜单的类名,客户端就失效,也需要修改,仍然重度依赖

而且,具体菜单的实现类又有共同的方法 createIterator ,完全可以进一步抽象。

改进上述设计——充分利用 JDK 自带的迭代器

首先不再为List这样的数据结构重新实现迭代器,因为JDK 5 之后,Java 已经给我们实现好了,对于JDK 5 之后的所有集合容器,都可以采用 JDK 自带的迭代器接口——java.util,Itreator,所以我们就不用自己写,只需实现数组的迭代器即可。

1、记住:JDK 不支持为数组生成迭代器

2、java.util 包中的 Collection 接口——Java 所有的集合都实现了该接口,该接口有迭代器方法。 

继续改进——抽象具体类的公共部分

可以为各个菜单实现类,提供一个公共的接口——Menu

原则:面向接口编程

有多个具体实现类的时候,要首先考虑不针对实现编程,而是面向接口编程,除非有共同的抽象方法+属性时,可以考虑抽象父类。本案例中,只需使用接口,就可以减少客户端 waittress 和具体菜单实现类之间的依赖。

import java.util.Iterator;

/**
 * 菜单系统要实现的方法,抽象为接口
 */
public interface Menu {
    Iterator createIterator();
}
 
/////////////////////////////
/**
 * 菜单的每项,抽象为类
 */
public class MenuItem {
    private String name;
    private String description;public MenuItem(String name,
                    String description) {
        this.name = name;
        this.description = description;
    }

    public String getName() {
        return name;
    }

    public String getDescription() {
        return description;
    }
}
 
/////////////////////////////////
// 餐厅菜单系统的迭代器,不需要实现额外声明的迭代器接口,而是重写JDK的迭代器即可
import java.util.Iterator;

/**
 * 重写 JDK 的迭代器
 * implements java.util.Iterator;
 */
public class DinerMenuIterator implements Iterator {
    private MenuItem[] items;
    private int position = 0;

    public DinerMenuIterator(MenuItem[] items) {
        this.items = items;
    }

    // 不需要变
    @Override
    public Object next() {
        MenuItem menuItem = items[position];
        position = position + 1;
        return menuItem;
    }

    // 不需要变
    @Override
    public boolean hasNext() {
        return position < items.length && items[position] != null;
    }

    // 重新实现,最好是重写
    @Override
    public void remove() {
        if (position <= 0) {
            throw new IllegalStateException
                    ("You can't remove an item until you've done at least one next()");
        }

        // 删除线性表的元素,所有元素需要往前移动一个位置
        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;
        }
    }
}
 
////////////////////
// 餐厅菜单系统
import java.util.Iterator;

/**
 * Created by wangyishuai on 2018/1/27
 */
public class DinerMenu implements Menu {
    private static final int MAX_ITEMS = 6;
    private int numberOfItems = 0;
    private MenuItem[] menuItems;

    // 实现方式不变
    public DinerMenu() {
        menuItems = new MenuItem[MAX_ITEMS];
        addItem("Soup of the day",
                "Soup of the day, with a side of potato salad");
    }

    // 实现方式不变
    public void addItem(String name, String description,
                        boolean vegetarian, double price) {
        MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
        if (numberOfItems >= MAX_ITEMS) {
            System.err.println("Sorry, menu is full!  Can't add item to menu");
        } else {
            menuItems[numberOfItems] = menuItem;
            numberOfItems = numberOfItems + 1;
        }
    }

    // 返回的是 java.util.Iterator;
    @Override
    public Iterator createIterator() {
        return new DinerMenuIterator(menuItems);
    }
}
 
/////////////////////////////////////////
// 煎饼,不需要再实现迭代器,因为使用的数据结构是JDK的容器,而对于JDK自带的集合容器,不需要自己实现迭代器
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * 对于JDK的集合容器——List,不需要RD实现迭代器
 */
public class PancakeHouseMenu implements Menu {
    private List<MenuItem> menuItems;

    public PancakeHouseMenu() {
        menuItems = new ArrayList<>();
        addItem("K&B's Pancake Breakfast", "Pancakes with scrambled eggs, and toast");
    }

    public void addItem(String name, String description) {
        MenuItem menuItem = new MenuItem(name, description);
        menuItems.add(menuItem);
    }

    @Override
    public Iterator createIterator() {
        // 返回 JDK ArrayList 自带的迭代器 iterator() 方法
        return menuItems.iterator();
    }
}
 
//////////////////////////////// 客户端
import java.util.Iterator;

public class Waitress {
    // 服务员依赖的菜单系统——通过接口解耦合
    private Menu pancakeHouseMenu;
    private Menu dinerMenu;

    // 修改为 Menu 接口,向上转型,解耦合
    public Waitress(Menu pancakeHouseMenu, Menu dinerMenu) {
        this.pancakeHouseMenu = pancakeHouseMenu;
        this.dinerMenu = dinerMenu;
    }

    /**
     * 以后修改遍历逻辑,客户端不需要修改
     * // 不用修改
     */
    public void printMenu() {
        // 为每个菜单系统,创建迭代器
        // java.util.Iterator;
        Iterator pancakeIterator = pancakeHouseMenu.createIterator();
        Iterator dinerIterator = dinerMenu.createIterator();
        printMenu(pancakeIterator);
        printMenu(dinerIterator);
    }

    /**
     * 接口的用法,向上转型
     * // 不用修改
     * java.util.Iterator;
     */
    private void printMenu(Iterator iterator) {
        // 先判断是否还能继续迭代
        while (iterator.hasNext()) {
            // Iterator 接口里 next 返回的是 Object 对象,故需要强制转换
            MenuItem menuItem = (MenuItem) iterator.next();
            System.out.print(menuItem.getName() + ", ");
            System.out.println(menuItem.getDescription());
        }
    }
}
public class MenuTestDrive {
    public static void main(String args[]) {
        Menu pancakeHouseMenu = new PancakeHouseMenu();
        Menu dinerMenu = new DinerMenu();
        // 即使具体的菜单实现类修改了名字或者环了实现类,客户端——Waitress 也不需要修改代码,解了耦合
        Waitress waitress = new Waitress(pancakeHouseMenu, dinerMenu);
        waitress.printMenu();
    }
}

针对 JDK 的迭代器重写的原则

remove 方法应不应该重写

虽然对于客户端来说,remove 方法非必须(当然业务需要的话,就必须自定义重写 remove),但是最好还是提供该方法,因为JDK的 Iterator接口里包含了该方法,如果不一起重写,可能会出问题。

如果客户端真的不需要删除元素,那么最好也重写该方法,只需要在重写的时候抛出一个自定义的(或者现成的)异常——如果有调用,就提醒客户端不能删除元素。JDK也是这样设计的,默认抛出异常 UnsupportedOperationException

线程安全问题

默认的迭代器接口是线程不安全的,如果有需要,要额外的加强线程安全。

迭代器模式的标准概念

迭代器模式又叫游标(Cursor)模式、Iterator模式,迭代子模式……是对象的行为模式之一,它把对容器中包含的内部对象的访问委托给外部的类,让外部的类可以使用 Iterator 按顺序进行遍历访问,而又不暴露其内部的数据结构。

Iterator Pattern (Another Name: Cursor)

Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.

脱离Java的领域,那么可以认为:迭代器模式可以顺序地访问聚集中的元素,而不必暴露聚集的内部状态(internal representation)。它把遍历的责任转移到了迭代器,而不是聚集本身,简化了聚集的接口和实现代码,也分割了责任。

迭代器模式的角色

Iterator(迭代器接口):该接口必须定义实现迭代功能的最小定义方法集,比如提供hasNext()和next()方法。

ConcreteIterator(具体的迭代器实现类):迭代器接口Iterator的实现类。可以根据具体情况加以实现。

Aggregate(聚集的接口):定义基本功能以及提供类似Iterator iterator()的方法。

concreteAggregate(聚集接口的实现类):容器接口的实现类。必须实现生成迭代器的方法。 

聚集体如果不使用 Iterator 模式,会存在什么问题

聚集类承担了太多功能

如果是自定义的聚集,那么需要由聚集自己实现顺序遍历的方法——直接在聚集的类里添加遍历方法。这样,容器类承担了太多功能:

一方面需要提供添加、删除等本身应有的功能;

一方面还需要提供遍历访问功能。

不仅责任不分离,还和客户端耦合太强

暴露聚集的太多内部实现细节

如果不使用迭代器模式,那么需要客户端自己实现服务的遍历(联系餐厅和煎饼屋的合并案例),会直接暴露聚集的数据结构,往往这是不必要的,客户端不需要了解服务的具体实现,也是为了程序的安全——不暴露太多的内部细节给客户端。

遍历聚集的时候修改聚集的元素,引起聚集的状态混乱

如果使用的是 JDK 的集合类,如果直接遍历,且遍历的时候对集合修改,会有异常抛出。因为,往往容器在实现遍历的过程中,需要保存遍历状态,当遍历操作和元素的添加、删除等操作夹杂在一起,这些更新功能在遍历的时候也被调用,很容易引起集合的状态混乱和程序运行错误等。此时应该为聚集使用迭代器模式,如果是JDK的集合类,就直接使用自带的迭代器进行迭代。

记住:Java 中的 foreach 循环看起来像一个迭代器,但实际上并不是,还是要使用迭代器模式

Iterator 支持从源集合中安全地删除对象,只需在 Iterator 上调用 remove() 即可。这样做的好处是可以避免 ConcurrentModifiedException ,这个异常顾名思意:当打开 Iterator 迭代集合时,同时又在对集合进行修改。有些集合不允许在迭代时删除或添加元素,但是调用 Iterator 的remove() 方法是个安全的做法。

List<String> list = new ArrayList<>(Arrays.asList("a","b","c","d"));
for(String s : list){
    if(s.equals("a")){
        list.remove(s);
    }
}
 
//会抛出一个ConcurrentModificationException异常,相反下面的显示正常
List<String> list = new ArrayList<>(Arrays.asList("a","b","c","d"));
Iterator<String> iter = list.iterator();
while(iter.hasNext()){
        String s = iter.next();
        if(s.equals("a")){
            iter.remove();
    }
}
// next() 必须在 remove() 之前调用。
// 在 foreach 中,编译器会使 next() 在删除元素之后被调用,因此就会抛出 ConcurrentModificationException 异常

参考

1、Iterator的remove方法可保证从源集合中安全地删除对象(转)

2、正确遍历删除List中的元素方法  http://www.jb51.net/article/98763.htm

Fail Fast 问题

如果一个算法开始之后,它的运算环境发生变化,使得算法无法进行必需的调整时,这个算法就应当立即发出故障信号。这就是 Fail Fast 的含义。同理,如果聚集的元素在一个动态迭代子的迭代过程中发生变化,迭代过程会受到影响而变得不能自恰。这时候,迭代子就应立即抛出一个异常。这种迭代子就是实现了Fail Fast 功能的迭代子。

使用迭代器模式的优点

Iterator 模式就是为了有效地处理按顺序进行遍历访问的一种设计模式,简单地说,Iterator模式提供一种有效的方法,可以屏蔽聚集对象的容器类的实现细节,而能对容器内包含的对象元素按顺序进行有效的遍历访问。所以,Iterator模式的应用场景可以归纳为以下几个:

  • 访问容器中包含的内部对象

  • 按顺序访问

优点总结:

1,实现功能分离,简化聚集的接口。让聚集只实现本身的基本功能,把迭代功能委托给外部类实现,符合类的单一职责设计原则。

2,隐藏聚集的实现细节,符合最小知道原则。为聚集或其子容器提供了一个统一接口,一方面方便客户端调用;另一方面使得客户端不必关注迭代器的实现细节。

3,可以为聚集或其子容器实现不同的迭代器,搭配其他设计模式,比如策略模式等,可以很容易的切换。 

4、客户端可以同时使用多个迭代器遍历一个聚集。

内部迭代器和外部迭代器

截止到此处,都是分析的外部迭代器模式——客户端来调用 next 方法,去取得下一个元素。

相反,内部迭代器是由迭代器自己控制游标,在这种情况下,必须告诉迭代器在游标移动的过程中,要做什么事情——必须将操作传给迭代器,因为内部迭代器的客户端,无法控制遍历过程,所欲内部迭代器伸缩性不强,一般不使用。

List 迭代的方向问题

都知道,next 方法是正向遍历,那么自然可以实现反向遍历,新加一个取得前一个元素的方法 + 一个判断游标是否已经走到了首节点的方法即可解决。

JDK也为我们做了实现:ListIterator接口,提供了一个previous方法,JDK中的任何实现了List接口的集合,都可以实现反向迭代。

非线性数据结构的迭代问题

澄清一个问题——迭代器模式是没有约束元素顺序的,即 next (previous)只是取出元素,并不是强制元素取出的先后顺序等价于元素的某种排序。通俗的说,不论是线性结构还是非线性的,甚至是包含重复元素的结构,除非有特殊业务需求,都能对其实现迭代器模式。

不可幻想:迭代的顺序就等价于集合中元素的某种有意义的排序,两者没有必然关系,谨记以避免做出错误判断,除非有自定义的顺序约束。

单一职责设计原则和迭代器模式

设计原则:一个类只有一个引起变化的原因。如果有一个类具有两个改变的原因,那么这会使得将来该类的变化机率上升,而当它真的改变时,你的设计中同时又有两个方面将会受到影响。

高内聚 > 单一职责原则

内聚:用来度量一个类或者模块紧密的达到了单一职责的目的(or 责任)。当一个类或者一个模块被设计为只支持一组相关的功能的时候,就说它具有高内聚的特性,反之就是低内聚的。

高内聚是一个比单一职责更普遍的概念,即遵守了高内聚的类,也同样具有单一职责。

迭代器模式就遵循了单一职责原则

其实前面的分析已经很全面,迭代器模式,分离了聚集的迭代的责任,有效的契合了单一职责设计原则。

扩展案例:合并咖啡厅的菜单系统

为其合并后的系统,增加咖啡厅的菜单,供应晚餐。下面是咖啡厅的菜单系统实现:

import java.util.HashMap;
import java.util.Map;

/**
 * 原始的咖啡厅菜单实现类
 */
public class CafeMenu {
    /**
     * 菜单使用了hash表存储,和现有的两个菜单系统实现不一样
     */
    private Map<String, MenuItem> menuItems = new HashMap<>();

    public CafeMenu() {
        addItem("Veggie Burger and Air Fries", "Veggie burger on a whole wheat bun, lettuce, tomato, and fries");
    }

    public void addItem(String name, String description) {
        MenuItem menuItem = new MenuItem(name, description);
        menuItems.put(menuItem.getName(), menuItem);
    }

    public Map<String, MenuItem> getItems() {
        return menuItems;
    }
}
 
//////////////////////////////////////////////////
public class MenuItem {
    private String name;
    private String description;public MenuItem(String name,
                    String description) {
        this.name = name;
        this.description = description;
    }

    public String getName() {
        return name;
    }

    public String getDescription() {
        return description;
    }
}

将咖啡厅菜单系统合并到现有的系统:

public interface Menu {
    Iterator createIterator();
}
 
//////////////////////// 咖啡厅菜单系统
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * 合并之后的咖啡厅菜单实现类
 * hash表也实现了JDK的迭代器,不需要RD自己实现
 */
public class CafeMenu implements Menu {
    /**
     * 菜单使用了hash表存储,和现有的两个菜单系统实现不一样
     * 实现不变
     */
    private Map<String, MenuItem> menuItems = new HashMap<>();

    // 实现不变
    public CafeMenu() {
        addItem("Veggie Burger and Air Fries",
                "Veggie burger on a whole wheat bun, lettuce, tomato, and fries");
    }

    // 实现不变
    public void addItem(String name, String description) {
        MenuItem menuItem = new MenuItem(name, description);
        menuItems.put(menuItem.getName(), menuItem);
    }

    /**
     * hash表支持JDK自带的迭代器 java.util.Iterator;
     */
    @Override
    public Iterator createIterator() {
        // 返回 java.util.Iterator; 只需要取得 hash 表的 value 集合即可
        return menuItems.values().iterator();
    }
}
 
//////////////////////// 客户端 Waitress
import java.util.Iterator;

public class Waitress {
    private Menu pancakeHouseMenu;
    private Menu dinerMenu;
    // 需要增加 cafeMenu
    private Menu cafeMenu;

    /**
     * 如果,太多的参数,可以使用建造者模式优化构造器
     * 需要增加 cafeMenu 参数
     */
    public Waitress(Menu pancakeHouseMenu, Menu dinerMenu, Menu cafeMenu) {
        this.pancakeHouseMenu = pancakeHouseMenu;
        this.dinerMenu = dinerMenu;
        this.cafeMenu = cafeMenu;
    }

    /**
     * 需要增加 cafeMenu 的迭代器
     */
    public void printMenu() {
        Iterator pancakeIterator = pancakeHouseMenu.createIterator();
        Iterator dinerIterator = dinerMenu.createIterator();
        Iterator cafeIterator = cafeMenu.createIterator();
        printMenu(pancakeIterator);
        printMenu(dinerIterator);
        printMenu(cafeIterator);
    }

    /**
     * 无需修改
     */
    private void printMenu(Iterator iterator) {
        while (iterator.hasNext()) {
            MenuItem menuItem = (MenuItem) iterator.next();
            System.out.print(menuItem.getName() + ", ");
            System.out.print(menuItem.getPrice() + " -- ");
            System.out.println(menuItem.getDescription());
        }
    }
}
 
//////////////////////// 测试
public class MenuTestDrive {
    public static void main(String args[]) {
        Menu pancakeHouseMenu = new PancakeHouseMenu();
        Menu dinerMenu = new DinerMenu();
        Menu cafeMenu = new CafeMenu();

        Waitress waitress = new Waitress(pancakeHouseMenu, dinerMenu, cafeMenu);
        waitress.printMenu();
    }
}

继续发现系统的问题——客户端违反了开闭原则

合并咖啡厅的过程中,发现每次合并新菜单,都要打开客户端,修改代码……客户端实现很丑陋,违反了开闭原则。

虽然我们抽象了菜单,让其在客户端解耦,并且为菜单系统分别实现了迭代器,让迭代责任分离,对客户端隐藏了具体实现,使用同一的迭代器接口,解耦了迭代动作。但是,仍然将菜单处理分成独立的对象看待,导致每次扩展,都需要修改客户端——客户端需要反复写:调用printMenue的代码,代码冗余严重,而且每次都要给构造器增加新参数。

需要一种更好的办法——集中管理菜单,使其使用一个迭代器即可应付菜单的扩展

解决方案:抽象客户端各个独立的菜单系统,只需保留一个迭代器

使用现成的 ArrayList 类实现:

import java.util.Iterator;
import java.util.List;

public class Waitress1 {
    /**
     * 把各个菜单系统集中到一个list,充分利用list的迭代器
     * 只需要一个类就搞定,不再每次都add一个菜单类了
     */
    private List<Menu> menus;

    public Waitress1(List<Menu> menus) {
        this.menus = menus;
    }

    public void printMenu() {
        // 取得list的迭代器,直接使用一个迭代器,就能遍历所有菜单,不需要在修改
        Iterator menuIterator = menus.iterator();
        while (menuIterator.hasNext()) {
            Menu menu = (Menu) menuIterator.next();
            printMenu(menu.createIterator());
        }
    }

    // 代码不需要变
    void printMenu(Iterator iterator) {
        while (iterator.hasNext()) {
            MenuItem menuItem = (MenuItem) iterator.next();
            System.out.print(menuItem.getName() + ", ");
            System.out.print(menuItem.getPrice() + " -- ");
            System.out.println(menuItem.getDescription());
        }
    }
}

基于迭代器模式实现的菜单系统无法实现树状菜单(无法扩展子菜单)

现在希望能够加上一份餐后甜点“子菜单”作为晚餐的饭后补充。如果我们能让甜点菜单变成餐厅菜单集合的一个子元素,就可以完美的解决。但是根据现在的实现,根本做不到。因为饭后甜点子菜单的实现基于数组——不变的,类型不同,无法扩展。生产环境中,这样的系统非常复杂,更加困难。

解决方案——树

1、需要某种树形结构,可以容纳菜单、子菜单和菜单项。

2、需要确定能够在每个菜单的各个项目之间游走,而且至少要像现在用迭代器一样方便。

3、需要能够更有弹性地在菜单项之间游走。比方说:可能只需要遍历甜点菜单,或者可以遍历餐厅的整个菜单。

此时,需要一种新的设计模式来解决这个案例的难题——组合模式,参看:优雅的处理树状结构——组合模式总结

欢迎关注

dashuai的博客是终身学习践行者,大厂程序员,且专注于工作经验、学习笔记的分享和日常吐槽,包括但不限于互联网行业,附带分享一些PDF电子书,资料,帮忙内推,欢迎拍砖!

 

 

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!