设计模式--组合模式

来源:互联网 发布:台湾人活跃于中国网络 编辑:程序博客网 时间:2024/05/17 04:33

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

知识点的梳理:

  • 组合模式提供一个结构,可同时包容个别对象和组合对象;
  • 组合模式允许客户对个别对象以及组合对象一视同仁;
  • 组合结构内的任意对象称为组件,组件可以是组合,也可以是叶节点;
  • 在实现组合模式时,有许多设计上的折衷。需要根据需要平衡透明性和安全性;

      

  • 在餐厅,煎饼屋的基础上,增加咖啡厅!
    • 先来看看咖啡厅的菜单,将代码整合进案例

import java.util.Hashtable;

import java.util.Iterator;

//咖啡厅菜单实现Menu接口,所以女招待使用咖啡厅菜单的方法,就和其他的两个菜单没有两样

public class CafeMenuimplements Menu{

//菜单项使用散列表存储

HashtablemenuItems = new Hashtable();

public CafeMenu(){

addItem("Veggie Burger and Air Fries"

,"Veggie burger on a whole wheat bun,lettuce,tomato,and fries"

,true

,3.99);

addItem("Soup of the day"

,"A cup of the soup of the day,with a side salad"

,false

,3.69);

addItem("Burrito"

,"A large burrito,with whole pinto beans,salsa,guacamole"

,true

,4.29);

}

//创建新的菜单项,并将它加入到散列列表中

private void addItem(Stringname, String description, booleanvegetarian, double price) {

MenuItemmenuItem = new MenuItem(name,description,vegetarian,price);

menuItems.put(menuItem.getName(),menuItem);

}

//实现createIterator方法。

@Override

public Iterator createIterator() {

//在这里取值的部分的迭代器

returnmenuItems.values().iterator();

}

}

  • 让女招待认识咖啡厅

public class Waitress {

MenupancakeHouseMenu;

MenudinerMenu;

MenucafeMenu;//咖啡厅菜单会和其他菜单一起被传入女招待的构造器中,然后记录在一个实例变量中

public Waitress(MenupancakeHouseMenu,Menu dinerMenu,Menu cafeMenu){

this.pancakeHouseMenu =pancakeHouseMenu;

this.dinerMenu =dinerMenu;

this.cafeMenu =cafeMenu;

}

public void printMenu(){

IteratorpancakeIterator = pancakeHouseMenu.createIterator();

IteratordinerIterator = dinerMenu.createIterator();

//将咖啡厅加入晚餐的菜单

IteratorcafeIterator = cafeMenu.createIterator();

System.out.println("MENU\n----\nBREAKFAST");

printMenu(pancakeIterator);

System.out.println("\nLUNCH");

printMenu(dinerIterator);

//传入printMenu打印

System.out.println("\nDINNER");

printMenu(cafeIterator);

}

private void printMenu(Iteratoriterator) {

while(iterator.hasNext()){

MenuItemmenuItem = (MenuItem)iterator.next();//取得下一项

System.out.print(menuItem.getName() + ", ");

System.out.print(menuItem.getPrice() + " -- ");

System.out.println(menuItem.getDescription());

}

}

}

  • 测试代码

public class MenuTestDrive {

public static void main(String[]args) {

//创建两张新菜单

PancakeHouseMenupancakeHouseMenu = new PancakeHouseMenu();

DinerMenudinerMenu = new DinerMenu();

CafeMenucafeMenu = new CafeMenu();

//然后创建一个女招待,并将菜单传送给她

Waitresswaitress = new Waitress(pancakeHouseMenu,dinerMenu,cafeMenu);

waitress.printMenu();//打印出菜单

}

}

效果:

  • 升级代码
    • 上面的示例成功解决了在原有基础上,再次增加一个咖啡厅是完全可能的。但在程序中调用三次printMenu()貌似不太明智。每次一有新的菜单加入,就必须打开女招待实现并加入更多的代码,着违反了"开放-关闭原则"
    • 如果将这些菜单全都打包进一个ArrayList中,然后取得它的迭代器,遍历每个菜单的话,是不是就解决了呢?来试试看

public class Waitress {

ArrayListmenus;//现在只需要一个菜单ArrayList

public Waitress(ArrayListmenus){

this.menus =menus;

}

public void printMenu(){

IteratormenuIterator = menus.iterator();

while(menuIterator.hasNext()){

//遍历菜单,把每个菜单的迭代器传给重载的printMenu()方法

Menumenu = (Menu)menuIterator.next();

printMenu(menu.createIterator());

}

}

//这个函数不需要改变

void printMenu(Iteratoriterator) {

while(iterator.hasNext()){

MenuItemmenuItem = (MenuItem)iterator.next();//取得下一项

System.out.print(menuItem.getName() + ", ");

System.out.print(menuItem.getPrice() + " -- ");

System.out.println(menuItem.getDescription());

}

}

}

这样做的话,暂时可以解决问题

  • 新的需求
    • 现在餐厅希望能够加上一份餐后甜点的"子菜单";
      • 现在不仅仅要支持多个菜单,还要支持菜单中的菜单;
      • 如果能让甜点菜单变成餐厅菜单集合的一个元素,就很好了。但是根据现在的实例,根本无法做到;
  • 现在怎么办?如果不重新设计,就无法容纳未来增加的菜单或子菜单等需求。那我们真正需要什么呢?
    • 我们需要某种树形结构,可以容纳菜单,子菜单和菜单项;
    • 我们需要确定能够在每个菜单的各个项之间游走,而且至少要像想在用迭代器一样方便;
    • 还需要能够更有弹性地在菜单项之间游走。比如,可能需要遍历贴点菜单,或者可以遍历餐厅的整个菜单(包括甜点菜单);
  • 定义组合模式
    • 以菜单为例来说明问题:
      • 这个模式能够创建一个树形结构,在同一个结构中处理嵌套菜单和菜单项组。通过将菜单和项放在相同的结构中,我们创建了一个"整体/部分"层次结构,即由菜单和菜单项组成的对象树;但是可以将它视为一个整体,像是一个丰富的大菜单;

  • 使用组合结构,能把相同的操作应用在组合和个别对象上。换句话说,在大多数情况下,可以忽略对象组合和个别对象之间的差别;
  • 类图
  • 说明:组合模式和迭代器模式有什么关系
    • 两者合作无间,可以在组合的实现中使用迭代器,而且做法还不只一种;
  • 利用组合设计菜单
    • 我们需要创建一个组件接口来作为菜单和菜单项的共同接口,这样就可以针对菜单或菜单项调用相同的方法;
    • 如何让菜单符合组合模式的结构呢?
  • 重新设计菜单
    • 实现菜单组件
      • 所有的组件都必须实现MenuComponent接口。然而,叶节点和组合节点的角色不同,所以有些方法可能并不适合某些节点。面对这种情况,有时候,最好是抛出运行时异常;

public abstract class MenuComponent {

//以下把"组合"方法组织在一起,即新增,删除和取得菜单组件

public void add(MenuComponentmenuComponent){

throw new UnsupportedOperationException();

}

public void remove(MenuComponentmenuComponent){

throw new UnsupportedOperationException();

}

public MenuComponent getChild(inti){

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

}

}

  • 实现菜单项

//扩展MenuComponent接口

public class MenuItemextends MenuComponent{

Stringname;

Stringdescription;

booleanvegetarian;

doubleprice;

public MenuItem(Stringname,String description,booleanvegetarian,doubleprice){

this.name=name;

this.description =description;

this.vegetarian =vegetarian;

this.price =price;

}

public String getName() {

returnname;

}

public String getDescription() {

returndescription;

}

public boolean isVegetarian() {

returnvegetarian;

}

public double getPrice() {

returnprice;

}

//MenuComponent类里覆盖print()方法。对菜单项来说,此方法会打印出完整的菜单项条目,包括:名字,描述,价格以及是否为素食

public void print(){

System.out.print(" "+getName());

if(isVegetarian()){

System.out.print("(v)");

}

System.out.println(", "+getPrice());

System.out.println(" --"+getDescription());

}

}

  • 实现组合菜单
    • 现在已经有了菜单项,还需要组合类,也就是菜单咯。这个组合类可以持有菜单项或者其他菜单。有一些方法并未在MenuComopnent类中实现,比如getPrice()和isVegertarian(),因为这些方法对菜单而言没有太大的意义;

//菜单和菜单项一样,都是MenuComponent

public class Menuextends MenuComponent {

//菜单可以有任意数目的孩子,这些孩子都必须属于MenuComponent类型,我们使用内部的ArrayList记录它们

ArrayListmenuComponents = new ArrayList();

Stringname;

Stringdescription;

//这和之前的实现不一样,我们将给每个菜单一个名字和一个描述。以前,每个菜单的类名称就是此菜单的名字

public Menu(Stringname,String description){

this.name =name;

this.description =description;

}

//在这里将菜单项和其他菜单加入到菜单中。因为菜单和菜单项都是MenuComponent,所以我们只需用一个方法就可以两者兼顾

//同样的道理,也可以删除或者取得某个MenuComponent

public void add(MenuComponentmenuComponent){

menuComponents.add(menuComponent);

}

public void remove(MenuComponentmenuComponent){

menuComponents.remove(menuComponent);

}

public MenuComponent getChile(inti){

return (MenuComponent)menuComponents.get(i);

}

//这是用来取得名字和描述的getter方法

//这里没有覆盖getPrice()isVegetarian(),因为这些方法对Menu来说没有意义。如果尝试调用这些方法的话,就会得到父类定义的异常

public String getName(){

returnname;

}

public String getDescription(){

returndescription;

}

//打印菜单的名称和描述,还要打印出菜单内所有组件的内容:其他菜单和菜单项

public void print(){

System.out.print("\n"+getName());

System.out.println(", "+getDescription());

System.out.println("----------------------");

//便利过程中,可能遇到其他菜单,或老是遇到菜单项。由于菜单和菜单项都实现了print(),那我们只要调用print()即可。

Iteratoriterator = menuComponents.iterator();

while(iterator.hasNext()){

MenuComponentmenuComponent = (MenuComponent)iterator.next();

menuComponent.print();

}

//在遍历期间,如果遇到另一个菜单对象,它的print()方法会开始另一个遍历,依次类推

}

}

  • 测试前的准备工作
    • 更新女招待的代码--她可是菜单的主要客户:

public class Waitress {

MenuComponentallMenus;

//只需要将最顶层的菜单组件交给她就可以了,最顶层菜单包含其他所有菜单,可以称为allMenus

public Waitress(MenuComponentallMenus){

this.allMenus =allMenus;

}

public void printMenu(){

//只需要调用最顶层菜单的print(),就可以打印整个菜单层次,包括所有菜单及所有菜单项

allMenus.print();

}

}

  • 运行时菜单的组合:
  • 编写测试代码

public class MenuTestDrive {

public static void main(String[]args) {

//先创建所有的菜单对象

MenuComponentpancakeHouseMenu = new Menu("PANCAKE HOUSE MENU","Breakfase");

MenuComponentdinerMenu = new Menu("DINER MENU","Lunch");

MenuComponentcafeMenu = new Menu("CAFE MENU","Dinner");

MenuComponentdessertMenu = new Menu("DESSERT MENU","Dessert of course!");

//需要一个最顶层的菜单,将它称为allMenus

MenuComponentallMenus = new Menu("ALL MENUS","All menus combined");

//使用组合的add()方法,将每个菜单都加入到顶层菜单allMenus

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 Breakfase"

,"Pancakes with fried eggs,sausage"

,false

,2.99));

pancakeHouseMenu.add(new MenuItem("Blueberry Pancakes"

,"Pancakes made with fresh blueberries"

,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"

,"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 Vegetabls over brown rice"

,true

,3.99));

dinerMenu.add(new MenuItem("Pasta"

,"Spagheti with Marinara Sauce,and a slice of sourdough bread"

,true

,3.89));

//在菜单中加入另一个菜单。由于菜单和菜单项都是MenuComponent,所以菜单可以顺利地被被加入

dinerMenu.add(dessertMenu);

dessertMenu.add(new MenuItem("Apple Pie"

,"Apple pie with a flakey crust,topped with vanilla ice cream"

,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 Soup of the day"

,true

,3.99));

cafeMenu.add(new MenuItem("Soup of the day"

,"A cup of the soup of the day,with a side salad"

,true

,3.69));

cafeMenu.add(new MenuItem("Burrito"

,"A large burrito,with whole pinto beans,salsa,guacamole"

,true

,4.29));

//将整个菜单层级构造完毕,把它整个交给女招待

Waitresswaitress = new Waitress(allMenus);

waitress.printMenu();

}

}

效果:

ALL MENUS, All menus combined

----------------------

  

PANCAKE HOUSE MENU, Breakfase

----------------------

K&B`s Pancake Breakfast(v), 2.99

--Pancakes with scrambled eggs,and toast

Regular Pancake Breakfase, 2.99

--Pancakes with fried eggs,sausage

Blueberry Pancakes(v), 3.49

--Pancakes made with fresh blueberries

Waffles(v), 3.59

--Waffles,with your choice of blueberries or strawberries

  

DINER MENU, Lunch

----------------------

Vegetarian BLT(v), 2.99

--(Fakin') Bacon with lettuce & tomato on whole wheat

BLT, 2.99

--Bacon with lettuce & tomato on whole wheat

Soup of the day, 3.29

--Soup of the day,with a side of potato salad

Hotdog, 3.05

--A hot dog,with saurkraut,relish,onions,topped with cheese

Steamed Veggies and Brown Rice(v), 3.99

--Steamed Vegetabls over brown rice

Pasta(v), 3.89

--Spagheti with Marinara Sauce,and a slice of sourdough bread

  

DESSERT MENU, Dessert of course!

----------------------

Apple Pie(v), 1.59

--Apple pie with a flakey crust,topped with vanilla ice cream

Cheesecake(v), 1.99

--Creamy New York cheesecake,with a chocolate graham crust

Sorbet(v), 1.89

--A scoop of raspberry and a scoop of lime

  

CAFE MENU, Dinner

----------------------

Veggie Burger and Air Fries(v), 3.99

--Veggie burger on a whole wheat bun,lettuce,tomato,and fries Soup of the day

Soup of the day(v), 3.69

--A cup of the soup of the day,with a side salad

Burrito(v), 4.29

--A large burrito,with whole pinto beans,salsa,guacamole

  • 问题:一个类,不是应该只有一个职责吗?现在组合模式是让一个类有两个责任的模式。它现在要管理层次结构,还要执行菜单的操作。
    • 组合模式以单一责任设计原则换取透明性。通过让组件的接口同时包含一些管理子节点和叶节点的操作,客户就可以将组合的叶节点一视同仁。也就是说,一个元素究竟是组合还是叶节点,对客户是透明的。
    • MenuComponent类中,同时具有两种类型的操作。因为客户有机会对一个元素做一些没有意义的操作(例如试图把菜单添加到菜单项),所以失去了一些"安全性";这是设计上的决策;当然也可以采用另一种方向的设计,将责任区分开来放在不同的接口中。这么一来,设计上就比较安全,但也因此失去了透明性,客户的代码将必须用条件语句和instanceof操作fu
    • 综上,有时候我们会故意做一些看似违反设计原则的事情,这是一个典型的折衷做法;
  • 组合模式中使用迭代器
    • 在上例中print()方法中,已经出现了迭代器。
    • 如果需要,可以让女招待实现迭代器遍历整个组合。比如,女招待可能想要游走整个菜单,挑出素食项;
    • 想要实现一个组合迭代器,要为每个组件都加上createIterator()方法,从抽象的MenuComponent类下手:

public abstract class MenuComponent {

//其余代码不变

public Iterator createIterator(){

throw new UnsupportedOperationException();

}

} 

  • 在菜单和菜单项类中实现这个方法:

public class Menuextends MenuComponent {

//其余代码不变

public Iterator createIterator(){

//这里使用新的,被称为CompositeIterator的迭代器。这个迭代器知道如何遍历任何组合

//将目前组合的迭代器传入它的构造函数

return new CompositeIterator(menuComponents.iterator());

}

}

public class MenuItemextends MenuComponent{

//其余代码不变

public Iterator createIterator(){

//这里的空迭代器需要延展出来说说

return new NullIterator();

}

}

  • 空迭代器:这可是空对象"设计模式"的另一个例子
    • 菜单项既然没有可遍历的,那么我们要如何实现createIterator()方法呢?两种选择:
      • 返回null:可以让createIterator()方法返回null,但是如果这么做,我们的客户代码就需要条件判断语句来判断返回值是否为null;
      • 返回一个迭代器,而这个迭代器的hasNext()永远返回false:这样客户不需要担心返回值是null,我们等于创建一个"没有任何意义"的迭代器;
    • 显然第二个选择会好一点,来看看它的代码:

//一个什么都不做的迭代器

public class NullIteratorimplements Iterator {

@Override

public boolean hasNext() {

//最重要的,当hasNext()被调用时,永远返回false

return false;

}

@Override

public Object next() {

return null;

}

@Override

public void remove() {

throw new UnsupportedOperationException();

}

}

  • OK,了解了空迭代器,来看看组合迭代器
    • CompositeIterator的工作是遍历组件内的菜单项,而且确保所有的子菜单(以及子子菜单....)都被包括进来。

import java.util.Iterator;

import java.util.Stack;

//实现java.util.Iterator接口

public class CompositeIteratorimplements Iterator {

Stackstack = new Stack();

//将要遍历的顶层组合的迭代器传入。放入一个堆栈数据结构中

public CompositeIterator(Iteratoriterator){

stack.push(iterator);

}

@Override

public Object next() {

//当客户想要取得下一个元素的时候,我们先调用hasNext()来确定是否还有下一个

if(hasNext()){

Iteratoriterator = (Iterator) stack.peek();

//如果还有下一个元素,就从堆栈中取出目前的迭代器,然后取得它的下一个元素

MenuComponentcomponent = (MenuComponent)iterator.next();

if(componentinstanceof Menu){

//如果元素是一个菜单,我们有了另一个需要被包含进遍历中的组合,所以我们将它丢进堆栈中,不管是不是菜单,我们都返回该组件

stack.push(component.createIterator());

}

returncomponent;

}else{

return null;

}

}

@Override

public boolean hasNext() {

if(stack.empty()){

//想要知道是否还有下一个元素,我们检查堆栈是否被清空;如果已经空了,就表示没有下一个元素了

return false;

}else{

//否则,我们就从堆栈的顶层中取出迭代器,看看是否还有下一个元素。如果它没有元素,我们将它弹出堆栈,然后递归地调用hasNext()

Iteratoriterator = (Iterator) stack.peek();

if(!iterator.hasNext()){

stack.pop();

return hasNext();

}else{

//否则,表示还有下一个元素,返回true

return true;

}

}

}

@Override

public void remove() {

//不支持删除,这里只有遍历

throw new UnsupportedOperationException();

}

}

  • 让女招待告诉我们哪些菜是素食

public class Waitress {

MenuComponentallMenus;

//只需要将最顶层的菜单组件交给她就可以了,最顶层菜单包含其他所有菜单,可以称为allMenus

public Waitress(MenuComponentallMenus){

this.allMenus =allMenus;

}

public void printMenu(){

//只需要调用最顶层菜单的print(),就可以打印整个菜单层次,包括所有菜单及所有菜单项

allMenus.print();

}

//该方法取得allMenus的组合并得到它的迭代器来作为我们的CompositeIterator

public void printVegetarianMenu(){

Iteratoriterator = allMenus.createIterator();

System.out.println("\nVEGETARIAN MENU\n----");

//遍历组合内的每个元素

while(iterator.hasNext()){

MenuComponentmenuComponent = (MenuComponent)iterator.next();

try{

//调用每个元素的isVegetarian()方法,如果为true,就调用它的print()方法

if(menuComponent.isVegetarian()){

menuComponent.print();

}

//在菜单上实现isVegetarian()方法,让它永远抛出异常。如果异常果真发生了,就捕捉这个异常,然后继续遍历

}catch(UnsupportedOperationExceptione){}

}

}

}

测试这个方法:

Waitress waitress = new Waitress(allMenus);

// waitress.printMenu();

waitress.printVegetarianMenu();

效果:

VEGETARIAN MENU

----

K&B`s Pancake Breakfast(v), 2.99

--Pancakes with scrambled eggs,and toast

Blueberry Pancakes(v), 3.49

--Pancakes made with fresh blueberries

Waffles(v), 3.59

--Waffles,with your choice of blueberries or strawberries

Vegetarian BLT(v), 2.99

--(Fakin') Bacon with lettuce & tomato on whole wheat

Steamed Veggies and Brown Rice(v), 3.99

--Steamed Vegetabls over brown rice

Pasta(v), 3.89

--Spagheti with Marinara Sauce,and a slice of sourdough bread

Apple Pie(v), 1.59

--Apple pie with a flakey crust,topped with vanilla ice cream

Cheesecake(v), 1.99

--Creamy New York cheesecake,with a chocolate graham crust

Sorbet(v), 1.89

--A scoop of raspberry and a scoop of lime

Apple Pie(v), 1.59

--Apple pie with a flakey crust,topped with vanilla ice cream

Cheesecake(v), 1.99

--Creamy New York cheesecake,with a chocolate graham crust

Sorbet(v), 1.89

--A scoop of raspberry and a scoop of lime

Veggie Burger and Air Fries(v), 3.99

--Veggie burger on a whole wheat bun,lettuce,tomato,and fries Soup of the day

Soup of the day(v), 3.69

--A cup of the soup of the day,with a side salad

Burrito(v), 4.29

--A large burrito,with whole pinto beans,salsa,guacamole

  • 关于printVegetarianMenu()方法内的try/catch
    • try/catch是一种错误处理的方法,而不是程序逻辑的方法。如果这样做,而是在调用isVegetarian()方法之前,用instanceof来检查菜单组件的运行时类型,来确定它是菜单项。但是这样做,就会因为无法统一处理菜单和菜单项而失去透明性;
    • 也可以改写MenuisVegetarian()方法,让它返回false。这提供了一个简单的解决方案,同时也保持了透明性;
    • 之所以使用try/catch,是为了传达:isVegetarian()是Menu没有支持的操作。这样的做法也允许后来人去为Menu实现一个合理的isVegetarian()方法,而我们不必为此再修改这里的代码了;
  • 模式对比

原创粉丝点击