Приведенный выше дизайн определенно удовлетворительный. Добавление новых типов в систему заключается в добавлении или изменении отдельных классов, что не приведет к изменению кода, чтобы распространить новое на всю систему. Кроме того, нет "злоупотреблений" RTTI, как это было в RecycleA.java. Однако, можно продвинуться на один шаг вперед и пуристически взглянуть на RTTI и сказать, что его использование должно быть полностью исключено из операций сортировки мусора по корзинам.
Чтобы выполнить это, вы должны сначала осознать, что все типозависимые действия в перспективе - такие как определение типа куска мусора и помещение его в соответствующую корзину - должны контролироваться через полиморфизм и динамическое связывание.
Предыдущий пример сначала сортирует по типу, а потом работает над последовательностью элементов, которые все принадлежат определенному типу. Но всякий раз, когда вы заметите, что вы выбираете определенные типы, остановитесь и задумайтесь. Основа идея полиморфизма (динамическая привязка к вызываемому методу) состоит в обработке типозависимой информации вместо вас. Тогда почему вы охотитесь за типами?
Ответом является что-то типа того, о чем вы не думали: Java выполняет только единую диспетчеризацию. Таким образом, если вы выполняете операцию более, чем над одним объектом, тип которых неизвестен, Java вызывает механизм динамического связывания только для одного из этих типов. Это не решает проблемы, так что вам нужно отказаться от ручного определения некоторых типов и эффективно производить свое собственное поведение динамического связывания.
Такое решение называется множественной диспетчеризацией, что означает установку конфигурации, так чтобы единственный вызов метода производил более одного динамического вызова метода и таким образом определял более одного типа в процессе вызовов. Чтобы получить такой эффект вам нужно работать не с одной иерархией типов: вам будет необходима иерархия типов для каждого диспетчера. Следующий пример работает с двумя иерархиями: с существующим семейством Мусора и с иерархией типов мусорных корзин, в которые помещается мусор. Вторая иерархия не всегда очевидна и в этом случае ее необходимо создать для получения множественной диспетчеризации (в этом случае будет только два диспетчера, что называется двойной диспетчеризацией).
Реализация двойной диспетчеризации
Помните, что полиморфизм может действовать только посредством вызова метода, так что если вы хотите получить двойную диспетчеризацию, должно быть два вызова метода: каждый из них используется для определение типа внутри своей иерархии. В иерархии Мусора будет создано новый метод addToBin( ), который принимает в качестве аргумента массив Типизированных корзин (TypedBin). Он использует этот массив для перебора и попыток добавить себя в соответствующую корзину, и это то место, где вы увидите двойную диспетчеризацию.
Новой иерархией является Типизированные Корзины (TypedBin), которая имеет свой собственный метод add( ), который также используется полиморфически. Но здесь существует дополнительный наворот: add( ) является перегруженным, принимающим аргументы различных типов мусора. Таким образом, сущностью схемы двойной диспетчеризации является применение перегрузки.
Редизайн программы приводит к дилемме: теперь необходимо, чтобы базовый класс Мусора содержал метод addToBin( ). Один из подходов состоит в копировании всего кода и изменений в базовый класс. Другой подход, который вы можете применить, когда вы не контролируете весь исходный код, состоит в помещении метода addToBin в интерфейс, оставив Мусор в покое, и в наследовании новых специфических типов Aluminum, Paper, Glass и Cardboard. Этот подход мы и применим здесь.
Большинство классов в этом дизайне должны быть публичными, так что они помещаются в своих собственных файлах. Вот вам интерфейс:
//: refactor:doubledispatch:TypedBinMember.java
// Интерфейсл для добавления метода
двойной
// диспетчеризации в иерархию мусора
// без изменения оригинальной иерархии.
package refactor.doubledispatch;
interface TypedBinMember {
// Новый метод:
boolean addToBin(TypedBin[] tb);
} // /:~
В каждом определенном подтипе, Aluminum, Paper, Glass и Cardboard, реализуется метод addToBin( ) из интерфейса TypedBinMember, но это выглядит, как будто код в точности повторяется в каждом случае:
//: refactor:doubledispatch:DDAluminum.java
// Aluminum для двойной диспетчеризации.
package refactor.doubledispatch;
import refactor.trash.*;
public class DDAluminum extends Aluminum implements TypedBinMember {
public DDAluminum(double wt) {
super(wt);
}
public boolean addToBin(TypedBin[] tb) {
for (int i = 0; i < tb.length; i++)
if (tb[i].add(this))
return true;
return false;
}
} // /:~
//: refactor:doubledispatch:DDPaper.java
// Paper для двойной диспетчеризации.
package refactor.doubledispatch;
import refactor.trash.*;
public class DDPaper extends Paper implements TypedBinMember {
public DDPaper(double wt) {
super(wt);
}
public boolean addToBin(TypedBin[] tb) {
for (int i = 0; i < tb.length; i++)
if (tb[i].add(this))
return true;
return false;
}
} // /:~
//: refactor:doubledispatch:DDGlass.java
// Glass для двойной диспетчеризации.
package refactor.doubledispatch;
import refactor.trash.*;
public class DDGlass extends Glass implements TypedBinMember {
public DDGlass(double wt) {
super(wt);
}
public boolean addToBin(TypedBin[] tb) {
for (int i = 0; i < tb.length; i++)
if (tb[i].add(this))
return true;
return false;
}
} // /:~
//: refactor:doubledispatch:DDCardboard.java
// Cardboard для двойной диспетчеризации.
package refactor.doubledispatch;
import refactor.trash.*;
public class DDCardboard extends Cardboard implements TypedBinMember {
public DDCardboard(double wt) {
super(wt);
}
public boolean addToBin(TypedBin[] tb) {
for (int i = 0; i < tb.length; i++)
if (tb[i].add(this))
return true;
return false;
}
} // /:~
В коде каждого addToBin( ) вызывается add( ) для каждого объекта TrashBin из массива. Но обратите внимание, что аргументом является this. Тип this отличается для каждого подкласса Мусора, так что этот код различен. (Хотя этот код принесет пользу, когда механизм параметризированных типов будет добавлен в Java.) Таким образом, мы получили первую часть двойной диспетчеризации, поскольку внутри этого метода вы знаете какой тип у вас в руках: Aluminum или Paper и т.п. Во время вызова метода add( ) эта информация передается с помощью типа this. Компилятор перенаправляет вызов для правильной перегруженной версии метода add( ). Но так как tb[i] производит ссылку на базовый тип TypedBin, этот вызов будет заканчиваться вызовом различных методов, в зависимости от типа TypedBin выбранного объекта. Это вторая диспетчеризация.
Вот базовый класс для TypedBin:
//: refactor:doubledispatch:TypedBin.java
// Контейнер для второй диспетчеризации.
package refactor.doubledispatch;
import refactor.trash.*;
import java.util.*;
public abstract class TypedBin {
Collection c = new ArrayList();
protected boolean addIt(Trash t) {
c.add(t);
return true;
}
public Iterator iterator() {
return c.iterator();
}
public boolean add(DDAluminum a) {
return false;
}
public boolean add(DDPaper a) {
return false;
}
public boolean add(DDGlass a) {
return false;
}
public boolean add(DDCardboard a) {
return false;
}
} // /:~
Вы можете видеть, что все перегруженные методы add( ) возвращают false. Если метод не перегружается в наследуемом классе, он будет продолжать возвращать false, и вызывающий метод (addToBin( ) в данном случае) решит, что текущий объект Мусора не был успешно добавлен в контейнер, и продолжит искать подходящий контейнер.
В каждом из подклассов TypedBin перегружается только один из методов, в соответствии с типом, для которого эта корзина создается. Например, CardboardBin перегружает add(DDCardboard). Перегруженный метод добавляет мусора в свой контейнер и возвращает true, в то время, как все остальные методы в CardboardBin возвращают false, так как они не перегружены. Это еще один случай, когда механизм параметризованных типов Java мог бы позволить автоматическую генерацию кода. (С помощью шаблонов в C++ вам не понадобилось бы явно писать подклассы или помещать метод addToBin( ) в Trash.)
Так как для этого примера типы мусора были подстроены и помещены в другой директорий, вам нужен другой файл с данными мусора, чтобы заставить все работать. Вот возможный пример DDTrash.dat:
//:! refactor:doubledispatch:DDTrash.dat
refactor.doubledispatch.DDGlass:54
refactor.doubledispatch.DDPaper:22
refactor.doubledispatch.DDPaper:11
refactor.doubledispatch.DDGlass:17
refactor.doubledispatch.DDAluminum:89
refactor.doubledispatch.DDPaper:88
refactor.doubledispatch.DDAluminum:76
refactor.doubledispatch.DDCardboard:96
refactor.doubledispatch.DDAluminum:25
refactor.doubledispatch.DDAluminum:34
refactor.doubledispatch.DDGlass:11
refactor.doubledispatch.DDGlass:68
refactor.doubledispatch.DDGlass:43
refactor.doubledispatch.DDAluminum:27
refactor.doubledispatch.DDCardboard:44
refactor.doubledispatch.DDAluminum:18
refactor.doubledispatch.DDPaper:91
refactor.doubledispatch.DDGlass:63
refactor.doubledispatch.DDGlass:50
refactor.doubledispatch.DDGlass:80
refactor.doubledispatch.DDAluminum:81
refactor.doubledispatch.DDCardboard:12
refactor.doubledispatch.DDGlass:12
refactor.doubledispatch.DDGlass:54
refactor.doubledispatch.DDAluminum:36
refactor.doubledispatch.DDAluminum:93
refactor.doubledispatch.DDGlass:93
refactor.doubledispatch.DDPaper:80
refactor.doubledispatch.DDGlass:36
refactor.doubledispatch.DDGlass:12
refactor.doubledispatch.DDGlass:60
refactor.doubledispatch.DDPaper:66
refactor.doubledispatch.DDAluminum:36
refactor.doubledispatch.DDCardboard:22
///:~
Вот остальная часть программы:
//: refactor:doubledispatch:DoubleDispatch.java
// Используем множественную диспетчеризацию
для обработки
// более одного неизвестного типа
во время вызова метода.
package refactor.doubledispatch;
import refactor.trash.*;
import java.util.*;
import junit.framework.*;
class AluminumBin extends TypedBin {
public boolean add(DDAluminum a) {
return addIt(a);
}
}
class PaperBin extends TypedBin {
public boolean add(DDPaper a) {
return addIt(a);
}
}
class GlassBin extends TypedBin {
public boolean add(DDGlass a) {
return addIt(a);
}
}
class CardboardBin extends TypedBin {
public boolean add(DDCardboard a) {
return addIt(a);
}
}
class TrashBinSet {
private TypedBin[] binSet = { new AluminumBin(), new PaperBin(),
new GlassBin(), new CardboardBin() };
public void sortIntoBins(Iterator it) {
while (it.hasNext()) {
TypedBinMember t = (TypedBinMember) it.next();
if (!t.addToBin(binSet))
System.err.println("Couldn't add " + t);
}
}
public TypedBin[] binSet() {
return binSet;
}
}
public class DoubleDispatch extends TestCase {
Collection bin = new ArrayList();
TrashBinSet bins = new TrashBinSet();
public DoubleDispatch() {
// Разбор мусора
все еще работает без изменений:
ParseTrash.fillBin("DDTrash.dat", bin);
}
public void test() {
// Сортируем
из главной корзины в индивидуальные
// типизированные
корзины:
bins.sortIntoBins(bin.iterator());
TypedBin[] tb = bins.binSet();
// Выполняем
sumValue для каждой корзины...
for (int i = 0; i < tb.length; i++)
Trash.sumValue(tb[i].c.iterator());
// ... и для
главной корзины
Trash.sumValue(bin.iterator());
}
public static void main(String args[]) {
junit.textui.TestRunner.run(DoubleDispatch.class);
}
} // /:~
TrashBinSet инкапсулирует все различные типы TypedBin, наряду с методом sortIntoBins( ), в котором имеет место двойная диспетчеризация. Вы можете видеть, что после внесения изменений в структуру, сортировка по Типизированным Корзинам стала заметно легче. Кроме того, эффективность двух динамических вызовов метода лучше, чем любой другой способ сортировки.
Обратите внимание на легкость использования системы в main( ), а также на полную независимость от любых специфических типов внутри main( ). Все другие методы, которые общаются только с интерфейсом базового класса Trash будут аналогично неуязвимы к изменениям в типах Trash.
Изменения, которые необходимо вносить при добавлении новых типов, относительно изолированы: вам нужно изменить TypedBin, наследовать новый тип Мусора со своим собственным методом addToBin( ), затем наследовать новый тип Типизированной Корзины (это просто копирование и небольшое редактирование), и наконец добавить новый тип в сборную инициализацию для TrashBinSet.
← | Абстрагирование использования | Шаблон Посетителя (The Visitor pattern) | → |