复杂的交互(Complex interactions)
多路分派(Multiple dispatching)
处理多种类型之间的交互可能会使程序变的相当杂乱。比如,考虑一个解析和执行数学表达式的系统。你需要支持数字+数字,数字×数字,等等,这里的数字 (Number) 是一系列数字对象的基类。但是,当我们仅仅给出a+b,我们并不知道a或者b的确切类型,那我们又如何让它们正确的交互呢?
问题的答案可能是你所没有想到的:Java只能做单路分派(single dispatching)。也就是说,如果对多余一个的类型未知的对象进行操作,Java只能对这些类型中的一种类型启用动态绑定机制。这并不能解决上面的问题,所以最后你还是得自己手工(写代码)帧测类型并且产生你自己的动态绑定行为。
这个方案就是多路分派(multiple dispatching)。别忘了多态只能通过成员函数调用来实现,所以如果你想实现双路分派(double dispatching),就必须得有两次成员函数调用:第一次调用决定第一个未知类型,第二次调用决定第二个未知类型。对于多路分派,你必须得有一个可供调用的多态方法来决定所有类型。通常,你可以通过设置配置项来实现用一个成员函数调用产生多于一个的动态成员函数调用,从而在这个过程中决定多于一种的类型。为了做到这一点,你需要多于一个的多态方法调用:每一个分派都需要一个调用。下面例子里的(多态)方法是compete()和eval(),它们都是同一个类(型)的成员函数。(这种情况下,只有两次分派,也就是双路分派)。如果你需要处理的是两个体系(hierarchies)的不同类型之间的交互,那每个体系都必须得实现一个多态方法调用。
下面是一个多路分派的例子:(石头剪子布)
//: multipledispatch:PaperScissorsRock.java
// Demonstration of multiple dispatching.
package multipledispatch;
import java.util.*;
import junit.framework.*;
// An enumeration type:
class Outcome {
private String name;
private Outcome(String name) { this.name = name; }
public final static Outcome
WIN = new Outcome("wins"),
LOSE = new Outcome("loses"),
DRAW = new Outcome("draws");
public String toString() { return name; }
}
interface Item {
Outcome compete(Item it);
Outcome eval(Paper p);
Outcome eval(Scissors s);
Outcome eval(Rock r);
}
class Paper implements Item {
public Outcome compete(Item it) { return it.eval(this); }
public Outcome eval(Paper p) { return Outcome.DRAW; }
public Outcome eval(Scissors s) { return Outcome.WIN; }
public Outcome eval(Rock r) { return Outcome.LOSE; }
public String toString() { return "Paper"; }
}
class Scissors implements Item {
public Outcome compete(Item it) { return it.eval(this); }
public Outcome eval(Paper p) { return Outcome.LOSE; }
public Outcome eval(Scissors s) { return Outcome.DRAW; }
public Outcome eval(Rock r) { return Outcome.WIN; }
public String toString() { return "Scissors"; }
}
class Rock implements Item {
public Outcome compete(Item it) { return it.eval(this); }
public Outcome eval(Paper p) { return Outcome.WIN; }
public Outcome eval(Scissors s) { return Outcome.LOSE; }
public Outcome eval(Rock r) { return Outcome.DRAW; }
public String toString() { return "Rock"; }
}
class ItemGenerator {
private static Random rand = new Random();
public static Item newItem() {
switch(rand.nextInt(3)) {
default:
case 0: return new Scissors();
case 1: return new Paper();
case 2: return new Rock();
}
}
}
class Compete {
public static void match(Item a, Item b) {
System.out.println(
a + " " + a.compete(b) + " vs. " + b);
}
}
public class PaperScissorsRock extends TestCase {
static int SIZE = 20;
public void test() {
for(int i = 0; i < SIZE; i++)
Compete.match(ItemGenerator.newItem(),
ItemGenerator.newItem());
}
public static void main(String args[]) {
junit.textui.TestRunner.run(PaperScissorsRock.class);
}
} ///:~
访问者(Visitor),多路分派的一种
假设说你手头有一组早先的类层次体系(class hierachy),这些类都是固定不能改变的;可能它们是从第三方买来的,所以你没法改变这个类层次体系。但是,你可能想给这个类层次体系添加新的多态方法,通常情况下这必须得向基类接口添加新的东西。所以问题就来了:你既需要给基类添加新的方法,而你又不能动基类。那到底该怎么办呢?
解决这类问题的设计模式叫做“访问者(visitor)”(《设计模式》里最后讲到的那个),它是建立在上一节的双路分派机制之上的。
Visitor模式使得你可以通过创建另外一个独立的visitor类型的类层次体系来扩展原始类型的接口,从而仿真(virtualize)实现原本针对原始类型的操作。原始类型的对象只是简单的“接受”访问者,然后调用访问者动态绑定的成员函数。
//: visitor:BeeAndFlowers.java
// Demonstration of "visitor" pattern.
package visitor;
import java.util.*;
import junit.framework.*;
interface Visitor {
void visit(Gladiolus g);
void visit(Runuculus r);
void visit(Chrysanthemum c);
}
// The Flower hierarchy cannot be changed:
interface Flower {
void accept(Visitor v);
}
class Gladiolus implements Flower {
public void accept(Visitor v) { v.visit(this);}
}
class Runuculus implements Flower {
public void accept(Visitor v) { v.visit(this);}
}
class Chrysanthemum implements Flower {
public void accept(Visitor v) { v.visit(this);}
}
// Add the ability to produce a string:
class StringVal implements Visitor {
String s;
public String toString() { return s; }
public void visit(Gladiolus g) {
s = "Gladiolus";
}
public void visit(Runuculus r) {
s = "Runuculus";
}
public void visit(Chrysanthemum c) {
s = "Chrysanthemum";
}
}
// Add the ability to do "Bee" activities:
class Bee implements Visitor {
public void visit(Gladiolus g) {
System.out.println("Bee and Gladiolus");
}
public void visit(Runuculus r) {
System.out.println("Bee and Runuculus");
}
public void visit(Chrysanthemum c) {
System.out.println("Bee and Chrysanthemum");
}
}
class FlowerGenerator {
private static Random rand = new Random();
public static Flower newFlower() {
switch(rand.nextInt(3)) {
default:
case 0: return new Gladiolus();
case 1: return new Runuculus();
case 2: return new Chrysanthemum();
}
}
}
public class BeeAndFlowers extends TestCase {
List flowers = new ArrayList();
public BeeAndFlowers() {
for(int i = 0; i < 10; i++)
flowers.add(FlowerGenerator.newFlower());
}
public void test() {
// It's almost as if I had a function to
// produce a Flower string representation:
StringVal sval = new StringVal();
Iterator it = flowers.iterator();
while(it.hasNext()) {
((Flower)it.next()).accept(sval);
System.out.println(sval);
}
// Perform "Bee" operation on all Flowers:
Bee bee = new Bee();
it = flowers.iterator();
while(it.hasNext())
((Flower)it.next()).accept(bee);
}
public static void main(String args[]) {
junit.textui.TestRunner.run(BeeAndFlowers.class);
}
} ///:~
练习
1. 写一个商务建模系统,它包括三种类型的Inhabitant:侏儒(代表工程师),精灵(代表市场人员),巨人(代表经理)。写一个Project类,创建不同的inhabitants并且让他们交互,必须要用到多路分派。
2. 改写练习1,使他们的交互更详细。每一种Inhabitant都可以用getWeapon()随机的产生一个武器:侏儒用Jargon或者Play,精灵用InventFeature或者SellImaginaryProduct,巨人用Edioct和Schedule。你必须决定在每次交互中哪个武器赢那个武器输(就像PaperScissorsRock.java那样)。给Project类添加一个battle () 成员函数,它接受两种Inhabitants(作为参数),使它们俩火并。再给Project类添加一个meeting()成员函数,创建一组侏儒,精灵和巨人,并让各个组之间开战直到最后剩下一组获胜。
3. 改写PaperScissorsrock.java,用表查询的方法代替双路分派。最简单的做法使写一个值为Maps的Map,用每个对象的类名称来作为Map的键。然后通过下面这种方法查找:
((Map)map.get(o1.getClass())).get(o2.getClass())
使用这种方法非常容易重心配置系统。什么时候使用这种方法会比硬编码的动态分发更合适呢?你能否用表查找的方法写一个语法简单的系统来实现动态分派?
4. 用练习3那种表查找的方法重做练习2。