《Head First 设计模式》读书笔记(9)—— 迭代器与组合模式 Iterator and Composite

本文共7000词,阅读需要20分钟,本文相关代码地址被托管在 gitee,这一章的组合模式的外部迭代器有点难懂

有很多地方可以把对象堆起来成为一个集合,比如数组,堆栈,列表或者散列表(hashtable)
但是如果你想遍历这些对象,你肯定不希望别人看着你如何实现这个集合,因为这样极其不专业
最好应该是客户能够遍历你的集合到那时无法窥探你的存储方式。

栗子

背景

餐厅和煎饼屋合并,两个原来的菜单是用不同数据结构实现的,餐厅用的是ArrayList,而煎饼屋用的数组,他们两个都不愿意改变自己的实现,因为两者代码都依赖很多,都不容易改变。
1.数组实现的

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

    public DinerMenu() {
        menuItems = new MenuItem[MAX_ITEMS];
        addItem("as","asd",true,0.1);
        addItem("as","asd",true,0.1);
        addItem("as","asd",true,0.1);
    }

    private void addItem(String a, String b, Boolean c, double d) {
        MenuItem menuItem = new MenuItem(a, b, c, d);
        if (numberOfItems >= MAX_ITEMS) {
            System.out.println("menu is full");
        } else {
            menuItems[numberOfItems]=menuItem;
            numberOfItems++;
        }
    }

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

2.List实现的

public class PancakeHouseMenu {
    ArrayList<MenuItem> menuItems;

    public PancakeHouseMenu() {
        menuItems= new ArrayList<>();
        addItem("as","asd",true,0.1);
        addItem("as","asd",true,0.1);
        addItem("as","asd",true,0.1);

    }

    private void addItem(String a,String b,Boolean c,double d) {
        MenuItem menuItem = new MenuItem(a,b,c,d);
        menuItems.add(menuItem);
    }

    public PancakeHouseMenu(ArrayList<MenuItem> menuItems) {
        this.menuItems = menuItems;
    }

    public ArrayList<MenuItem> getMenuItems() {
        return menuItems;
    }
}

如果你现在希望实现一些功能,例如打印所有的价格,或者某些原料为肌肉的食品,总之你需要遍历一下
大概流程就不细说,总之你需要创建两个菜单对象,每个对象使用get方法拿到以后然后分别for循环,其中可以穿插一些if语句判断
其实你会发现每一个都是MenuItem对象。所以一定有办法可以简化。

找到不变的东西,然后封装——封装遍历

上面两个遍历其实java8用lambda表达式很容易弄,但是假如不用这些的话,那就需要for循环
List对应size(),get()方法;数组直接用length()和下标值

其实我们可以分别创建一个Iterator对象,就是迭代器,你会发现两者使用迭代器的代码竟然并没有什么差异,数组的迭代器

public class DinerMenuIterator implements Iterator {
    MenuItem[] menuItems;
    int position = 0;

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

    @Override
    public boolean hasNext() {
        if (position >= menuItems.length || menuItems[position] == null) {
            return false;
        } else {
            return true;
        }
    }

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

List的迭代器:

public class PancakeHouseMenuIterator implements Iterator {
    ArrayList<MenuItem> menuItems;
    int position = 0;

    public PancakeHouseMenuIterator(ArrayList<MenuItem> menuItems) {
        this.menuItems = menuItems;
    }

    @Override
    public boolean hasNext() {
        if (position >= menuItems.size() || menuItems.get(position) == null) {
            return false;
        } else {
            return true;
        }
    }

    @Override
    public Object next() {
        MenuItem menuItem = menuItems.get(position);
        position = position + 1;
        return menuItem;
    }
}

于是主方法遍历的时候只是在使用两个迭代器

public class Menu {
    public static void main(String[] args) {
        DinerMenu dinerMenu = new DinerMenu();
        PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
        Iterator dinerMenuIterator = dinerMenu.createIterator();
        Iterator pancakeHouseMenuIterator = pancakeHouseMenu.createIterator();
        printMenu(dinerMenuIterator);
        printMenu(pancakeHouseMenuIterator);
    }

    private static void printMenu(Iterator iterator) {
        while (iterator.hasNext()){
            MenuItem menuItem = (MenuItem) iterator.next();
            System.out.println(menuItem.getDescription()+" "+menuItem.getPrice());
        }
    }
}

迭代器前后对比

原来需要两个循环,现在实现迭代器只需要迭代一个迭代器,而且不管多少都是循环一个
原来各种实现的方法中使用的都是具体的类,现在就是在面向一个接口
原来你可以看到具体实现的数据结构,现在你不知道里面到底有什么

使用java内置的Iterator接口


Collection接口里已经定义了了Iterator iterator()方法
而数组的实现则需要手动实现

import java.util.Iterator;

public class DinerMenuIterator implements Iterator {
    MenuItem[] menuItems;
    int position = 0;

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

    @Override
    public boolean hasNext() {
        if (position >= menuItems.length || menuItems[position] == null) {
            return false;
        } else {
            return true;
        }
    }

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

    @Override
    public void remove() {
        if (position <= 0){
            throw new IllegalStateException("can't remove");
        }
        if (menuItems[position-1]!=null){
            for (int i = position-1; i < menuItems.length-1; i++) {
                menuItems[i]=menuItems[i+1];
            }
        }
        menuItems[menuItems.length-1]=null;
    }
}

定义迭代器模式

迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。
主要目的是就是访问的时候别人不知道你怎么实现的,使用统一的方法访问不同的聚合对象,意义就在于这样吧元素游走的责任交给了迭代器,让最后使用聚合和实现相关操作的接口变得很简单,让那些操作更专注到其功能上,而不是主要用在了遍历身上。

单一责任原则

一个类应该只有一个引起变化的原因

一个类应该由自己要完成的责任,而遍历这种责任如果维护起来的话,可能其本身的责任代码也要随着改动,所以一个类的责任代码就有了两个可能会引起变化的原因
所以我们应该将一个责任只指派给一个类,这其实很难,但是遍历这种同时出现,还是应该消除这种改变原因

新的背景:咖啡厅的菜单

增加一个用hashtable的咖啡店,然后按照相同的方法对hashtable的values进行遍历,其实还是和以前一样

这种做法让这个菜单确实变得有极大的扩展性

java还有很多的Collection接口,能够存取一群对象,他们都具有不同的接口,但是都能够实现迭代器,尤其是java8以后的lambda表达式

新背景:如果菜单还有子菜单

如果某个菜单里还希望添加一个甜点菜单,这是一个Menu对象,而不是一个MenuItem对象,所以原来那套有行不通了
现在的菜单应该是这个样子的:

我们需要重构

定义组合模式

组合模式允许你将对象组合成树形结构来表现“整体/部分”层次结构,组合能让客户以一致的方式来处理个别对象以及对象组合。
类图:

再看菜单

最后的菜单就是各种菜单对象的组合,现在利用组合模式设计菜单
上面所了就两种对象,一种Menu对象,一种MenuItem对象,现在写一个MenuComponent接口作为两种对象的超

public abstract class MenuComponent {
    public void add(MenuComponent menuComponent){
        throw new UnsupportedOperationException();
    }

    public void remove(MenuComponent menuComponent){
        throw new UnsupportedOperationException();
    }

    public MenuComponent getChild(int i){
        throw new UnsupportedOperationException();
    }

    public String getName(){
        throw new UnsupportedOperationException();
    }

    public String getDescription(){
        throw new UnsupportedOperationException();
    }

    public double getPrice(){
        throw new UnsupportedOperationException();
    }

    public boolean isVegetarian(){
        throw new UnsupportedOperationException();
    }

    public void print(){
        throw new UnsupportedOperationException();
    }
}


package headfirst.iterator_pattern.menu.five;

public class MenuItem extends MenuComponent {
    String name;
    String description;
    boolean vegetarian;
    double price;

    public MenuItem(String name, String description, boolean vegetarian, double price) {
        this.name = name;
        this.description = description;
        this.vegetarian = vegetarian;
        this.price = price;
    }

    @Override
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    @Override
    public boolean isVegetarian() {
        return vegetarian;
    }

    public void setVegetarian(boolean vegetarian) {
        this.vegetarian = vegetarian;
    }

    @Override
    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    @Override
    public void print() {
        System.out.println(this.getName());
        if (isVegetarian()){
            System.out.println("v");
        }
        System.out.println(this.getDescription());
        System.out.println(this.getPrice());
    }
}



package headfirst.iterator_pattern.menu.five;

import javafx.scene.input.InputMethodTextRun;

import java.util.ArrayList;
import java.util.Iterator;

public class Menu extends MenuComponent {
    ArrayList<MenuComponent> menuComponents = new ArrayList<>();
    String name;
    String description;

    public Menu(String name, String description) {
        this.name = name;
        this.description = description;
    }

    @Override
    public void add(MenuComponent menuComponent) {
        menuComponents.add(menuComponent);
    }

    @Override
    public void remove(MenuComponent menuComponent) {
        menuComponents.remove(menuComponent);
    }

    @Override
    public MenuComponent getChild(int i) {
        return menuComponents.get(i);
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getDescription() {
        return description;
    }

    @Override
    public void print() {
        System.out.println(this.getName());
        System.out.println(this.getDescription());
        Iterator iterator = menuComponents.iterator();
        while (iterator.hasNext()){
            MenuComponent menuComponent = (MenuComponent) iterator.next();
            menuComponent.print();
        }
    }
}

关于组合模式的讨论

在迭代器模式中,你刚说一个类应该只有一种责任,单一责任原则
而现在一个类有两个责任,组合模式不但要设计其本身的层次结构,而且还要执行菜单的操作
这是不容争辩的,但是组合模式利用单一责任原则换取了“透明性”,通过让组件的接口同时包含一些管理子节点和叶结点的操作,客户就可以将组合和叶结点一视同仁,也就是用户对一个元素是组合还是叶结点是透明的。
这是一个很典型的折衷案例,尽管我们应该遵从设计原则,但是我们有时也会取舍一些原则,是为了一些更方便

内部迭代器和外部迭代器

以前女招待遍历打印整个菜单,其实她并没有使用迭代器模式去遍历,而是在每个菜单组件内部已经实现迭代遍历,这是内部遍历,但是如果你想一个迭代器直接遍历组件中所有的项,那就要使用外部的迭代器,而且需要使用栈结构来保存组合的层次结构,所以外部迭代器会看起来很复杂。

public class CompositeIterator implements Iterator {
    Stack stack = new Stack();

    public CompositeIterator(Iterator iterator) {
        stack.push(iterator);
    }

    @Override
    public boolean hasNext() {
        if (stack.empty()) {
            return false;
        } else {
            Iterator iterator = (Iterator) stack.peek();
            if (!iterator.hasNext()) {
                stack.pop();
                return this.hasNext();
            } else {
                return true;
            }
        }
    }

    @Override
    public Object next() {
        if (hasNext()) {
            Iterator iterator = (Iterator) stack.peek();
            MenuComponent menuComponent = (MenuComponent) iterator.next();
            if (menuComponent instanceof Menu) {
                stack.push(menuComponent.createIterator());
            }
            return menuComponent;
        } else {
            return null;
        }
    }
}

空迭代器——空对象设计模式的一种

比如上面的具体某道菜,它不像菜单内部也没什么可以遍历的,所以怎么实现组件的createIterator方法。
选择1:
直接返回null对象,但是在客户代码中你就要用条件语句来判断返回值
选择2:
返回迭代器,但是这个迭代器的hasNext()永远返回false
这样就不需要判断,只是它永远都返回的东西是不存在

public class NullIterator implements Iterator {
    @Override
    public boolean hasNext() {
        return false;
    }

    @Override
    public Object next() {
        return null;
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException();
    }
}

在命令模式中,曾经也用到过空对象模式,用空命令来代表初始化状态下的某个命令

个人总结

组合模式为了让客户免于检查自己的操作,把其可能设计的多种对象用一个相同的接口去实现,甚至采用抛出很多异常去实现。
组合模式通常内部的方法嵌入迭代器模式,如果是外部想进行迭代,通常因为要保存层次结构,会使用栈去存取。

发表评论

电子邮件地址不会被公开。