热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

初识设计模式chapter09迭代器与组合模式

初识设计模式chapter 09-迭代器与组合模式1引言有许多方法可以把对象堆起来成为一个集合(collection)。你可以把他们放进数组、堆栈、或者是散列表中,这是你的自由。每
初识设计模式 chapter 09-迭代器与组合模式

1 引言

有许多方法可以把对象堆起来成为一个集合(collection)。你可以把他们放进数组、堆栈、或者是散列表中,这是你的自由。每一种都有它自己的优点和适合的使用时机,但总是有一个时候,你的客户想要遍历这些对象,而当他这么做的时候,你打算让客户看到你的实现吗?我们当然希望不要!这太不专业可。没关系,不要为你的工作担心,你将在本章中学习如何能让客户遍历你的对象而又无法窥视你存储对象的方式;也将学习如何创建一些对象集合(super collection),能够一口气就跳过某些让人望而生畏的数据结构。你还将学到一些关于对象职责的知识。

2 正文

2.1 对象村餐厅和对象村煎饼屋合并了

这真是个好消息,现在我们可以在同一个地方想用两种食品了。但是,好像有点麻烦。煎饼屋的菜单使用数组存储,而餐厅的菜单使用ArrayList存储。至少是两个餐厅都统一按照下面的代码来实现菜单项(单独的菜品)。

菜单项实现:

public class MenuItem {
String name;
String description;
boolean vegetarian;
double price;
/*
* 菜单项包含了名称、描述、是否为素食、价格
* 将这些值传入构造器来初始化这个菜单项
*
* 下面的getter方法可以获取菜单项的各个字段
*/
public MenuItem(String name,
String description,
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 class PancakeHouseMenu implements Menu {
ArrayList menuItems;
/*
* 这是煎饼屋的实现
* 使用一个ArrayList存储他的菜单项
* 在菜单的构造器中,每一个菜单项都会被加入到ArrayList中
*
* 要加入一个菜单项,煎饼屋的做法是:创建一个新的菜单项对象,传入每一个变量,
* 然后将它加入到此ArrayList中
*/
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",
false,
2.99);
addItem("Blueberry Pancakes",
"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;
}
}

餐厅的菜单实现

public class DinerMenu implements Menu {
static final int MAX_ITEMS = 6;
int numberOfItems = 0;
MenuItem[] menuItems;
/*
* 1 餐厅使用的是数组来存储菜单项,所以可以控制菜单的长度,并且在去除菜单项的时候,不需要转类型
* 2 和煎饼屋一样,餐厅也是使用addItem()方法在构造器中创建菜单项
* 3 addItem()方法需要拿所有必要的参数来创建一个菜单项,并实例化它。这个方法也会检查数组是否
* 超过了数组的最大长度
*/
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);
addItem("Steamed Veggies and Brown Rice",
"Steamed vegetables over brown rice", true, 3.99);
addItem("Pasta",
"Spaghetti with 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, menu is full! Can't add item to menu");
} else {
menuItems[numberOfItems] = menuItem;
numberOfItems = numberOfItems + 1;
}
}
public MenuItem[] getMenuItems() {
return menuItems;
}
}

以上实现的缺点: 1、我们是针对PancakeHouseMenu和DinnerMenu的具体实现编码,不是针对接口。 2、如果我们决定从DinnerMenu切换到另一种菜单,此菜单的项是用HashTable来存放的,我们会因此需要修改女招待中的许多代码。 3、女招待需要知道每个菜单如何表达内部的菜单项项集合,这违反了封装。 4、我们有重复的代码,printMenu()方法需要两个循环,来遍历两种不同的菜单。如果我们加上第三种菜单,我们就需要三个循环。

2.2 会见迭代器模式

关于迭代器模式,你所需要知道的第一件事情,就是它依赖于一个名为迭代器的接口。 一个迭代器包含两个接口:hasNext()方法告诉我们是否在这个聚合中还有更多的元素,next()方法返回这个聚合中的下一个对象。

现在我们需要为餐厅菜单实现一个具体的迭代器

public class DinerMenuIterator implements Iterator {
MenuItem[] items;
int position = 0;
/*
* 实现迭代器接口
* 1 position记录当前数组遍历的位置
* 2 构造器需要被传入一个菜单项的数组当做参数
* 3 next()方法返回数组内的下一个元素,并递增其位置
* 4 hasNext()方法会检查我们是否已经取得数组内所有的元素。如果还有元素待遍历,就返回true
* 5 因为使用的固定长度的数组,所以要检查数组长度是否超过了最大长度和是否为null
*/
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;
}
}
}

女招待的代码

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);
}
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、两个菜单实现相同的接口。 2、煎饼屋菜单和餐厅菜单都实现了新的createIterator()方法,它们负责为各自的菜单项创建迭代器。 3、迭代器让女招待能够从具体类的实现中解耦。她不需要知道菜单是使用数组、ArrayList,还是便利贴来实现。她只关心她能够取得的迭代器。

2.3 定义迭代器模式

迭代器模式:提供一种方法书序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。
这很有意义:这个模式给你提供了一种方法,可以顺序访问一个聚集对象中的元素,而又不知道内部是如何表示的。
另一个队你的设计造成重要影响的:迭代器模式把在元素之间游走的责任交给迭代器,而不是聚合对象。这不仅让聚合的接口和实现变得更简洁,也可以让聚合更专注在它所应该专注的事情上面(管理对象集合),而不必去理会遍历的事情。

单一责任:一个类应该只有一个引起变化的原因。
类的每个责任都有改变的潜在区域。超过一个责任,意味着超过一个改变的区域。 这个原则告诉我们尽量让每一个类保持单一责任。 要如何解决呢?这听起来很容易,但其实做起来并不简单:区分设计中的责任,是最困难的事情之一。我们的大脑很习惯看着一大群的行为,然后将他们集中在一起。我们的大脑很习惯看着一大群的行为,然后将它们集中在一起,尽管他们可能属于两个或者多个不同的责任。
想要成功的唯一方法,就是努力不懈地检查你的设计,随着系统的成长,随时观察有没有迹象显示某个类改变的原因超出一个

2.4 迭代器与集合

我们所使用的这些类都属于Java Collection Framework的一部分。这里所谓的Framework指的是一群类和接口,其中包括了ArrayList、Vector、LinkedList、Stack和PriorityQueue。这些类都实现了Java.util.Collection接口。这个接口包含了很多有用的方法,可以操纵一群对象。

Java 5的迭代器和集合 Java 5包含一种新形势的for语句,成为for/in。这可以让你在一个集合或者一个数组中遍历,而且不需要显式创建迭代器。想使用for/in,语法是这样的

for (Object obj: collection) {
//动作 }

2.5 初始组合模式

正当我们以为这一切很完美的时候,现在他们希望能够
加上一份餐后甜点的“子菜单”。 如果我们能让甜点菜单编程餐厅菜单集合的一个元素,那该有多好。但是根据现在的实现,根据做不到。

所以在我们的新设计中,真正需要些什么呢? 1 我们需要某种树形结构,可以容纳菜单、子菜单和菜单项。 2 我们需要确定能够在每个菜单的各个项之间游走,而且至少要像现在用迭代器一样方便。 3 我们也需要能够更有弹性地再菜单项之间游走。比方说,可能只需要遍历甜点菜单,或者可以遍历餐厅的整个菜单(包括甜点菜单在内)。

组合模式:允许你将对象组合成树形结构来表现“整体/部分”层次结构。组合能让客户以一致的方式处理个别对象以及对象组合。
组合模式让我们能用树形方式创建对象的结构,树里面包含了组合以及个别的对象。
使用组合机构,我们能把相同的操作应用在组合和个别对象上。换句话说,在大多数情况下,我们可以忽略对象组合和个别对象之间的差别。

2.6 利用组合设计菜单

我们要如何在菜单上应用组合模式呢?一开始,我们需要创建一个组件接口来作为菜单和菜单项的共同接口,让我们能够用统一的做法来处理菜单和菜单项。换句话说,我们可以针对菜单或者菜单项调用相同的方法。

实现菜单组件,所有的组件都必须实现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();
}
}

实现菜单项

public class MenuItem extends MenuComponent {
String name;
String description;
boolean vegetarian;
double price; /*
* 1 首先我们要扩展MenuComponent接口
* 2 其他和之前的类定义没有太大区别,增加了toString()方法
*/
public MenuItem(String name,
String description,
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;
/*
* 1 菜单和菜单项一样,都是MenuComponent
* 2 菜单可以有任意数目的子节点,这些子节点都必须属于MenuComponent类型,使用内部ArrayList记录它们
* 3 我们再这里将菜单项和其他菜单加入到菜单中。因为菜单和菜单项都是MenuComponent,所以我们只需要一个方法就可以两者兼顾
* 4 同样的道理,我们也可以删除或取得某个MenuComponent
*/
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("---------------------");
Iterator iterator = menuComponents.iterator();
while (iterator.hasNext()) {
MenuComponent menuCompOnent=
(MenuComponent)iterator.next();
menuComponent.print();
}
}
}

编写测试程序

public class MenuTestDrive {
public static void main(String args[]) {
/*
* 1 先创建所有的菜单对象
* 2 我们需要一个最顶层的菜单,将它称为allMenus
* 3 我们使用组合的add()方法,将每个菜单都加入到顶层戴丹allMenu()中
* 4 现在我们需要加上所有的菜单项
* 5 然后我们也在菜单中加入另一个菜单
* 6 一旦我们将珍格格菜单层次构造完毕,把它整个交给女招待,你会发现,女招待要将整份菜单打印出来,简直易如反掌
*/
MenuComponent pancakeHouseMenu =
new Menu("PANCAKE HOUSE MENU", "Breakfast");
MenuComponent dinerMenu =
new Menu("DINER MENU", "Lunch");
MenuComponent cafeMenu =
new Menu("CAFE MENU", "Dinner");
MenuComponent dessertMenu =
new Menu("DESSERT MENU", "Dessert of course!");
MenuComponent coffeeMenu = new Menu("COFFEE MENU", "Stuff to go with your afternoon coffee");
MenuComponent allMenus = new Menu("ALL MENUS", "All menus combined");
allMenus.add(pancakeHouseMenu);
allMenus.add(dinerMenu);
allMenus.add(cafeMenu);
pancakeHouseMenu.add(new MenuItem(
"K&B's Pancake Breakfast",
"Pancakes with scrambled eggs, and toast",
true,
2.99));
pancakeHouseMenu.add(new MenuItem(
"Regular Pancake Breakfast",
"Pancakes with fried eggs, sausage",
false,
2.99));
pancakeHouseMenu.add(new MenuItem(
"Blueberry Pancakes",
"Pancakes made with fresh blueberries, and blueberry syrup",
true,
3.49));
pancakeHouseMenu.add(new MenuItem(
"Waffles",
"Waffles, with your choice of blueberries or strawberries",
true,
3.59));
dinerMenu.add(new MenuItem(
"Vegetarian BLT",
"(Fakin') Bacon with lettuce & tomato on whole wheat",
true,
2.99));
dinerMenu.add(new MenuItem(
"BLT",
"Bacon with lettuce & tomato on whole wheat",
false,
2.99));
dinerMenu.add(new MenuItem(
"Soup of the day",
"A bowl of the soup of the day, with a side of potato salad",
false,
3.29));
dinerMenu.add(new MenuItem(
"Hotdog",
"A hot dog, with saurkraut, relish, onions, topped with cheese",
false,
3.05));
dinerMenu.add(new MenuItem(
"Steamed Veggies and Brown Rice",
"Steamed vegetables over brown rice",
true,
3.99));
dinerMenu.add(new MenuItem(
"Pasta",
"Spaghetti with Marinara Sauce, and a slice of sourdough bread",
true,
3.89));
dinerMenu.add(dessertMenu);
dessertMenu.add(new MenuItem(
"Apple Pie",
"Apple pie with a flakey crust, topped with vanilla icecream",
true,
1.59));
dessertMenu.add(new MenuItem(
"Cheesecake",
"Creamy New York cheesecake, with a chocolate graham crust",
true,
1.99));
dessertMenu.add(new MenuItem(
"Sorbet",
"A scoop of raspberry and a scoop of lime",
true,
1.89));
cafeMenu.add(new MenuItem(
"Veggie Burger and Air Fries",
"Veggie burger on a whole wheat bun, lettuce, tomato, and fries",
true,
3.99));
cafeMenu.add(new MenuItem(
"Soup of the day",
"A cup of the soup of the day, with a side salad",
false,
3.69));
cafeMenu.add(new MenuItem(
"Burrito",
"A large burrito, with whole pinto beans, salsa, guacamole",
true,
4.29));
cafeMenu.add(coffeeMenu);
coffeeMenu.add(new MenuItem(
"Coffee Cake",
"Crumbly cake topped with cinnamon and walnuts",
true,
1.59));
coffeeMenu.add(new MenuItem(
"Bagel",
"Flavors include sesame, poppyseed, cinnamon raisin, pumpkin",
false,
0.69));
coffeeMenu.add(new MenuItem(
"Biscotti",
"Three almond or hazelnut biscotti COOKIEs",
true,
0.89));
Waitress waitress = new Waitress(allMenus);
waitress.printMenu();
}
}

关于组合模式的小疑问。
组合模式以单一责任设计原则获取透明性(transparency)。什么是透明性?通过让组件和接口同时包含一些管理子节点和叶节点的操作,客户就可以将组合和叶节点一视同仁。也就是说,一个元素究竟是组合还是叶节点,对客户是透明的。
所以,回到上面的问题,这是一个很典型的折中案例。尽管我们收到设计原则的知道,但是,我们总是需要观察某原则对我们设计所造成的影响。有时候,我们会故意做一些看似违反原则的事情。

2.7 组合迭代器

1 我们在MenuComponent中加入一个createIterator()方法,这意味着每个菜单和菜单项都必须实现这个方法,也意味着,对一个组合调用CreateIterator()方法,将会应用于该组合的所有孩子。 2 现在我们需要在菜单和菜单项中实现这个方法。

这个CompositeIterator是一个不可小觑的迭代器。它的工作室遍历组件内的菜单项,而且确保所有的子菜单都被包括进来。 注意:这里使用的递归,跟我默念“递归是我的朋友,递归是我的朋友。。。。”

public class CompositeIterator implements Iterator {
Stack stack = new Stack();
public CompositeIterator(Iterator iterator) {
stack.push(iterator);
}
/*
* (non-Javadoc)
* @see java.util.Iterator#next()
* 1 当客户想要取得下一个元素的时候,我们先调用hasNext()来确定是否还有下一个
* 2 如果还有下一个元素,我们就从堆栈中取出目前的迭代器,然后取得它的下一个元素
* 3 如果元素时一个菜单那,我们有了另一个需要被包含进遍历中的组合,所以我们将它丢进堆栈中。不管是不是菜单那,我们都返回该组件。
*/
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;
}
}
/*
* (non-Javadoc)
* @see java.util.Iterator#hasNext()
* 1 想要知道是否还有下一个元素,我们检查堆栈是否被清空;如果已经空了,就表示没有下一个元素了
* 2 否则,我们就从堆栈的顶层中取出迭代器,看看是否还有下一个元素,如果没有元素,我们将它弹出堆栈,然后递归地调用hasNext()
* 3 否则,表示还有下一个元素,我们返回true
*/
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();
}
}

3 本章小结

好长的一章,代码片也不少,好在终于到头了。学习过程中我们经常会遇到这种相对较长的学习周期,这时候可以分解为多个步骤来完成,可以实时记录自己的进度,而不是简单的未完成/已完成,可以有一定的激励作用噢。 回到这章设计模式来,迭代器大家想必不会陌生,这边自定义了迭代器接口为的就是让我们明白迭代器的工作原理。组合模式结合递归算法,这个在处理树形结构时绝对是利器。之前在写一个小工具时,也遇到过类似的问题,当时还不知道有此等利器,完全凭自己的想法把功能实现了,回头看看有没有优化的可能。需求很简单,把Excel中的记录按照树形结构输出成xml文件,有兴趣的同学可以看下我之前的博客(http://blog.csdn.net/adoaiwen1314/article/details/17075491),欢迎提出用组合迭代器模式设计的代码。


推荐阅读
author-avatar
Jiaquan_Sun_106
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有