您的位置:首页 > 其它

Design Pattern学习笔记之迭代器模式和复合模式(the Iterator and Composite Pattern)

2012-12-19 14:42 405 查看

Design Pattern学习笔记之迭代器模式和复合模式(the Iterator and Composite Pattern)

1.    引子--Whois?

我们有很多种将对象塞入集合的方式,可以用array、stack、list、hashtable等等,以上的每一种都各有优缺点。但是当我们考虑遍历这些集合的问题时,你是否打算向外部暴露你如何实现集合的细节(array or stack)?作为一个面向对象的设计人员而言,答案肯定是否定的。那么,本章接下来的内容将为你展示如何在封装集合实现细节的前提下,让客户端遍历集合中的元素;另外还将展示一种令人印象深刻的跨越不同数据的遍历方法;如果这些内容还不够丰富,那,我们还会讨论类的职责,引入新的OO设计准则。

2.    问题来了—支持不同菜单

提供早餐的Pancake餐厅和提供午餐的Diner餐厅合并了,以后你在任意一家餐厅既可以点早餐也可以点午餐了,那两家的菜单系统也要合并到一起,问题来了,我们看一下现有的两家餐厅的菜单实现。

两家餐厅对菜单中的每一个菜单项定义倒是相同的,都有名称、描述、是否素食、价格四个属性,我们来看看源代码:

public
class
MenuItem {
    String name;
    String description;
    boolean
vegetarian;
    double
price;
 
    public MenuItem(String name,

                    Stringdescription,
                    boolean vegetarian,

                    double price)

    {
       this.name = name;
       this.description = description;
       this.vegetarian = vegetarian;
       this.price = price;
    }
 
    public String getName() {
       return
name;
    }
 
    public String getDescription() {
       return
description;
    }
 
    public
double
getPrice() {
       return
price;
    }
 
    public
boolean
isVegetarian() {
       return
vegetarian;
    }
    public String toString() {
       return (name +
", $" + price +
"\n   " + description);
    }
}

Diner餐馆的菜单实现

public
class
DinerMenu {
    static
final int
MAX_ITEMS = 6;
    int
numberOfItems = 0;
    MenuItem[] menuItems;
 
    public DinerMenu() {
       menuItems = new MenuItem[MAX_ITEMS];
 
       addItem("VegetarianBLT",
           "(Fakin')Bacon with lettuce & tomato on whole wheat",
true, 2.99);
       addItem("BLT",
           "Bacon withlettuce & tomato on whole wheat",
false, 2.99);
       addItem("Soup of theday",
           "Soup of theday, with a side of potato salad",
false, 3.29);
       addItem("Hotdog",
           "A hot dog,with saurkraut, relish, onions, topped with cheese",
           false, 3.05);
       addItem("SteamedVeggies and Brown Rice",
           "Steamedvegetables over brown rice",
true, 3.99);
       addItem("Pasta",
           "Spaghettiwith Marinara Sauce, and a slice of sourdough bread",
           true, 3.89);
    }
 
    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, menuis full!  Can't add item to menu");
       } else {
           menuItems[numberOfItems] = menuItem;
           numberOfItems =
numberOfItems + 1;
       }
    }
 
    public MenuItem[] getMenuItems() {
       return
menuItems;
    }
    // other menu methods here
}

Pancake餐馆的菜单实现

public
class
PancakeHouseMenu implements Menu {
    ArrayList menuItems;
 
    public PancakeHouseMenu() {
       menuItems = new ArrayList();
   
       addItem("K&B'sPancake Breakfast",

           "Pancakeswith scrambled eggs, and toast",

           true,
           2.99);
 
       addItem("RegularPancake Breakfast",

           "Pancakeswith fried eggs, sausage",

           false,
           2.99);
 
       addItem("BlueberryPancakes",
           "Pancakesmade with fresh blueberries, and blueberry syrup",
           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;
    }
    // other menu methods here
}

可以看到两家餐馆的菜单使用了两种不同的集合实现方式,为了说明这两种不同的实现方式带来的问题,我们引入虚拟的服务员对象,她还在合并后的餐馆上班,就需要应付两种不同的菜单。来看看服务员要实现的接口:

    printMenu();prints every item on the menu

    printBreakfastMenu();prints just breakfast items.

    printLunchMenu();prints just lunch items.

    printVegetarianMenu();prints all vegetarian menu items.

    isItemVegetarian(name);given the name of an item, returns true if the items is vegetarian, otherwise,return false.

    试着来实现第一个接口,打印出所有菜单项,按照目前两个餐馆菜单的实现方式,只能如下实现:

    PancakeHouseMenu pancakeHouseMenu =
new PancakeHouseMenu();
    ArrayList<MenuItem> breakfastItems =
pancakeHouseMenu.getMenuItems();
   
    DinerMenu dinerMenu =
new
DinerMenu();
    MenuItem[] lunchItems =
dinerMenu.getMenuItems();
    public
void
print(){
        for(int i = 0; i <
breakfastItems.size();i++){
           MenuItem menuItem = breakfastItems.get(i);
           System.out.print(menuItem.getName() +
" ");
           System.out.println(menuItem.getPrice() +
" ");
           System.out.println(menuItem.getDescription());
       }
       for(int i = 0; i <
lunchItems.length; i++){
           MenuItem menuItem = lunchItems[i];
           System.out.print(menuItem.getName() +
" ");
           System.out.println(menuItem.getPrice() +
" ");
           System.out.println(menuItem.getDescription());
       }     
    }

上面的代码绝对不能让人愉悦,要完成打印的工作,使用了两个看起来几乎一样的循环,更让人痛苦的是,服务员要实现的每一个接口几乎都要做同样的工作。从OO设计的角度总结下服务员类存在的问题(当然该问题并不是服务员类自身造成的):

1.  没有实现面向接口编程(面向具体的PancakeHouseMenu和DinerMenu)

2.  没有封装变化(如果改变任何一个餐馆菜单的实现方式,比如换成hashtable,都要修改很多代码)

3.  没有隐藏信息(服务员类需要知道每个餐馆菜单的具体实现方式)

4.  存在大量的代码复制

3.    改进—应用迭代器模式

两家餐馆都不愿意修改代码(或者说是为了容忍对方而单单修改自己的代码),这就把我们推到了一个比较尴尬的境地,如何解决以上问题?如果我们让两个集合都实现同样的接口,那就有可能让服务员类只看到接口,消除重复的循环。

从前面的介绍中,我们学习到要封装变化,在当前面临的遍历菜单的问题,我们打算把遍历一个集合这件事情封装起来。

回头再看一下遍历两个不同菜单的操作:

for(int i = 0; i <
breakfastItems.size();i++){
           MenuItem menuItem = breakfastItems.get(i);
       }
       for(int i = 0; i <
lunchItems.length; i++){
           MenuItem menuItem = lunchItems[i];
       }
我们打算引入一个新的对象,Iterator,由他来封装对集合的遍历操作。那它会是下面的样子:
Iterator pancakeIterator = breakfastItems.createIterator();
while (iterator.hasNext()) {
    MenuItem menuItem =(MenuItem)iterator.next();
}
以上实现就暗合迭代器模式的思想,迭代器模式基于迭代器接口,任何一种集合实现,只要实现迭代器接口,我们在进行遍历时都可以按照以上方式进行遍历,从而简化客户端的工作。
我们定义迭代器接口,并为Diner餐馆的菜单类增加迭代器:
public
interface
Iterator {
    boolean hasNext();
    Object next();
}
public
class
DinerMenuIterator implements Iterator {
    MenuItem[] items;
    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() {
       if (position >=
items.length ||
items[position] ==
null) {
           return
false
;
       } else {
           return
true
;
       }
    }
}
我们为Diner餐馆的Menu类增加创建迭代器的方法,对Pancake餐馆的菜单类也进行类似改造,两个Menu类都支持创建Iterator,那现在的服务员类打印菜单的方法就变成下面的样子:
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());
       }
    }
改进后有什么效果?
1.      两家餐厅的工作人员很开心,因为他们只需做很小的改动就能实现目标(我们提供迭代器类后,只需在两个餐馆的menu类中增加创建迭代器的方法);
2.      服务员类的代码变得更容易维护和扩展。
更具体的来说:
1.      应用迭代器之前,menu类的实现细节暴露给了服务员类(服务员类知道你使用的是ArrayList);应用迭代器之后,实现了对实现细节的封装。
2.      应用迭代器之前,有两个for循环,存在重复代码;应用之后,无论哪种实现的menu,都能使用一个循环。
3.      应用迭代器之前,服务员代码与具体类(MenuItem[],ArrayList)绑定;应用迭代器之后,面向接口编程(Iterator)。
4.      应用迭代器之前,服务员代码与具体的menu类绑定;应用之后,依然与具体类的Menu类绑定,虽然两个类看起来差不多。
再次改进之前,我们先来看看现在的类结构:

4.    再次改进

经过第一次改进,我们的服务员类在遍历两家餐馆的菜单时,方便了不少,从原来的依赖于每一家餐馆的菜单实现,到依赖于Iterator接口,降低了类之间的耦合度,使用迭代器封装对菜单的遍历,是个了不起的改进。
我们取得了进步,但远不完美,服务员类看上去要依赖于两家餐馆的菜单类,而这两个类看起来差不多,下一步,我们希望解除这一部分的依赖。
改进的方式和第一次的方式相同,我们先为两个菜单类定义公共的接口,由于在所举的例子中,我们的主要目的是遍历菜单,因此为了灵活起见,定义一个超级简单的接口。
public
interface
Menu {
    public Iterator createIterator();
}
接口只有一个方法,就是创建一个迭代器。
另外一个变化是,我们要使用jdk的迭代器来替换自己定义的迭代器(之前自己实现迭代器是为了讲述的方便),看看jdk中迭代器的接口。
public
interface
Iterator<E> {
    boolean hasNext();
    E next();
    void remove();
}
和自己写的迭代器唯一的差别是多了一个remove方法(remove的含义是删除迭代器当前位置的前一个元素)。可以根据实际需要选择实现remove方法,或者简单抛出UnsupportedOerationException异常。
看看两个菜单类对Menu接口的实现:
由于ArrayList提供了创建迭代器的实现,因此它更简单。
    Pancake餐馆的menu类
    public
class
PancakeHouseMenu implements Menu {
    ArrayList menuItems;
 
    public PancakeHouseMenu() {
       menuItems = new ArrayList();
   
       addItem("K&B's PancakeBreakfast",

           "Pancakeswith scrambled eggs, and toast",

           true,
           2.99);
 
       addItem("RegularPancake Breakfast",

           "Pancakeswith fried eggs, sausage",

           false,
           2.99);
 
       addItem("BlueberryPancakes",
           "Pancakesmade with fresh blueberries, and blueberry syrup",
           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;
    }
 
    public Iterator
createIterator() {
       return
menuItems.iterator();
    }
 
    // other menu methods here
}
数组本身没有提供创建迭代器的实现,我们还需要自己创建迭代器,不过要新增remove方法的是实现。
Diner餐馆的迭代器:
public
class
DinerMenuIterator implements Iterator {
    MenuItem[] list;
    int
position = 0;
 
    public DinerMenuIterator(MenuItem[] list) {
       this.list = list;
    }
 
    public Object next() {
       MenuItem menuItem = list[position];
       position =
position + 1;
       return menuItem;
    }
 
    public
boolean
hasNext() {
       if (position >=
list.length ||
list[position] ==
null) {
           return
false
;
       } else {
           return
true
;
       }
    }
 
    public
void
remove() {
       if (position <= 0) {
           throw
new
IllegalStateException
              ("You can'tremove an item until you've done at least one next()");
       }
       if (list[position-1] !=
null) {
           for (int i =
position-1; i < (list.length-1); i++) {
              list[i] =
list[i+1];
           }
           list[list.length-1] =
null;
       }
    }
}
Diner餐馆的菜单类:
public
class
DinerMenu implements Menu {
    static
final int
MAX_ITEMS = 6;
    int
numberOfItems = 0;
    MenuItem[] menuItems;
 
    public DinerMenu() {
       menuItems = new MenuItem[MAX_ITEMS];
 
       addItem("VegetarianBLT",
           "(Fakin')Bacon with lettuce & tomato on whole wheat",
true, 2.99);
       addItem("BLT",
           "Bacon withlettuce & tomato on whole wheat",
false, 2.99);
       addItem("Soup of theday",
           "Soup of theday, with a side of potato salad",
false, 3.29);
       addItem("Hotdog",
           "A hot dog,with saurkraut, relish, onions, topped with cheese",
           false, 3.05);
       addItem("SteamedVeggies and Brown Rice",
           "Steamedvegetables over brown rice",
true, 3.99);
       addItem("Pasta",
           "Spaghettiwith Marinara Sauce, and a slice of sourdough bread",
           true, 3.89);
    }
 
    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, menuis full!  Can't add item to menu");
       } else {
           menuItems[numberOfItems] = menuItem;
           numberOfItems =
numberOfItems + 1;
       }
    }
 
    public MenuItem[] getMenuItems() {
       return
menuItems;
    }
 
    public Iterator createIterator() {
       return
new
DinerMenuIterator(menuItems);
       //return newAlternatingDinerMenuIterator(menuItems);
    }
 
    // other menu methods here
}
改进后的服务员类:
public
class
Waitress {
    Menu pancakeHouseMenu;
    Menu dinerMenu;
 
    public Waitress(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);
    }
}
改进后的类图:

服务类看到的都是接口。

5.    理论篇—迭代器模式的定义

The IteratorPattern provides a way to access the elements of an aggregate objectsequentially without exposing its underlying representation.

迭代器模式提供一种隐藏集合底层实现,顺序访问集合对象元素的方法。

迭代器模式的类图:

迭代器模式将集合的具体实现隐藏起来,对外提供统一的遍历接口,使得客户端能使用统一的接口处理集合元素。

迭代器模式的引入让集合类只关注集合的存储和管理,把集合的遍历从集合类本身中分割出来,符合一个类有且只有一种职责的设计准则。

6.    不辨不明—没有傻问题

Q:在其他教科书里面,看到讲解Iterator模式时,接口使用的方法是first, next, isDone和currentItem,这几个接口和我们之前讲的Iterator接口有什么区别?

A:两种接口实际上是一样的,随着技术的推移,jdk对接口进行修改,更方便使用。可以看出,next和currentItem合并成了新的next;isDone就是hasNext;新接口中抛弃了first,因为在jdk中,重头遍历的一般做法是重新创建迭代器;新接口扩展了remove。由此可见,迭代器的接口总是随着实际应用而变化,只要切合实际,方便使用就好,不用完全遵从统一的接口。

Q:听说迭代器有内部迭代器和外部迭代器两种,是这样么?我们之前介绍的是哪一种迭代器?

A:确实有这两种迭代器,我们介绍的属于外部迭代器,外部的意思是指由客户端来控制遍历,对遍历到的元素做一些操作;内部迭代器是指由迭代器自己控制遍历,我们需要对集合元素所做的操作在创建迭代器时,作为参数传入,由迭代器自己调用。由于外部迭代器由客户端自己控制,更灵活;但是内部迭代器使用起来更简单,只需传递集合和要做的操作,由迭代器完成剩余工作。

Q:本节介绍的迭代器是从前往后遍历的,能否创建从后往前遍历的迭代器?

A:当然可以。把next方法换成previous方法就可以了(hasNext方法也要做相应调整)。

Q:谁来决定迭代器遍历的顺序?

A:集合对象的遍历顺序由集合的底层实现方式和迭代器的实现决定,迭代器一般不考虑遍历时的顺序。如果确有要求,需要特殊考虑。

Q:在实际编程时,是否必须使用jdk的Iterator接口?

A:当然不是必须的。不过,使用jdk的接口,有个好处就是很多jdk定义的类内置了该接口的实现,不用自己写代码了;当然,如果原有的接口不能满足要求,你要扩展新的方法,也可以使用自己定义的接口。

Q:jdk的Enumeration接口和Iterator接口的区别在哪里?

A:Enumeration接口是比较老的接口(我们在适配器模式中提到过它),它的两个方法hasMoreElements,nextElement被替代成Iterator接口的hasNext和next,由于Iterator接口支持的类更多,因此推荐使用Iterator。

7.    理论篇—单一职责

回顾过去,如果把遍历的工作放到集合对象中,有什么问题?集合对象会多出来一堆遍历相关的方法,变得更复杂,仅此而已?

坏处当然不止这一点,关键的问题是,我们给集合类赋予了两个职责:管理集合元素和遍历集合元素,这样的话,如果这两项工作的任意一个发生变化(集合存储方式变化或者遍历方式变化),都会导致集合类的变化。其实有个新的OO准则来避免这种情况发生:

SingleResponsibility – A class should have only one reason to change. 单一职责原则,一个类只应该因为一个理由而变化。

OO设计准则为什么从另外一个角度定义?(而不是从类有且只有一个职责定义),是由于合理划分类的职责是设计工作中最困难的事情之一,我们往往把两个不同的职责合二为一,解决这个问题的唯一途径,就是不断检查我们的设计,是否有类会为两个不同的事情而改变,从而发现类职责划分的问题。

在软件设计中,还有一个词高内聚(cohesion),经常出现,这个词其实跟单一职责的准则息息相关:单一职责的类肯定是高内聚的;而有多个职责的类则很难做到高内聚,因为他往往要把很多不相干的方法放在一起。

看一个例子:

8.    再次改进—更简单的服务员类

使用迭代器设计模式后,原有的餐馆菜单变灵活了很多,我们再加入一家cafe餐馆,它的菜单使用另外一种集合(hashtable)的实现方式。

改造后的cafe餐馆菜单类

public
class
CafeMenu implements Menu {
    Hashtable menuItems =
new
Hashtable();
 
    public CafeMenu() {
       addItem("VeggieBurger and Air Fries",
           "Veggieburger on a whole wheat bun, lettuce, tomato, and fries",
           true, 3.99);
       addItem("Soup of theday",
           "A cup ofthe soup of the day, with a side salad",
           false, 3.69);
       addItem("Burrito",
           "A largeburrito, with whole pinto beans, salsa, guacamole",
           true, 4.29);
    }
 
    public
void
addItem(String name, String description,
                         boolean vegetarian,
double price)
    {
       MenuItem menuItem = new MenuItem(name, description, vegetarian,price);
       menuItems.put(menuItem.getName(), menuItem);
    }
 
    public Hashtable getItems() {
       return
menuItems;
    }
 
    public Iterator createIterator() {
       return
menuItems.values().iterator();
    }
}

增加café餐馆后的服务员类

public
class
Waitress {
    Menu pancakeHouseMenu;
    Menu dinerMenu;
    Menu cafeMenu;
 
    public Waitress(Menu pancakeHouseMenu, Menu dinerMenu, MenucafeMenu) {
       this.pancakeHouseMenu = pancakeHouseMenu;
       this.dinerMenu = dinerMenu;
       this.cafeMenu = cafeMenu;
    }
 
    public
void
printMenu() {
       Iterator pancakeIterator = pancakeHouseMenu.createIterator();
       Iterator dinerIterator = dinerMenu.createIterator();
       Iterator cafeIterator = cafeMenu.createIterator();
 
       System.out.println("MENU\n----\nBREAKFAST");
       printMenu(pancakeIterator);
       System.out.println("\nLUNCH");
       printMenu(dinerIterator);
       System.out.println("\nDINNER");
       printMenu(cafeIterator);
    }
}

服务员类看着有点别扭,不是么?三家餐馆的菜单类差不多,但是要一个一个处理;另外,每增加一家餐馆都要修改服务员类,违背了开闭准则。看看我们的改进:

public
class
Waitress {
    ArrayList menus;
    
 
    public Waitress(ArrayList menus) {
       this.menus = menus;
    }
  
    public
void
printMenu() {
       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());
       }
    }
}

将菜单放入集合中,之前服务员类对每个菜单的操作转变成对集合的遍历。

9.    新问题—子菜单

我们使用迭代器模式对餐馆菜单的实现进行改造后,融合各种餐馆的菜单都很顺利,看起来事情处理的很完美,现在问题来了,有个餐馆的菜单要支持的子菜单,用编程的眼光来看,集合中放置的内容不再全是menuitem了,可能还会有另外的menu,怎么处理?

每当在设计中面临类似问题时,我们知道一个大的改变要发生了,需要慎重地做一个executive decision,因为设计已经不能满足需求的变化,需要重新设计才能解决问题。

根据需求,结合已有知识,我们得出以下考虑:

1.      我们需要一种树形结构,用于容纳菜单、子菜单以及菜单项等各种类型的数据。

2.      需要保证能像迭代器一样方便地访问每个菜单项

3.      我们可能需要一种更灵活的菜单访问方式:只遍历某个餐馆子菜单的菜单项或者遍历餐馆的所有菜单项,包括子菜单。

按照树形结构画出来菜单的类型如下图:

10.             理论篇—复合模式定义

我们将使用复合模式结合已有的迭代器模式来解决面临的问题。

         TheComposite Pattern allows you to compose objects into tree structures torepresent part-whole hierarchies. Composite lets clients treat individualobjects and compositions of objects uniformly.

         复合模式允许你将对象组合成树形结构来表示部分与整体的层次关系。复合模式使得客户端使用统一接口访问单一对象以及组合对象。

         类关系:

        

         从复合模式的定义来看,该模式很适合解决我们面临的问题:将菜单、子菜单、菜单项全部放置到一种结构中;使用统一的接口访问,就可以写出高灵活性的遍历方法。

11.             实践篇—改进餐馆菜单设计

应用复合模式,需要做什么?首先要为菜单和菜单项定义统一的接口(component),然后让菜单和菜单项实现该接口,来看看类图:

我们把菜单项和菜单需要实现的方法集成到一起,形成MenuComponent抽象类,在抽象类中提供每个方法的默认实现;在MenuItem和Menu1中提供对自己有意义的方法实现,来覆盖父类方法。看看代码:

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

MenuItem和Menu各自覆盖对自己有意义的方法

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

                    Stringdescription,
                    boolean vegetarian,

                    double price)

    {
       this.name = name;
       this.description = description;
       this.vegetarian = vegetarian;
       this.price = price;
    }
 
    public String getName() {
       return
name;
    }
 
    public String getDescription() {
       return
description;
    }
 
    public
double
getPrice() {
       return
price;
    }
 
    public
boolean
isVegetarian() {
       return
vegetarian;
    }
 
    public
void
print() {
       System.out.print("  " + getName());
       if (isVegetarian()) {
           System.out.print("(v)");
       }
       System.out.println(", " + getPrice());
       System.out.println("     -- " + getDescription());
    }
}

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;
    }
 
    public
void
add(MenuComponent menuComponent) {
       menuComponents.add(menuComponent);
    }
 
    public
void
remove(MenuComponent menuComponent) {
       menuComponents.remove(menuComponent);
    }
 
    public MenuComponent getChild(int i) {
       return (MenuComponent)menuComponents.get(i);
    }
 
    public String getName() {
       return
name;
    }
 
    public String getDescription() {
       return
description;
    }
 
    public
void
print() {
       System.out.print("\n" + getName());
       System.out.println(", " + getDescription());
       System.out.println("---------------------"); 

}

以上实现有什么问题?

在menu方法中实现的print方法跟menuItem中实现的print方法含义不同,menuItem打印出了菜单项的名称、描述以及价格,menu中仅仅打印出了菜单的名称和描述;我们希望菜单的print方法也能打印出所有的菜单项信息。对print方法进行如下改进:

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

至此就可以改进我们的服务员类了:

public
class
Waitress {
    MenuComponent allMenus;
 
    public Waitress(MenuComponent allMenus) {
       this.allMenus = allMenus;
    }
 
    public
void
printMenu() {
       allMenus.print();
    }
}

12.             不辨不明—复合模式与单一职责

使用复合模式后,menuComponent的方法集成了菜单的方法以及菜单项的方法,这不是跟单一职责原则冲突?

可以这样说,在使用复合模式来改进餐馆菜单设计的过程中,确实违背了单一职责的原则,但是我们用单一职责准则换来了服务员类对菜单和菜单项的透明访问,服务员类只需知道menuComponent即可,不管具体是菜单还是菜单项。

当然这样设计还会带来不安全使用的问题,客户端可能会对menu或者menuItem进行不适合的操作,而编译器丝毫不能发现。一种解决方式是为每个类声明不同的接口,但是这样的话,就不能透明访问,客户端需要使用条件判断,来判定类型才能执行操作。

总之,现实中的所有设计都需要做妥协的工作,需要综合考虑各种因素,两害相权取其轻,我们觉得保持客户端访问的透明性更重要,因此选择了这种改进方式。

13.             复合迭代器

应用复合模式之后,我们定义了统一的抽象类menuComponent,从而对客户端屏蔽了menu和menuItem的不同,现在我们更进一步,为menuComponent提供统一的Iterator实现,使得客户端可以像使用其他迭代器一样使用menuComponent的迭代器。

我们为menuComponent类添加createIterator方法,以下是menu类和menuItem类的迭代器实现。

Menu类的迭代器实现

public Iterator createIterator() {
       return
new
CompositeIterator(menuComponents.iterator());
    }

public
class
CompositeIterator implements Iterator {
    Stack stack = new Stack();
  
    public CompositeIterator(Iterator iterator) {
       stack.push(iterator);
    }
  
    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
;
       }
    }
 
    public
boolean
hasNext() {
       if (stack.empty()) {
           return
false
;
       } else {
           Iterator iterator = (Iterator)
stack.peek();
           if (!iterator.hasNext()) {
              stack.pop();
              return hasNext();
           } else {
              return
true
;
           }
       }
    }
  
    public
void
remove() {
       throw
new
UnsupportedOperationException();
    }
}

MenuItem类的迭代器实现

public Iterator createIterator() {
       return
new
NullIterator();
    }

为什么引入空迭代器?

如果不使用空迭代器,我们可能会在创建迭代器的方法被调用时返回null,那么client就不得不判断迭代器是否为空;使用空迭代器,让这个特殊的迭代器在每次调用hasNext时都返回false,这样的话客户端的处理就一致了。来看看空迭代器的实现:

public
class
NullIterator implements Iterator {
  
    public Object next() {
       return
null
;
    }
 
    public
boolean
hasNext() {
       return
false
;
    }
  
    public
void
remove() {
       throw
new
UnsupportedOperationException();
    }
}

如果我们想打印出所有素食的菜单,怎么处理?

好的方法,当然是在MenuComponent类中增加新的判断是否为素食的方法,看看代码:

public
void
printVegetarianMenu() {
       Iterator iterator = allMenus.createIterator();
 
       System.out.println("\nVEGETARIANMENU\n----");
       while (iterator.hasNext()) {
           MenuComponent menuComponent =
                  (MenuComponent)iterator.next();
           try {
              if (menuComponent.isVegetarian()) {
                  menuComponent.print();
              }
           } catch (UnsupportedOperationException e) {}
       }
    }

MenuComponent包含menu和menuItem两种,isVegetarian方法对menu类没有意义,所以这里使用了try/catch进行控制。

一般来说,try/catch用来进行异常处理,不推荐进行程序逻辑控制,在这个地方使用,实际上也是一种妥协,为了换得之前我们提到的“透明性”,我们进行了非常规的处理。

当然,使用别的方式也能达到“透明”的效果,比如在menu类里面实现一个永远返回false的isVegetarian方法,但是在这里,我们更倾向于使用抛出异常的方式,来让代码更具有可读性。

14.             炉边谈话

1.      当你要处理的集合类数据存在部分-整体关系,而你想使用统一的接口来处理这些数据时,使用复合模式。

2.      什么叫部分-整体关系?考虑下图形编程接口,一般的顶层类为Frame或者Panel,在这个顶层类上你可以添加菜单、文本框、标签等界面元素,当系统展示时,由Frame或者Panel负责所有内容的展示,你不用考虑每一个界面元素的展示。Frame或Panel跟其他界面元素就是整体和部分的关系。

3.      你说的统一接口是什么意思?统一接口就是可以对menu和menuItem调用print,而client不用关心到底是什么,他们都会做正确的事情。

4.      这样说的话,集合中的每一个元素都要做同样的事情,实际中不是这样的,不是么?是这样的,实际统一接口(MenuComponent)中的一些方法对于特定的类是无意义的,我们为了达到对客户端“透明化”的目标,做出了其他牺牲。

5.      对于无意义的方法,你怎么处理?可以根据实际需要返回空,或者其他业务上有意义的值,当然也可以更积极的处理,抛出不支持的异常。

6.      你怎么管理集合数据和叶子数据?放到树形结构中。

7.      树形结构的每一个节点是否记录父亲节点?在某些特殊操作中,记录父亲节点会方便操作,比如删除一个节点。

8.      本节的实现描述的很简单,是否还有很多复合模式实现要考虑的问题没有涉及?是这样的,比如有序的树;比如如果需要多次遍历和计算时,对复合节点结果的缓存等。

15.             Review

新模式:

The IteratorPattern provides a way to access the elements of an aggregate objectsequentially without exposing its underlying representation.

迭代器模式提供一种隐藏集合底层实现,顺序访问集合对象元素的方法。

The CompositePattern allows you to compose objects into tree structures to representpart-whole hierarchies. Composite lets clients treat individual objects andcompositions of objects uniformly.

          复合模式允许你将对象组合成树形结构来表示部分与整体的层次关系。复合模式使得客户端使用统一接口访问单一对象以及组合对象。

新准则:

SingleResponsibility – A class should have only one reason to change. 单一职责原则,一个类只应该因为一个理由而变化。

模式回顾:

a)        迭代器允许在不暴露集合对象内部实现的前提下,访问集合中的元素。

b)        迭代器把遍历集合对象的工作从集合对象中分离出来,封装在另外一个对象中。

c)        迭代器提供遍历集合对象的统一接口,让客户端灵活处理各种集合。

d)        应该致力于设计出单一职责的类。

e)        复合模式提供了一种容纳单个对象和集合对象的结构。

f)         复合模式允许客户端使用统一接口使用单个对象和集合对象。

g)        在复合模式的实现过程中,有很多平衡的考虑。

OO准则:

a. 封装变化,encapsulate what varies

b. 组合优于继承, favorcomposition over inheritance

c. 面向接口编程,program to interfaces, not implementation

d. 致力于实现交互对象之间的松散耦合, strive for loosely coupled designs between objects that interact

e. 类应该对于扩展开发,对于修改封闭, classes should be open for extension but closed for modification

f. 依赖于抽象类或者接口而不是具体类。Depend on abstraction. Do not depend on concrete classes.

g. 只跟关系最密切的对象打交道。Principle of Least Knowledge – talk only to your immediate friend.

h. 别调用我,由我在需要的时候调用你。Don’t call us, we’ll call you.

i. 单一职责原则,一个类只应该因为一个理由而变化。    Single Responsibility – A class should have only one reason tochange.

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息