Head First 设计模式(九)迭代器与组合模式

迭代器模式

定义

迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示

迭代器模式我们很熟悉,其实就是Java中集合的迭代原理,如果你研究过集合的源码,就会很容易理解这个模式。

场景+代码

场景

现在有两家餐厅:煎饼店和饭店,它们想要合并。两家店的菜单需要统一起来,以便服务员给客户看。

但此时有一个问题,煎饼店和饭店的菜单,实现是不一样的:

/**
 * 菜单项
 */
public class MenuItem {
    String name;
    double price;

    public MenuItem() {}

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

    public String getName() {
        return name;
    }

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

    public double getPrice() {
        return price;
    }

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

/**
 * 煎饼菜单
 */
public class PancakeMenu {
    ArrayList<MenuItem> menuItems = new ArrayList<>();

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

    public PancakeMenu() {
        initItems();
    }

    public void initItems(){
        addItem("葱花饼", 3.5);
        addItem("牛肉饼", 6);
        addItem("猪肉饼", 5);
        addItem("韭菜饼", 2.5);
    }
}

/**
 * 午餐菜单
 */
public class DinnerMenu {
    MenuItem[] menuItems = new MenuItem[20];
    int numberOfItems = 0;

    public DinnerMenu() {
        initItems();
    }

    public void addItem(String name,double price){
        MenuItem menuItem = new MenuItem(name, price);
        if(numberOfItems >= menuItems.length){
            System.out.println("菜单已满,不能再添加!");
            return ;
        }
        menuItems[numberOfItems] = menuItem;
        numberOfItems++;
    }

    public Iterator<MenuItem> createIterator() {
        return new DinnerMenuIterator(menuItems);
    }

    public void initItems(){
        addItem("青椒肉丝炒饭", 12);
        addItem("番茄鸡蛋炒饭", 10);
        addItem("鱼香茄子炒饭", 11);
        addItem("土豆烧鸡炒饭", 12.5);
    }
}

从代码我们可以看出,两家店的菜单一个是数组实现,一个是集合实现。

这时,我们想要服务员提供新的菜单时,得分别针对两种不同的底层实现,进行不同的迭代:

    /**
     * 打印出菜单
     */
    public void printMenu() {
        ArrayList<MenuItem> pMenuItems = pancakeMenu.menuItems;
        for (int i = 0; i < pMenuItems.size(); i++) {
            MenuItem menuItem = pMenuItems.get(i);
            System.out.println(menuItem.getName()+":"+menuItem.getPrice());
        }

        MenuItem[] dMenuItems = dinnerMenu.menuItems;
        for (int i = 0,n = dMenuItems.length; i < n; i++) {
            MenuItem menuItem = dMenuItems[i];
            System.out.println(menuItem.getName()+":"+menuItem.getPrice());
        }
    }

这个代码产生了几个问题:

  1. 破坏了封装,暴露了菜单的实现细节
  2. 扩展性不好,如果现在有一家新的咖啡店加盟,它的菜单实现是HashMap,那又得重新增加新的代码。
  3. 迭代的部分实际上大同小异,我们一直在进行“重复性”的代码

如何改进?让我们思考一下之前学过的设计原则:

找出应用中可能需要变化之处,把他们独立出来。

每个菜单的迭代过程是不一样的,那我们可以将菜单的迭代动作封装出来呀。这就是迭代器模式

类图

我们先来看一下该模式的类图:

迭代动作被封装成了一个接口Iterator,每一种聚合(也就是不同底层实现的集合),都对应了一种具体的迭代器ConcreteIterator

实现

接下来我们结合场景来写出迭代器模式的详细代码:

/**
 * 迭代器接口
 */
public interface Iterator<E> {

    public boolean hashNext();

    public E next();
}
/**
 * 饭店菜单迭代器
 */
public class DinnerMenuIterator implements Iterator<MenuItem>{
    MenuItem[] menuItems;
    int position = 0;

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

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

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

}
/**
 * 煎饼菜单迭代器
 */
public class PancakeMenuIterator implements Iterator<MenuItem>{
    ArrayList<MenuItem> menuItems;
    int position = 0;

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

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

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

}

定义了两种菜单迭代器后,我们只需要把迭代器组合进对应的菜单,即可让菜单拥有对应的迭代功能

/**
 * 煎饼菜单
 */
public class PancakeMenu {
    ArrayList<MenuItem> menuItems = new ArrayList<>();

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

    public PancakeMenu() {
        initItems();
    }

    /**
     *  创建自己的迭代器
     */
    public Iterator<MenuItem> createIterator() {
        return new PancakeMenuIterator(menuItems);
    }

    public void initItems(){
        addItem("葱花饼", 3.5);
        addItem("牛肉饼", 6);
        addItem("猪肉饼", 5);
        addItem("韭菜饼", 2.5);
    }
}

/**
 * 午餐菜单
 */
public class DinnerMenu {
    MenuItem[] menuItems = new MenuItem[20];
    int numberOfItems = 0;

    public DinnerMenu() {
        initItems();
    }

    public void addItem(String name,double price){
        MenuItem menuItem = new MenuItem(name, price);
        if(numberOfItems >= menuItems.length){
            System.out.println("菜单已满,不能再添加!");
            return ;
        }
        menuItems[numberOfItems] = menuItem;
        numberOfItems++;
    }
    /**
     *  创建自己的迭代器
     */
    public Iterator<MenuItem> createIterator() {
        return new DinnerMenuIterator(menuItems);
    }

    public void initItems(){
        addItem("青椒肉丝炒饭", 12);
        addItem("番茄鸡蛋炒饭", 10);
        addItem("鱼香茄子炒饭", 11);
        addItem("土豆烧鸡炒饭", 12.5);
    }
}

现在的服务员,不需要清楚菜单的具体设计,打印总菜单时,只需要调用各自菜单的迭代器即可。而且即使扩展增加新的菜单时,也更加容易:

/**
 * 服务员
 */
public class Waitress {
    PancakeMenu pancakeMenu = new PancakeMenu();
    DinnerMenu dinnerMenu = new DinnerMenu();

    /**
     * 打印出菜单
     */
    public void printMenu() {
        Iterator<MenuItem> iterator = pancakeMenu.createIterator();
        printMenu(iterator);
        iterator = dinnerMenu.createIterator();
        printMenu(iterator);
    }

    private void printMenu(Iterator<MenuItem> iterator){
        while(iterator.hashNext()){
            MenuItem menuItem = iterator.next();
            System.out.println(menuItem.getName()+":"+menuItem.getPrice());
        }
    }
}

新设计原则-单一责任原则

看到这里,有人可能会问,为什么要把迭代的功能抽离成一个接口、还搞组合这么麻烦呢?我们可以直接在各种菜单里面,加上自己的菜单迭代方法就行了呀。

这样同样不会破坏封装,服务员打印订单时调用菜单的打印方法,不会暴露出菜单的内部具体细节。但是,这样做,有两个不好的地方:

  1. 每个菜单都要实现各自的迭代方法,如果新加盟店的菜单实现和已经合并的店一致,那又将造成重复性代码
  2. 违法了一个设计原则:单一责任原则

我们先看一下迭代器模式的优点:

迭代器模式能够不暴露聚合内部的具体实现,而且也让聚合任务减轻,把“游走”的任务放在了迭代器上,简化了聚合的接口和实现,也让任务各得其所

加粗部分就是单一责任原则的体现。我们现在来正式看一下定义:

单一责任原则:一个类应该只有一个引起变化的原因

之所以我们要让一个类只有一个改变的原因,在于:我们需要避免类的改变,而类的责任越多时,它改变的机率就越大。而且,当类真的改变时,两个责任的代码都可能受到影响

组合模式

定义

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

当你有数个对象的集合,它们彼此直接有“整体/部分”的关系,而且你想用一致的方式来对待这些对象时,就需要用到组合模式。组合模式通常是用树形结构

场景+代码

场景

还是采用上面的菜单场景。

首先,细心的朋友会发现上面的服务员代码仍有些不完美的地方:

/**
 * 服务员
 */
public class Waitress {
    PancakeMenu pancakeMenu = new PancakeMenu();
    DinnerMenu dinnerMenu = new DinnerMenu();

    /**
     * 打印出菜单
     */
    public void printMenu() {
        Iterator<MenuItem> iterator = pancakeMenu.createIterator();
        printMenu(iterator);
        iterator = dinnerMenu.createIterator();
        printMenu(iterator);
    }
    ……
}

菜单没有被统一管理起来,我们定义了两个菜单成员变量,调用了两次printMenu方法。当菜单增多时,这个数目还得继续增加……

思考一下,我们可以设计一个菜单父类接口,让现有的煎饼菜单和正餐菜单实现这个接口,然后服务员只需要拥有一个抽象菜单类的数组,在打印菜单时遍历数组,使用元素的迭代器就可以了。

public interface Menu {
    public abstract void addItem(String name,double price);
    public abstract Iterator<MenuItem> createIterator();
}

public class Waitress {
    List<Menu> menus = new ArrayList<>();

    /**
     * 打印出菜单
     */
    public void printMenu() {
        for (Menu menu : menus) {
            Iterator<MenuItem> iterator = menu.createIterator();
            printMenu(iterator);
        }
    }

    private void printMenu(Iterator<MenuItem> iterator){
        while(iterator.hashNext()){
            MenuItem menuItem = iterator.next();
            System.out.println(menuItem.getName()+":"+menuItem.getPrice());
        }
    }
}

看上去很完美,但不幸,现在来了新的需求:给正餐菜单增加“甜点”子菜单。如图,让甜点菜单变成正餐菜单中的一个子节点,我们想要的类似下图。但很明显,现在的设计无法满足:

类图

怎么办?这个时候我们就要采用组合模式了。我们先看看它的类图:

我们需要某种树状结构来容纳嵌套菜单和菜单项,正如类图所似。

代码

根据类图,我们设计出一个Component抽象类,让菜单(Composite)和菜单项(Leaf)继承它:

public abstract class MenuComponent {
    //=======菜单(组合节点)的方法=======
    /**
     * 增加菜单项
     */
    public void add(MenuComponent component) {
        throw new UnsupportedOperationException();
    }
    /**
     * 删除指定菜单项
     */
    public void remove(MenuComponent component) {
        throw new UnsupportedOperationException();
    }
    /**
     * 获取指定下标的菜单项
     */
    public MenuComponent get(int i) {
        throw new UnsupportedOperationException();
    }

    //=======菜单项(叶子节点)的方法=======
    /**
     * 获取菜单项的名称
     */
    public String getName() {
        throw new UnsupportedOperationException();
    }

    /**
     * 获取菜单项的价格
     */
    public double getPrice() {
        throw new UnsupportedOperationException();
    }
    /**
     * 打印出菜单中的所有菜单项的信息,或者单个菜单项的信息
     * 菜单和菜单项都要用到的方法
     */
    public void print(){

    }
}

/**
 * 组合菜单
 */
public class Menu extends MenuComponent{
    List<MenuComponent> menuComponents = new ArrayList<>();
    String name;
    String description;

    public Menu() {}

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

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

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

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

    @Override
    public void print() {
        System.out.println("菜单名称:"+getName()+"("+getDescription()+")");
        for (MenuComponent menuComponent : menuComponents) {
            menuComponent.print();
        }
    }

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

    public String getDescription() {
        return description;
    }
}
/**
 * 菜单项
 */
public class MenuItem extends MenuComponent{
    String name;
    double price;

    public MenuItem() {}

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

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

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

    @Override
    public void print() {
        System.out.println(name+":"+price);
    }

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


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

要注意的是,抽象父类中,我们同时将菜单项和菜单的方法定义了出来,继承时,菜单项和菜单只需要覆盖各自的实现即可。这很明显违法了我们上面提到的“单一责任”设计原则,但在具体场景下,为了需求和设计,我们必须采用折中的方式。

结合新的“组合”菜单设计,服务员现在变得很幸福,我们来看看服务员的新代码:

/**
 * 服务员
 */
public class Waitress {
    MenuComponent allMenus;

    public Waitress(MenuComponent allMenus) {
        this.allMenus = allMenus;
    }

    /**
     * 打印出菜单
     */
    public void printMenu() {
        allMenus.print();
    }

}

测试:

public class Main {
    public static void main(String[] args) {
        //建造烧饼菜单
        MenuComponent pancakeMenu = new Menu("早餐","原烧饼店菜单");
        pancakeMenu.add(new MenuItem("葱花饼", 3.5));
        pancakeMenu.add(new MenuItem("牛肉饼", 6));
        pancakeMenu.add(new MenuItem("猪肉饼", 5));
        pancakeMenu.add(new MenuItem("韭菜饼", 2.5));
        //建造正餐菜单子菜单-甜点菜单
        MenuComponent dessertMenu = new Menu("甜点","正餐子菜单");
        dessertMenu.add(new MenuItem("芒果牛奶冰", 8));
        dessertMenu.add(new MenuItem("珍珠果豆花", 6));
        dessertMenu.add(new MenuItem("榴莲蛋黄", 8));
        //建造正餐菜单
        MenuComponent dinnerMenu = new Menu("正餐","原饭店菜单");
        dinnerMenu.add(new MenuItem("青椒肉丝炒饭", 12));
        dinnerMenu.add(new MenuItem("番茄鸡蛋炒饭", 10));
        dinnerMenu.add(new MenuItem("鱼香茄子炒饭", 11));
        dinnerMenu.add(new MenuItem("土豆烧鸡炒饭", 12.5));
        dinnerMenu.add(dessertMenu);
        //建造主菜单
        MenuComponent totalMenu = new Menu("菜单", "总菜单");
        totalMenu.add(pancakeMenu);
        totalMenu.add(dinnerMenu);
        //服务员打印菜单
        Waitress waitress = new Waitress(totalMenu);
        waitress.printMenu();
    }
}/**Output:
菜单名称:菜单(总菜单)
菜单名称:早餐(原烧饼店菜单)
葱花饼:3.5
牛肉饼:6.0
猪肉饼:5.0
韭菜饼:2.5
菜单名称:正餐(原饭店菜单)
青椒肉丝炒饭:12.0
番茄鸡蛋炒饭:10.0
鱼香茄子炒饭:11.0
土豆烧鸡炒饭:12.5
菜单名称:甜点(正餐子菜单)
芒果牛奶冰:8.0
珍珠果豆花:6.0
榴莲蛋黄:8.0
*/

扩展:组合迭代器

我们现在再扩展一下,这种组合菜单如何设计迭代器呢?细心的朋友应该观察到,我们刚才使用的迭代都是递归调用的菜单项和菜单内部迭代的方式。

现在我们想设计一个外部迭代的方式怎么办?譬如出现一个新需求:服务员需要打印出蔬菜性质的所有食品菜单。

首先,我们给MenuComponent加上判断蔬菜类食品的方法,然后在菜单项中进行重写:

public abstract class MenuComponent {

    …………
    /**
     * 判断是否为蔬菜类食品
     */
    public boolean isVegetarian() {
        throw new UnsupportedOperationException();
    }
}
/**
 * 菜单项
 */
public class MenuItem extends MenuComponent{
    String name;
    double price;
    /**蔬菜类食品标志*/
    boolean vegetarian;

    …………

    public boolean isVegetarian() {
        return vegetarian;
    }

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

}   

然后我们设计一个新的组合菜单迭代器。注意,这个迭代器中使用了递归的算法,弄懂递归后理解代码就不难:

public class CompositeIterator implements Iterator<MenuComponent>{
    /**迭代器栈,存储最近一次迭代的菜单的迭代器*/
    Stack<Iterator<MenuComponent>> stack = new Stack<>();

    public CompositeIterator() {}

    public CompositeIterator(Iterator<MenuComponent> iterator) {
        //初始化时中将组合菜单的总迭代器(即List<MenuComponent>的迭代器)放入栈
        stack.push(iterator);
    }
    @Override
    public boolean hasNext() {
        if(stack.isEmpty())
            return false;
        //返回栈顶的迭代器
        Iterator<MenuComponent> iterator = stack.peek();
        //如果该迭代器仍有元素可迭代,返回true
        if(iterator.hasNext()){
            return true;
        }
        //如果迭代器元素都已迭代完,弹出栈,递归判断栈中下一个迭代器
        else {
            stack.pop();
            return hasNext();
        }
    }

    @Override
    public MenuComponent next() {
        //如果没有元素可迭代,直接返回null
        if(!hasNext())
            return null;
        //有元素可迭代时,返回栈顶迭代器
        Iterator<MenuComponent> iterator = stack.peek();
        //取出迭代器中的下一个元素
        MenuComponent component = iterator.next();
        //如果该元素为菜单,将子菜单的迭代器放入栈中。这样下一次执行next()方法,则迭代该子菜单了
        if(component instanceof Menu){
            stack.push(component.createIterator());
        }

        return component;
    }
}

再在组合菜单中增加创建迭代器的方法:

public abstract class MenuComponent {

        …………

    /**
     * 创建迭代器
     * @return 
     */
    public abstract Iterator<MenuComponent> createIterator();
}
/**
 * 菜单项
 */
public class MenuItem extends MenuComponent{

    …………

    @Override
    public Iterator<MenuComponent> createIterator() {
        return new NullIterator();
    }
}   

/**
 * 组合菜单
 */
public class Menu extends MenuComponent{

    …………

    @Override
    public Iterator<MenuComponent> createIterator() {
        return new CompositeIterator(menuComponents.iterator());
    }
}

注意,NullIterator是之前讲过的“空对象”设计思想的一种体现:

/**
 * 空迭代器,空对象思想的体现
 */
public class NullIterator implements Iterator<MenuComponent>{

    @Override
    public boolean hasNext() {
        return false;
    }

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

}

最后,我们给服务员增加新的打印蔬菜食品菜单的方法:

    /**
     * 打印蔬菜食品
     */
    public void printVegetarianMenu() {
        Iterator<MenuComponent> iterator = allMenus.createIterator();

        while (iterator.hasNext()) {
            MenuComponent component = iterator.next();
            try{
                if(component.isVegetarian())
                    System.out.println(component.getName());
            }
            //如果为菜单调用isVegetarian方法,会直接抛出异常,这里会直接不处理
            catch (UnsupportedOperationException e) {}
        }
    }

测试:

public class Main {
    public static void main(String[] args) {
        //建造烧饼菜单
        MenuComponent pancakeMenu = new Menu("早餐","原烧饼店菜单");
        pancakeMenu.add(new MenuItem("葱花饼", 3.5,true));
        pancakeMenu.add(new MenuItem("牛肉饼", 6,false));
        pancakeMenu.add(new MenuItem("猪肉饼", 5,false));
        pancakeMenu.add(new MenuItem("韭菜饼", 2.5,true));
        //建造正餐菜单子菜单-甜点菜单
        MenuComponent dessertMenu = new Menu("甜点","正餐子菜单");
        dessertMenu.add(new MenuItem("芒果牛奶冰", 8,false));
        dessertMenu.add(new MenuItem("珍珠果豆花", 6,false));
        dessertMenu.add(new MenuItem("榴莲蛋黄", 8,false));
        //建造正餐菜单
        MenuComponent dinnerMenu = new Menu("正餐","原饭店菜单");
        dinnerMenu.add(new MenuItem("青椒肉丝炒饭", 12,false));
        dinnerMenu.add(new MenuItem("番茄鸡蛋炒饭", 10,true));
        dinnerMenu.add(new MenuItem("鱼香茄子炒饭", 11,false));
        dinnerMenu.add(new MenuItem("土豆烧鸡炒饭", 12.5,false));
        dinnerMenu.add(dessertMenu);
        //建造主菜单
        MenuComponent totalMenu = new Menu("菜单", "总菜单");
        totalMenu.add(pancakeMenu);
        totalMenu.add(dinnerMenu);
        //服务员打印菜单
        Waitress waitress = new Waitress(totalMenu);
        //waitress.printMenu();
        waitress.printVegetarianMenu();
    }
}/**Output:
葱花饼
韭菜饼
番茄鸡蛋炒饭
*/

组合模式的迭代器就设计好啦~~


本文总结自:

《Head First 设计模式》第九章:迭代器和组合模式

猜你喜欢

转载自blog.csdn.net/z55887/article/details/70548861