Множественная диспетчеризация (Multiple dispatching)

Когда работаете со многими типами, которые взаимодействуют друг с другом, программа может стать грязной. В качестве примера можно привести систему, анализирующую и выполняющую математические выражения. Возможно, вы захотите написать Number + Number, Number * Number и т. п. где Number - базовый класс для ряда числовых объектов. Но когда вы пишите a + b и не знаете точный тип a и b, то как вы заставите их правильно взаимодействовать?

Ответ начинается с того, о чем вы, возможно, даже и не думали: Java выполняет только единую диспетчеризацию. То есть, если вы выполняете операцию над более чем одним объектом, тип которого не известен, Java может вызвать механизм динамического связывания только для одного из этих типов. Это не решает проблему, так что вы вынуждены определять некоторые типы вручную и воспроизводить свой собственный механизм динамического связывания.

Решение называется множественной диспетчеризацией. Помните, что полиморфизм может возникнуть только через вызов функций-методов, так что если вы хотите запустить механизм двойной диспетчеризации, должно быть два вызова функций-членов: первый определяет первый неизвестный тип, а второй вызов определяет второй неизвестный тип. С множественной диспетчеризацией вы должны иметь вызовы полиморфичных методов для определения каждого типа. Обычно, вы будете стараться получить такую конфигурацию, которая при единственном вызове метода производила более одного динамического вызова функции-члена и таким образом определяла более одного типа в процессе. Чтобы получить такой эффект, вам необходимо работать с более, чем одним вызовом полиморфичного метода: вам необходим один вызов для каждой диспетчеризации. Методы в следующем примере вызывают compete( ) и eval( ), в котором оба члена одинакового типа. (В этом случае будет только два диспетчера, что называется двойной диспетчеризацией). Если вы работаете с двумя разными взаимодействующими иерархиями типов, то вы будете иметь вызовы полиморфических методов для каждой иерархии.

Вот пример множественной диспетчеризации:

//: 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), вариант множественной диспетчеризации

Исходим из того, что главная иерархия класса, которая у вас есть, фиксирована; вероятно вы получили ее от стороннего производителя и вы не можете менять эту иерархию. Однако, вам нужно добавить новые полиморфические методы в эту иерархию, что обычно означает, что вы должны добавить что-то в интерфейс базового класса. Так что дилемма состоит в том, что вам нужно добавить методы в базовый класс, но вы не можете касаться базового класса. И что поделать с этим?

Шаблон проектирования, который позволяет решить проблему такого рода, называется "визитер" (это последний шаблон в книге "Design Patterns"), и он основывается на схеме двойной диспетчеризации, показанной в последнем разделе.

Шаблон визитера позволяет вам расширить интерфейс первичного типа путем создания отдельной иерархии классов типа Visiter для виртуализации операций, выполняемых над первичным типом. Объекты первичного типа просто "принимают" визитера, затем вызывают член-функцию визитера, выполняющую динамическое связывание.

//: visitor:BeeAndFlowers.java
// Демонтсрация шаблона "визитер".
package visitor;

import java.util.*;

import junit.framework.*;

interface Visitor {
  
void visit(Gladiolus g);
  
  
void visit(Runuculus r);
  
  
void visit(Chrysanthemum c);
}

// Иерархия Flower не может быть изменена:
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);
  
}
}
// /:~

#[BT_414]#

Упражнения

  1. Создайте среду бизнес-модели с тремя типами Жителей: Гном (для инженеров), Эльф (для торговцев) и Тролль (для менеджеров). Теперь создайте класс с названием Projrct, который создает различное население и заставляет их взаимодействовать (interact( )) друг с другом используя множественную диспетчеризацию.
  2. Измените предыдущий пример, чтобы сделать взаимодействие более детальным. Каждый Житель может случайным образом производить Оружие с помощью метода getWeapon( ): Гномы используют Jargon или Play, Эльфы используют InventFeature или SellImaginaryProduct а Тролли используют Edict и Schedule. Вы должны решить, какое оружие "выигрывает", а какое "проигрывает" при каждом взаимодействии (как и в PaperScissorsRock.java). Добавьте член-функцию battle( ) к Project, которая принимает двух Жителей и сравнивает их друг с другом. Теперь создайте член-функцию meeting( ) для Project, которая создает группы Гномов, Эльфов и Менеджеров, и организует битвы групп друг с другом пока не останутся члены только одной группы. Это "победители".
  3. Измените PaperScissorsRock.java, заменив двойную диспетчеризацию на таблицу поиска. Наиболее простой способ сделать это - создать Карту из Карт (Map), ключами в каждой Карте являются класс каждого объекта. Затем вы можете выполнить поиск, записав:
    ((Map)map.get(o1.getClass())).get(o2.getClass())
    Обратите внимание насколько легче переконфигурировать систему. Когда более подходящим способом является этот подход по сравнению с жестко закодированной динамической диспетчеризацией? Можете ли вы создать систему, которая имеет синтаксически упрощенное использование динамической диспетчеризации, но использует таблицу поиска?
  4. Измените Упражнение 2, чтобы в нем использовалась техника таблицы поиска, используемая в Упражнении 3.