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

迭代器与组合模式

面对不同的问题,自然会用到不同的数据结构,甚至相同的问题也可以用不同的数据结构来实现。比如数组和ArrayList都可以构造一个列表。

但当想获得集合内的元素时,直接的取用就会涉及到集合内部的具体实现,而这些往往不是我们希望暴露给外部的。因此,可以为集合设计迭代器来遍历集合内元素,随后只需向外部提供迭代器,就可以隐藏内部的实现。

迭代器模式

考虑两个餐厅的菜单的合并,其中一个使用数组保存菜单,而另一个使用ArrayList保存菜单。而两个餐厅都不想修改自己的数据结构,因为会涉及大量的代码改动(这同样适用于现实中的其他问题)。

package collection;

import java.util.Iterator;

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

    public DinerMenu() {
        menuItems = new MenuItem[MAX_ITEMS];

        addItem("Vegetarian BLT",
                "(Fakin') Bacon with lettuce & tomato on whole wheat",
                true,
                2.99);

        addItem("BLT",
                "Bacon with lettuce & tomato on whole wheat",
                false,
                2.99);

        addItem("Soup of the day",
                "Soup of the day, with a side of potato salad",
                false,
                3.29);

        addItem("Hotdog",
                "A hot dog, with saurkraut, relish, onions, topped with cheese",
                false,
                3.05);
    }

    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++;
        }
    }

    public MenuItem[] getMenuItems() {
        return getMenuItems;
    }
}
package collection;

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

public class PancakeHouseMenu {
    ArrayList menuItems;

    public PancakeHouseMenu() {
        menuItems = new ArrayList();

        addItem("K&B's Pancake Breakfast",
                "Pancakes with scrambled eggs, and toast",
                true,
                2.99);

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

        addItem("Blueberry Pancake Breakfast",
                "Pancakes made with fresh blueberries",
                true,
                3.49);

        addItem("Waffles",
                "Waffles, with your choice of blueberries or strawberries",
                true,
                3.59);
    }

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

    public ArrayList getMenuItems() {
        return menuItems;
    }
}

如果直接地获取两个餐厅的菜单,就需要用两个循环,在每个循环中针对其中一个餐厅的内部实现来编程。这会造成大量的重复代码。并且,如果某个餐厅改变的内部实现,每个关于其的循环内的代码都需要修改。

为了不暴露内部实现,应该设计迭代器来遍历集合。

package collection;

import java.util.Iterator;

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

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

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

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

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

public class PancakeHouseIterator implements Iterator {
    ArrayList items;
    int position = 0;

    public PancakeHouseIterator(ArrayList items) {
        this.items = items;
    }

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

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

现在可以移除两个餐厅类中直接获取内部集合的方法,用获取迭代器的方法代替。

但实际上,Java中已经提供Iterator接口,因此对于已经存在可用迭代的ArrayList,不必特地为其设计迭代器。而为了满足删除菜单中元素的要求,还需要在数组实现的餐厅的迭代器中实现remove方法。

@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;
        }
    }

现在显示菜单的方法仍然是针对具体的餐厅菜单类实现的,需要设计一个菜单接口类来抽象它们。

package collection;

import java.util.Iterator;

public class Waiter {
    Menu pancakeHouseMenu;
    Menu dinerMenu;

    public Waiter(Menu pancakeHouseMenu, Menu dinerMenu) {
        this.pancakeHouseMenu = pancakeHouseMenu;
        this.dinerMenu = dinerMenu;
    }

    public void printMenu() {
        Iterator pancakeIterator = pancakeHouseMenu.createIterator();
        Iterator dinerIterator = dinerMenu.createIterator();

        System.out.println("MENU\n----\nBREAKFAST");
        printMenu(pancakeIterator);
        System.out.println("\nLUNCH");
        printMenu(dinerIterator);
    }

    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());
        }
    }
}

现在我们成功地把具体的实现和调用的方法分开了,显示菜单的方法不关注餐厅菜单的具体数据结构。

设计原则

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

当一个类具有多个改变的原因时,在未来修改该类的代码的概率就会很高,维护成本高。

针对这种问题,应该尽量让每个类保持单一的责任,控制改变的原因。

内聚是一个比单一责任原则更普遍的的概念,当一个模块或一个类被设计成只支持一组相关的功能时,我们说它具有高内聚;反之,说它具有低内聚。高内聚的类更加容易维护。

组合模式

使用迭代器模式的餐厅合并已经有了不错的效率,如果有更多的餐厅想要加入,为了方便,也可以用ArrayList来保存不同餐厅的菜单,然后在先显示菜单的方法中遍历这些菜单即可。

但这样的结构仍然缺乏弹性:无法处理嵌套的子菜单

因此必须使用树的结构来嵌套菜单。这种层次结构能让客户以一致的方式处理个别对象以及对象组合,称作组合模式

通过组合模式,对象的组合和个别对象之间的差别就被忽略了

package collection;

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();
    }
}

首先编写一个树结点的抽象类,该类提供了叶节点和组合节点需要实现的方法。

然后修改MenuItem类,令其继承MenuComponent类,作为叶节点,并额外实现print方法。

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

修改Menu类,令其继承MenuComponent类,作为组合节点,并额外实现print方法和getChild方法以及addremove等方法。此时Menu类不再是接口,而是一种组合节点类。

package collection;

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

public class Menu extends MenuComponent {
    ArrayList 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 (MenuComponent) menuComponents.get(i);
    }

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

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

    @Override
    public void print() {
        System.out.print("\n" + getName());
        System.out.println(", " + getDescription());
        System.out.println("---------------------");

        Iterator iterator = menuComponents.iterator();
        while (iterator.hasNext()) {
            MenuComponent menuComponent = (MenuComponent) iterator.next();
            menuComponent.print();
        }
    }
}

现在实现Waiter类十分简单,只需要保存一个根节点,然后调用根节点的print即可。根节点在打印的自己的过程中会递归地调用子节点的print方法。注意,此时我们不需要原来的餐厅菜单类了,抽取其中的餐品信息即可。

package collection;

public class Waiter {
    MenuComponent allMenus;

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

    public void printMenu() {
        allMenus.print();
    }
}

虽然组合模式在管理层次的同时也执行了菜单的操作,但换取得到了透明性。现在用户可以将组合节点和叶节点视作相同的节点。

组合迭代器

如果不想每次都输出菜单中所有的内容,而是有条件地筛选,就需要使用迭代器来遍历,组合模式也很好地兼容了迭代器的使用。在组合节点的迭代器中递归地使用子节点的迭代器即可。

package collection;

import java.util.Iterator;
import java.util.Stack;

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

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

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

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

组合节点的迭代器要使用上面这种新类型。这种迭代器深度优先地遍历组合节点内的所有元素。

package collection;

import java.util.Iterator;

public class NullIterator implements Iterator {

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

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

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

叶节点的迭代器使用上面这种空迭代器,这是为了避免用户需要判断是否真正获取了迭代器。

现在我们来用迭代器显示素食菜单。

public void printVegetarianMenu() {
        Iterator iterator = allMenus.createIterator();
        System.out.println("\nVEGETARIAN MENU\n----");
        while (iterator.hasNext()) {
            MenuComponent menuComponent = (MenuComponent) iterator.next();
            try {
                if (menuComponent.isVegetarian()) {
                    menuComponent.print();
                }
            } catch (UnsupportedOperationException e) {}
        }
    }
posted @ 2020-05-07 16:20  Aries99C  阅读(495)  评论(0)    收藏  举报