В этой главе будет дан обзор процесса решения проблемы путем применения шаблонов методом эволюции. Для этого дизайн первого приближения будет использован для начального решения, а затем это решение будет проверяться и будут применяться различные шаблоны проектирования к проблеме (некоторые из которых будут работать, а другие не будут). Ключевым вопросом, с помощью которого мы будет искать лучшее решение, является "что изменится?".
Этот процесс похож на то, что Мартин Фовлер (Martin Fowler) говорит в своей книге "Refactoring: Improving the Design of Existing Code"[12] (хотя он имеет тенденцию говорить о части кода чаще, чем о дизайне уровня шаблона). Вы начнете с решения, а затем, когда обнаружите, что оно не удовлетворяет вашим требованиям, вы исправите это. Конечно, это естественная тенденция, но в компьютерном программировании это будет очень сложно выполнить при применении процедурного программирования, и принятие идеи, что мы можем переработать и дизайн, прибавляет к общей массе доказательство, что объектно-ориентированное программирование - это "хорошая вещь".
Симулятор переработки мусора
Природа этой проблемы состоит в том, что мусор выбрасывается без сортировки в единую корзину, так что специфичная часть информации теряется. Но позже специфическая информация может быть восстановлена для достижения правильной сортировки мусора. В качестве начального решения используем RTTI (описано в 12 главе Thinking in Java, 2nd edition).
Это не простой дизайн, поскольку он налагает ограничения. Поэтому он так интересен - это больше походит на те беспорядочные проблемы, которые вы имеете на вашей работе. Дополнительное ограничение состоит в том, что мусор поступает на мусороперерабатывающую станцию перемешанным. Программа должна смоделировать сортировку мусора. В этом месте вступает RTTI: у вас есть куча неизвестных частей мусора, а программа оценивает, к какому точно типу он относится.
//: refactor:recyclea:RecycleA.java
// Переработка с RTTI.
package refactor.recyclea;
import java.util.*;
import java.io.*;
import junit.framework.*;
abstract class Trash {
private double weight;
Trash(double wt) {
weight = wt;
}
abstract double getValue();
double getWeight() {
return weight;
}
// Суммируем количество Мусора в корзине:
static void sumValue(Iterator it) {
double val = 0.0f;
while (it.hasNext()) {
// Первый вид RTTI:
// Проверка динамическим приведением
Trash t = (Trash) it.next();
// Полиморфизм в действии:
val += t.getWeight() * t.getValue();
System.out.println("weight of " +
// Используем RTTI для получения типовой
// информации о классе:
t.getClass().getName() + " = " + t.getWeight());
}
System.out.println("Total value = " + val);
}
}
class Aluminum extends Trash {
static double val = 1.67f;
Aluminum(double wt) {
super(wt);
}
double getValue() {
return val;
}
static void setValue(double newval) {
val = newval;
}
}
class Paper extends Trash {
static double val = 0.10f;
Paper(double wt) {
super(wt);
}
double getValue() {
return val;
}
static void setValue(double newval) {
val = newval;
}
}
class Glass extends Trash {
static double val = 0.23f;
Glass(double wt) {
super(wt);
}
double getValue() {
return val;
}
static void setValue(double newval) {
val = newval;
}
}
public class RecycleA extends TestCase {
Collection bin = new ArrayList(), glassBin = new ArrayList(),
paperBin = new ArrayList(), alBin = new ArrayList();
private static Random rand = new Random();
public RecycleA() {
// Наполним Мусором корзину:
for (int i = 0; i < 30; i++)
switch (rand.nextInt(3)) {
case 0:
bin.add(new Aluminum(rand.nextDouble() * 100));
break;
case 1:
bin.add(new Paper(rand.nextDouble() * 100));
break;
case 2:
bin.add(new Glass(rand.nextDouble() * 100));
}
}
public void test() {
Iterator sorter = bin.iterator();
// Отсортируем Мусор:
while (sorter.hasNext()) {
Object t = sorter.next();
// RTTI покажет членство в классах:
if (t instanceof Aluminum)
alBin.add(t);
if (t instanceof Paper)
paperBin.add(t);
if (t instanceof Glass)
glassBin.add(t);
}
Trash.sumValue(alBin.iterator());
Trash.sumValue(paperBin.iterator());
Trash.sumValue(glassBin.iterator());
Trash.sumValue(bin.iterator());
}
public static void main(String args[]) {
junit.textui.TestRunner.run(RecycleA.class);
}
} // /:~
В исходном коде для этой книги этот файл будет помещен в поддиректорий recyclea, который не включен в поддиректорий refractor. Инструмент распаковки позаботится о помещении его в правильный директорий. Причина такого размещения состоит в том, что глава переписывает этот пример несколько раз и помещает каждую версию в свой собственный директорий (с использованием пакета по умолчанию в каждом директории, чтобы облегчить работу программы), так что имена классов не конфликтуют.
Несколько объектов ArrayList создаются для хранения Мусор (Trash). Конечно, ArrayList содержит Object, так что они могут содержать все, что угодно. Причина для того, чтобы они хранили Мусор (Trash) состоит в том, чтобы вы были осторожны и не положили ничего, кроме Trash. Если вы поместите что-то "неправильное" в ArrayList, вы не получите никаких предупреждений или ошибок во время компиляции - вы узнаете об этом только через исключение во время выполнения.
Когда добавляется ссылка Trash, она теряет свою специфическую идентичность и становится просто ссылкой на Object (над ней происходит восходящее приведение). Однако по причине полиморфизма происходит правильное течение программы, когда вызывается метод динамического ограничения посредством сортирующего Итератора, в котором результирующий Object будет приведен назад к типу Trash. sumValue( ) также получает Iterator для выполнения операций над каждым объектом из ArrayList.
Это выглядит как простое преобразование типа Trash в контейнере, содержащем базовый тип, и обратное приведение типа. Почему бы ни поместить мусор в соответствующий приемник сразу? (Неужели в этом вся загадка переработки). В такой программе было бы легче перерабатывать, но иногда структура системы и гибкость могут принести выгоду от приведения к базовому классу.
Программа удовлетворяет требованиям дизайна: она работает. Это может быть здорово, если это разовое решение. Однако полезные программы имеют тенденцию эволюционировать со временем, так что вы должны спросить: "Что, если ситуация изменится?" Например, если картон станет допустимым к обработке, то как эту возможность можно интегрировать в систему (особенно, если программа большая и сложная). Так как приведенный выше код основывается на выражении switch, которое может быть размыто по всей программе, вы должны просматривать весь код каждый раз, когда добавляется новый тип, и если вы пропустите одном из мест, компилятор не даст вам никакой помощи, указав на ошибку.
Ключевой момент неправильного использования RTTI в данном случае заключается в том, что проверяется каждый тип. Если вы ищите только определенное подмножество типов, поскольку это подмножество имеет специальную обработку, то возможно это хорошо. Но если вы охотитесь за каждым типом внутри выражения switch, то вы, вероятно, упускаете важный момент, и определенно делаете ваш код более трудным в уходе. В следующем разделе мы взглянем на то, как эта программа эволюционирует через несколько этапов и станет более гибкой. Это должно показать значимый пример дизайна программ.
Улучшение дизайна
Решения в книге "Design Patterns" организованы вокруг вопроса "Что изменится при эволюции этой программы?" Обычно это наиболее важный вопрос, который вы можете задать при любом дизайне. Если вы хотите построить вашу систему вокруг ответа, результат может быть двойственный: ваша система не только позволит легкий (и дешевый) уход, но вы можете также произвести компоненты, которые можно повторно использовать, так что другие системы могут быть построены с меньшими затратами. Это обещает объектно-ориентированное программирование, но это не происходит автоматически. Для этого требуется думать и понимать вашу часть. В этом разделе мы посмотрит на то, как происходит этот процесс во время детализации системы.
Ответ на вопрос "Что измениться?" для перерабатывающей системы единственный: можно добавить больше типов в систему. Цель дизайна состоит в том, чтобы сделать это добавление типов настолько безболезненным, насколько это возможно. В перерабатывающей программе мы хотим инкапсулировать все части, в которых указывается информация о типе, как упоминается, так чтобы (если нет других причин) любые изменения могли быть локализованы в этой инкапсуляции. Это заставляет нас активизировать процесс по очистке остального кода.
"Создание большего количества объектов"
Здесь проявляется главный принцип объектно-ориентированного дизайна, о котором я впервые говорил с Грэди Бучем (Grady Booch): "Если дизайн чрезмерно сложный, сделайте больше объектов". Это одновременно является и противоречащим интуиции, и простым до нелепости, и все же - это наиболее полезный совет, который я нашел. (Вы можете заметить, что "создание большего числа объектов" часто эквивалентно "добавлению другого уровня обходных путей".) В общем, если вы нашли место с беспорядочным кодом, решите, класс какого рода мог бы очистить его. Часто второстепенным эффектом очистки является то, что система становится лучше структурированной и более гибкой.
Рассмотрим первое место, в котором создаются объекты Мусора - это выражение switch внутри main( ):
for(int i = 0; i < 30; i++)
switch((int)(rand.nextInt(3)) {
case 0 :
bin.add(new
Aluminum(rand.nextDouble() * 100));
break;
case 1 :
bin.add(new
Paper(rand.nextDouble() * 100));
break;
case 2 :
bin.add(new
Glass(rand.nextDouble() * 100));
}
Это точно беспорядочный код, а также место, которое вы обязаны менять, когда добавляете новый тип. Если добавление новых типов - это обычное дело, лучшим решением будет единственный метод, который принимает всю необходимую информацию и производит ссылку на объект правильного типа, уже приведенного к базовому классу объекта мусора. В книге "Design Patterns" это названо, как шаблон создания (которых здесь несколько). Специальный шаблон, который применен здесь, является вариацией Метода Фабрики. В данном случае метод фабрики является статическим членом класса Trash, но в более общем случае этот метод является перегруженным в наследуемом классе.
Идея метода фабрики состоит в том, что вы передаете наиважнейшую информацию, которую нужно знать для создания вашего объекта, а затем просто ждете получения ссылки (уже приведенной к базовому типу), которая будет получена в качестве возвращаемого значения. С этого момента вы рассматриваете объект полиморфически. Таким образом, вам не нужно знать точного типа объекта, который уже создан. Фактически, метод фабрики прячет его от вас, предотвращая неверное употребление. Если вы хотите использовать объект без полиморфизма, вы должны явно использовать RTTI и приведение типов.
Но здесь есть небольшая проблема, особенно когда вы используете более сложный подход (не покорзинный здесь) создания метода фабрики в базовом классе и перегрузке его в наследуемых классах. Что если требуемая информация в наследуемом классе включает больше аргументов или отличающиеся аргументы? Создание большего количества объектов - решает эту проблему. Для реализации метода фабрики класс Trash получает новый метод с названием factory. Чтобы спрятать создающие данные, возьмем новый класс, с названием Messenger, который несет всю необходимую информацию методу factory для создания соответствующего объекта Мусора (мы начинали упоминать Посыльного (Messenger), как о шаблоне проектирования, но это достаточно просто, так что вы можете не придавать ему этот статус). Вот простейшая реализация Messenger:
class Messenger {
int type;
// Должны изменить это для
добавления другого типа:
static final int MAX_NUM = 4;
double data;
Messenger(int typeNum, double val) {
type = typeNum % MAX_NUM;
data = val;
}
}
Работа объекта Messenger состоит только в содержании информации для метода factory( ). Теперь, если есть ситуация, в котором методу factory( ) необходимо больше информации или информация другого рода для создания нового типа объекта Мусора, нет необходимости в изменении интерфейса factory( ). Класс Messenger может быть изменен путем добавления новых данных и новых конструкторов или более типичным способом для объектно-ориентированного подхода - путем создания подклассов.
Метод factory( ) для этого примера работает следующим образом:
static Trash factory(Messenger i) {
switch (i.type) {
default: // Чтобы успокоить компилятор
case 0:
return new Aluminum(i.data);
case 1:
return new Paper(i.data);
case 2:
return new Glass(i.data);
// :
case 3:
return new Cardboard(i.data);
}
}
Таким образом, определение точного типа объекта достаточно просто, но вы можете представить более сложные системы, в которых factory( ) использует более сложный алгоритм. Теперь это спрятано в одном месте, и вы знаете, куда нужно обратится, чтобы добавить новый тип.
Создание новых объектов теперь упростится в main( ):
for(int i = 0; i < 30; i++)
bin.add(
Trash.factory(
new Messenger(
rand.nextInt(Messenger.MAX_NUM),
rand.nextDouble() * 100)));
Теперь создается объект Messenger для передачи данных в метод factory( ), которые необходимы для создание некоторого объекта Мусора в куче и возврата ссылки, которая добавляется в корзину ArrayList bin. Конечно, если вы измените количество и тип аргументов, это выражение опять должно быть изменено, но изменений будет меньше, если создание объекта Messenger будет автоматизировано. Например, в конструктор Messenger может быть передан список (ArryaList) аргументов (или напрямую в вызов метода factory( ), в сущности). Поэтому требуется, чтобы аргументы анализировались и проверялись в момент выполнения, что предоставит великолепную гибкость.
Из этого кода видно, что "вектор изменений" проблемы фабрики ответственен за решение: если вы добавляете новые типы в систему (изменение), вы должны изменить код только внутри фабрики, так что фабрика изолирует эффект изменений.
Шаблон прототипного создания
Проблема приведенного выше дизайна состоит в том, что в нем все еще требуется центральное расположение, при котором все типы объектов должны быть известны: внутри метода factory( ). Если в систему регулярно добавлять новые типы, метод factory( ) должен меняться при появлении каждого нового типа. Когда вы обнаружите такое поведение, полезно попробовать пройти на шаг вперед и переместить всю информацию о типе - включая его создание - в класс, представляющий этот тип. Таким образом, все, что вам нужно для добавления нового типа в систему, это наследовать единственный класс.
Для перемещения информации относительно создания типа внутрь каждого специфического типа Мусора будет использован шаблон "прототипа" (из книги "Design Patterns"). Основа идеи состоит в том, что у вас есть главная последовательность объектов, каждый из которых заинтересован в создании своего типа. Объекты в последовательности используются только для создания новых объектов, с помощью операций, которые похожи на схему клонирования (clone( )), встроенную в корневой класс Java Object. В этом случае мы назовем метод клонирования tClone( ). Когда вы готовы создать новый объект, вероятно у вас есть некоторая информация, которая определяет тип объекта, который вы хотите создать, затем вы выполняете главную последовательность сравнения вашей информации с какой-либо информацией, содержащейся в прототипных объектах главной последовательности. Когда вы обнаружите нужное вам совпадение, вы клонируете объект.
В этой схеме нет жестко закодированной информации для создания. Каждый объект знает, как раскрывать соответствующую информацию и как клонировать себя. В таком случае нет необходимости менять метод factory( ) при добавлении нового типа в систему.
Один из подходов к проблеме прототипов состоит в добавлении нескольких методов для поддержки создания новых объектов. Однако, в Java 1.1 уже есть поддержка создания новых объектов, если у вас есть объект Class. С помощью Java 1.1 рефлексии (представленной в главе 12 книги "Thinking in Java, 2nd edition") вы можете вызвать конструктор, даже если у вас есть только объект Class. Это великолепное решения для проблемы прототипов.
Список прототипов будет представлен неявно с помощью списка на все объекты типа Class, которые вы хотите создать. Кроме того, если прототип не сработает, метот factory( ) предположит, что это произошло потому, что определенный объект Class не находится в списке и попробует загрузить его. При такой динамической загрузке прототипов классу Мусора не нужно знать с какими типами он работает, так что не потребуется изменений при добавлении новых типов. Это позволит облегчить повторное использование в оставшейся части главы.
//: refactor:trash:Trash.java
// Базовый класс для примера переработки
Мусора.
package refactor.trash;
import java.util.*;
import java.lang.reflect.*;
public abstract class Trash {
private double weight;
public Trash(double wt) {
weight = wt;
}
public Trash() {
}
public abstract double getValue();
public double getWeight() {
return weight;
}
// Суммируем значения Мусора, передавая
// Iterator на любой контейнер Мусора:
public static void sumValue(Iterator it) {
double val = 0.0f;
while (it.hasNext()) {
// Один из сортов RTTI: Динамическая проверка - приведение типа
Trash t = (Trash) it.next();
val += t.getWeight() * t.getValue();
System.out.println("weight of " +
// Используем RTTI для получения типичной
// информации относительно класса:
t.getClass().getName() + " = " + t.getWeight());
}
System.out.println("Total value = " + val);
}
public static class Messenger {
public String id;
public double data;
public Messenger(String name, double val) {
id = name;
data = val;
}
}
// Оставшаяся часть класса предоставляет
// поддержку прототипов:
private static List trashTypes = new ArrayList();
public static Trash factory(Messenger info) {
Iterator it = trashTypes.iterator();
while (it.hasNext()) {
// Каким-то образом определяем новый тип
// для создания и создаем его:
Class tc = (Class) it.next();
if (tc.getName().indexOf(info.id) != -1) {
try {
// Получаем метод динамического конструктора,
// принимающего аргумент типа double:
Constructor ctor = tc
.getConstructor(new Class[] { double.class });
// Вызываем конструктор
// для создания объекта:
return (Trash) ctor.newInstance(new Object[] { new Double(
info.data) });
}
catch (Exception e) {
throw new RuntimeException("Cannot Create Trash", e);
}
}
}
// Класса не было в списке. Пробуем загрузить его,
// но он должен в вашем class path!
try {
System.out.println("Loading " + info.id);
trashTypes.add(Class.forName(info.id));
}
catch (Exception e) {
throw new RuntimeException("Prototype not found", e);
}
// Загрузка успешна.
// Должен работать рекурсивный вызов:
return factory(info);
}
} // /:~
Основной класс Trash и метод sumValue( ) остается таким, как и прежде. Оставшаяся часть класса поддерживает шаблон прототипа. Прежде всего, вы видите два внутренних класса (которые сделаны статическими, так что они являются внутренними только с точки зрения организации кода), описывающие исключения, которые могут возникнуть. Далее следует список (ArrayList), называемый trashTypes, который используется для хранения Class.
В методе Trash.factory( ) строка (String id) внутри объекта Messenger (эта версия класса Messenger отличается от описанной ранее) содержит имя типа Мусора для создания. Эта строка сравнивается с именем класса (Class) из списка. Если есть совпадение, то это тот объект, который нам нужно создать. Конечно, есть много способов для определения, какой объект вы хотите создать. Здесь используется такой, при котором информация, прочитанная из файла, может быть помещена в объект.
Как только вы определили какой вид мусора нужно создать, вступает в игру метод рефлексии. Метод getConstructor( ) получает агрумент, являющийся массивом Class. Этот массив представляет аргументы, в правильном порядке, для конструктора, который вы ищите. В данном случае, массив создается динамически с помощью синтаксиса создания массива в Java 1.1:
new Class[] {double.class}
Этот код предполагает, что каждый тип Мусора имеет конструктор, который получает double (заметьте, что double.class отличается от Double.class). Так же возможно, для более гибкого решения, вызывать метод getConstructors( ), который возвращает массив возможных конструкторов.
То, что возвращается из getConstructor( ) является ссылкой на объект Constructor (часть java.lang.reflect). Вы вызываете конструктор динамически с помощью метода newInstance( ), который принимает массив из Object, содержащий реальные аргументы. Этот массив опять таки создается с помощью синтаксиса Java 1.1:
new Object[]{new Double(Messenger.data)}
Однако в данном случае значение double должно помещаться внутрь оберточного класса, так как оно должно быть частью массива объектов. В процессе вызова newInstance( ) значение double выделяется, но вас это может несколько смутить - аргумент может быть как double, так и Double, но когда вы делаете вызов, вы всегда должны передавать Double. К счастью, эта особенность существует только для примитивных типов.
Как только вы поймете, как делать это, процесс создания новых объектов при передаче только ссылок Class, станет заметно легче. Рефлексия также позволяет вам вызывать методы таким же динамическим образом.
Конечно, соответствующая ссылка Class может и не быть в списке trashTypes. В этом случае return во внутреннем цикле не будет выполнен, и вы дойдете до конца. Здесь программа попробует исправить ситуацию путем динамической загрузки объекта Class и добавления его в список trashTypes. Если он все еще не может быть найден, то это действительно ошибка, но если загрузка прошла успешно, то метод factory( ) вызывается рекурсивно для повторной попытки.
Как вы увидите, превосходство этого дизайна состоит в том, что этот код не нужно менять, вне зависимости от разных ситуаций он будет использоваться (предполагается, что все подклассы Мусора имеют конструктор, который принимает аргумент double).
Подклассы Мусора
Чтобы влиться в схему прототипов необходимо удовлетворить только одному требованию, чтобы каждый новый подкласс Мусора содержал конструктор, принимающий аргумент типа double. Рефлексия Java заботится обо всем остальном.
Вот различные типы мусора, каждый из которых содержится в собственном файле, но является частью пакета Trash (опять таки, чтобы облегчить повторное использование внутри главы):
//: refactor:trash:Aluminum.java
// Класс Aluminum с прототипностью.
package refactor.trash;
public class Aluminum extends Trash {
private static double val = 1.67f;
public Aluminum(double wt) {
super(wt);
}
public double getValue() {
return val;
}
public static void setValue(double newVal) {
val = newVal;
}
} // /:~
//: refactor:trash:Paper.java
// Класс Paper с прототипностью.
package refactor.trash;
public class Paper extends Trash {
private static double val = 0.10f;
public Paper(double wt) {
super(wt);
}
public double getValue() {
return val;
}
public static void setValue(double newVal) {
val = newVal;
}
} // /:~
//: refactor:trash:Glass.java
// Класс Glass с прототипностью.
package refactor.trash;
public class Glass extends Trash {
private static double val = 0.23f;
public Glass(double wt) {
super(wt);
}
public double getValue() {
return val;
}
public static void setValue(double newVal) {
val = newVal;
}
} // /:~
А вот новый тип Мусора:
//: refactor:trash:Cardboard.java
// Класс Cardboard с прототипностью.
package refactor.trash;
public class Cardboard extends Trash {
private static double val = 0.23f;
public Cardboard(double wt) {
super(wt);
}
public double getValue() {
return val;
}
public static void setValue(double newVal) {
val = newVal;
}
} // /:~
Вы можете заметить, что, за исключением конструктора, в любом из этих классов нет ничего специального.
Разбор Мусора из внешнего файла
Информация об объектах Мусора будет читаться из внешнего файла. Файл содержит всю необходимую информацию о каждом куске мусора в одной строке в виде Мусор:вес, например:
//:! refactor:trash:Trash.dat
refactor.trash.Glass:54
refactor.trash.Paper:22
refactor.trash.Paper:11
refactor.trash.Glass:17
refactor.trash.Aluminum:89
refactor.trash.Paper:88
refactor.trash.Aluminum:76
refactor.trash.Cardboard:96
refactor.trash.Aluminum:25
refactor.trash.Aluminum:34
refactor.trash.Glass:11
refactor.trash.Glass:68
refactor.trash.Glass:43
refactor.trash.Aluminum:27
refactor.trash.Cardboard:44
refactor.trash.Aluminum:18
refactor.trash.Paper:91
refactor.trash.Glass:63
refactor.trash.Glass:50
refactor.trash.Glass:80
refactor.trash.Aluminum:81
refactor.trash.Cardboard:12
refactor.trash.Glass:12
refactor.trash.Glass:54
refactor.trash.Aluminum:36
refactor.trash.Aluminum:93
refactor.trash.Glass:93
refactor.trash.Paper:80
refactor.trash.Glass:36
refactor.trash.Glass:12
refactor.trash.Glass:60
refactor.trash.Paper:66
refactor.trash.Aluminum:36
refactor.trash.Cardboard:22
///:~
Обратите внимание, что класс должен включать путь, когда указывается его имя, в противном случае класс не будет найден.
Этот файл читается с использованием ранее определенного инструмента StringList, а каждая строчка разбивается с помощью метода класса String indexOf( ) для получения индекса символа ':'. После этого используется метод substring( ) класса String для выделения имени типа мусора, а далее получается вес, который переводится в double с помощью метода static Double.valueOf( ). Метод trim( ) удаляет пробелы с обеих сторон строки.
Разборщик Мусора помещен в отдельном файле, так как он будет использоваться повторно в этой главе:
//: refactor:trash:ParseTrash.java
// Разбор файла, содержащего объекты
Мусора,
// Помещаем каждый в хранилище Fillable
holder.
package refactor.trash;
import java.util.*;
import java.io.*;
import com.bruceeckel.util.StringList;
public class ParseTrash {
public static void fillBin(String filePath, Fillable bin) {
Iterator it = new StringList(filePath).iterator();
while (it.hasNext()) {
String line = (String) it.next();
String type = line.substring(0, line.indexOf(':')).trim();
double weight = Double.valueOf(
line.substring(line.indexOf(':') + 1).trim()).doubleValue();
bin.addTrash(Trash.factory(new Trash.Messenger(type, weight)));
}
}
// Специальный случай для
обработки Collection:
public static void fillBin(String filePath, Collection bin) {
fillBin(filePath, new FillableCollection(bin));
}
} // /:~
В RecycleA.java использовался ArrayList для хранения объектов Мусора. Однако, можно использовать и другие типы контейнеров. Чтобы сделать это, первая версия fillBin( ) будет получать ссылку на Fillable, который просто является интерфейсом, который поддерживает метод, называемый addTrash( ):
//: refactor:trash:Fillable.java
// Любой объект, который может быть
помещен в Мусор.
package refactor.trash;
public interface Fillable {
void addTrash(Trash t);
} // /:~
Все, что поддерживает этот интерфейс, может использоваться в качестве fillBin. Конечно же, Collection не реализует Fillable, так что это не работает. Так как Collection используется в большинстве примеров, имеет смысл добавить второй перегруженный метод fillBin( ), принимающий Collection. Тогда любой объект Collection может быть использован, как Fillable, используя класс-адаптер:
//: refactor:trash:FillableCollection.java
// Адаптер, превращающий Collection
в Fillable.
package refactor.trash;
import java.util.*;
public class FillableCollection implements Fillable {
private Collection c;
public FillableCollection(Collection cc) {
c = cc;
}
public void addTrash(Trash t) {
c.add(t);
}
} // /:~
Как вы видите, единственное назначение этого класса состоит в соединении метода addTrash из Fillable с методом класса Collection add( ). Имея этот класс, перегруженный метод fillBin может быть использован с Collection в ParseTrash.ava:
public static void fillBin(String filePath, Collection bin) {
fillBin(filePath, new FillableCollection(bin));
}
Этот подход работает с любым классом контейнера, которые часто используются. Другой вариант - когда класс контейнера может предоставить свой собственный адаптер, который реализует Fillable. (Вы увидите это позже, в DynaTrash.java.)
Переработка с прототипами
Теперь вы можете увидеть пересмотренную версию RecycleA.java с использованием техники прототипов:
//: refactor:recycleap:RecycleAP.java
// Recycling with RTTI and Prototypes.
package refactor.recycleap;
import refactor.trash.*;
import java.util.*;
import junit.framework.*;
public class RecycleAP extends TestCase {
Collection bin = new ArrayList(), glassBin = new ArrayList(),
paperBin = new ArrayList(), alBin = new ArrayList();
public RecycleAP() {
// Заполняем
корзинау Мусора:
ParseTrash.fillBin("../trash/Trash.dat", bin);
}
public void test() {
Iterator sorter = bin.iterator();
// Сортируем
мусор:
while (sorter.hasNext()) {
Object t = sorter.next();
// RTTI для показа членства в классе:
if (t instanceof Aluminum)
alBin.add(t);
else if (t instanceof Paper)
paperBin.add(t);
else if (t instanceof Glass)
glassBin.add(t);
else
System.err.println("Unknown type " + t);
}
Trash.sumValue(alBin.iterator());
Trash.sumValue(paperBin.iterator());
Trash.sumValue(glassBin.iterator());
Trash.sumValue(bin.iterator());
}
public static void main(String args[]) {
junit.textui.TestRunner.run(RecycleAP.class);
}
} // /:~
Все объекты Мусора, так же, как и ParseTrash и классы поддержки, теперь являются частью класса refractor.trash, так что они просто импортируются.
Процесс открытия файла данных, содержащего описание Мусора, и разбор этого файла был помещен в статический метод ParseTrash.fillBin( ), так что теперь это не является частью фокусов дизайна. Вы увидите, что в оставшейся части главы, не имеет значения какие новые классы будут добавлены, метод ParseTrash.fillBin( ) будет продолжать работать без изменений, что говорит о хорошем дизайне.
В терминах создания объектов, этот дизайн, несомненно, строго локализовал изменения, которые вам нужно сделать для добавления новых типов в систему. Однако есть существенная проблема в использовании RTTI, которая явно показана здесь. Кажется, что программа работает нормально, но пока она никогда не обнаруживает картон, даже когда картон присутствует в списке! Это происходит по причине использования RTTI, при которой ищутся только те типы, которые вы сказали. Ключевым моментом отказа от RTTI будет то, что проверяется каждый тип в системе, а не единственный тип или подмножество типов. Как вы увидите позже, есть способ использования полиморфизма вместо проверки каждого типа. Но если вы используете RTTI в огромных количествах таким способом, и вы добавляете новый тип в вашу систему, вы можете легко забыть сделать необходимые изменения в вашей программе и воспроизвести трудный для обнаружения баг. Так что предпримем ценную попытку избавиться от RTTI в этом случае, не только по эстетической причине - при этом получится более легкий в уходе код.
Абстрагирование использования
С созданием мы разобрались, теперь пришло время заняться дизайном оставшейся части: где используются классы. Так как здесь происходит сортировка по корзинам, которая особенно громоздкая и бросающаяся в глаза, почему бы ни взять этот процесс и не спрятать его внутри класса? В этом заключен принцип "Если вы должны сделать что-то громоздкое, попробуйте локализовать громоздкость внутри класса". Это выглядит так:
Инициализация объекта TrashSorter теперь должна меняться при добавлении нового типа Мусора в модель. Вы должны представить, что класс TrashSorter должен выглядеть примерно так:
class TrashSorter extends ArrayList {
void sort(Trash t) { /* ... */
}
}
Таким образом, TrashSorter является списком (ArrayList) списка ссылок (ArrayList) Мусора, и с помощью add( ) вы можете установить еще один, например:
TrashSorter ts = new TrashSorter();
ts.add(new ArrayList());
Однако теперь сортировка становится проблемой. Как заставить статически закодированный метод работать притом, что могут быть добавлены новые типы? Чтобы решить эту проблему информация о типах должна быть удалена из метода sort( ), чтобы все, что нужно сделать, заключалось в вызове общего метода, который заботился бы о детализации по типу. В этом, конечно, заключается другой метод описания динамического построения метода. Таким образом sort( ) будет просто проходить по последовательности и вызывать динамически построенный метод для каждого списка (ArrayList). Так как задачей этого метода является сборка кусков мусора, в которых он заинтересован, он вызовет метод garb(Trash). Структура выглядит примерно так:
TrashSorter'у нужно вызывать каждый метод grab( ) и получать различные результаты в зависимости от типа Мусора, содержащегося в ArrayList. То есть, каждый список должен осознавать, какой тип он содержит. Классический подход к этой проблеме состоит в создании базового класса "Мусорная корзина" и наследовании от него новых классов для каждого типа, который вы хотите собирать. Если бы в Java был механизм параметризированных типов, это был бы наиболее подходящий способ. Нам не нужно было бы вручную кодировать все классы, этот механизм мог бы построить все за нас. Дальнейшее рассмотрение поможет нам выработать лучший подход.
Основной принцип дизайна ООП состоит в "Использовании членов-данных для различения состояний, использовании полиморфизма для различия в поведении". Первой мыслью, которая может возникнуть у вас, может быть та, что метод grab( ) конечно же ведет себя по-разному в зависимости того, содержится в списке (ArrayList) Бумага или Стекло. Но что этот метод делает, строго определяется типом и ничем другим. Это может быть интерпретировано, как различные состояния, а так как в Java существует класс для представления типа (Class), это можно использовать для определения типа Мусора, при помещении его в определенную корзину Tbin.
Конструктор для такого Tbin ожидает, что вы передадите ему Class по вашему выбору. Это сообщит списку, какой тип ему предназначено хранить. Затем метод grab( ) использует Class BinType и RTTI, чтобы проверить, что объект Мусора, который вы держите, соответствует типу, который поддерживает корзина.
Вот новая версия программы:
//: refactor:recycleb:RecycleB.java
// Контейнеры, которые собирают
объекты по интересам.
package refactor.recycleb;
import refactor.trash.*;
import java.util.*;
import junit.framework.*;
// Контейнер, который допускает
только правильный тип
// Мусора (устанавливается в конструкторе):
class Tbin {
private List list = new ArrayList();
private Class type;
public Tbin(Class binType) {
type = binType;
}
public boolean grab(Trash t) {
// Кроверяем
тип класса:
if (t.getClass().equals(type)) {
list.add(t);
return true; // Объект собран
}
return false; // Объект не собран
}
public Iterator iterator() {
return list.iterator();
}
}
class TbinList extends ArrayList {
void sortTrashItem(Trash t) {
Iterator e = iterator(); // Двигается по себе
while (e.hasNext())
if (((Tbin) e.next()).grab(t))
return;
// Тужна новая
корзина(Tbin) для этого типа:
add(new Tbin(t.getClass()));
sortTrashItem(t); // Рекурсивный вызов
}
}
public class RecycleB extends TestCase {
Collection bin = new ArrayList();
TbinList trashBins = new TbinList();
public RecycleB() {
ParseTrash.fillBin("../trash/Trash.dat", bin);
}
public void test() {
Iterator it = bin.iterator();
while (it.hasNext())
trashBins.sortTrashItem((Trash) it.next());
Iterator e = trashBins.iterator();
while (e.hasNext())
Trash.sumValue(((Tbin) e.next()).iterator());
Trash.sumValue(bin.iterator());
}
public static void main(String args[]) {
junit.textui.TestRunner.run(RecycleB.class);
}
} // /:~
Tbin содержит ссылку type на Class для того типа, который устанавливается в конструкторе и который должен собираться. Метод grab( ) сравнивает этот тип с типом того объекта, который вы передали. Обратите внимание, что в этом дизайне grab( ) принимает только объекты типа Trash, так что вы получите проверку базового типа во время компиляции, но вы также можете принимать просто Object и это также будет работать.
TTbinList содержит множество ссылок на Tbin, так что метод sort( ) может перебирать Tbin для поиска корзины, содержащей объекты Мусора. Если совпадение не найдено, то он создает новую корзину Tbin для того типа, который не был найден, и делает рекурсивный вызов самого себя - при следующем проходе корзина нового типа будет найдена.
Обратите внимание на общность этого кода: он не будет меняться совсем, если будет добавлен новый тип. Если большая часть вашего кода не нуждается в изменениях, когда вы добавляете новый тип (или при других изменениях), то вы получили легко расширяемую систему.
Множественная диспетчеризация (Multiple dispatching)
Приведенный выше дизайн определенно удовлетворительный. Добавление новых типов в систему заключается в добавлении или изменении отдельных классов, что не приведет к изменению кода, чтобы распространить новое на всю систему. Кроме того, нет "злоупотреблений" 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)
Теперь рассмотрим применения шаблона дизайна, который имеет полностью отличную цель по отношению к проблеме сортировки мусора.
Для этого шаблона мы более не будем заботиться об оптимизации при добавлении новых типов Мусора в систему. Вместо этого такой шаблон делает добавление новых типов Мусора более сложным. Предположение состоит в том, что у вас есть первичная иерархия классов, которая фиксирована. Возможно, она пришла к вам от другого поставщика, и вы не можете делать изменения в этой иерархии. Однако вам желательно добавлять новые полиморфические методы в эту иерархию, что означает, что обычно вам нужно что-то добавить в интерфейс базового класса. Таким образом, дилемма состоит в том, что вам нужно добавить методы в базовый класс, но вы не можете трогать базового класса. Как вам выкрутится в такой ситуации?
Дизайн шаблона, который решает проблемы такого рода, называется "визитер" (это последний дизайн шаблона в книге "Design Patterns"), и он строится на схеме двойной диспетчеризации, показанной в предыдущем разделе.
Шаблон визитера позволяет вам расширить интерфейс первичного типа путем создания отдельной иерархии классов типа Visiter для виртуализации операций, выполняющихся над первичными типами. Объекты первичного типа просто "принимают" визитера, затем вызывают методы динамического связывания визитера. Это выглядит следующим образом:
Теперь, если v является ссылкой типа Visitable для объекта типа Aluminum, получаем код:
PriceVisitor pv = new PriceVisitor();
v.accept(pv);
который использует двойную диспетчеризацию, являющуюся причиной двух вызовов полиморфических методов: первый из них выполняет выбор Алюминиевой версии метода accept( ), а второй выполняется внутри accept( ), когда динамически вызывается специфическая версия visit( ) при использовании ссылки v на базовый класс Visiter.
Эта конфигурация означает, что новая функциональность может быть добавлена в систему в форме нового подкласса Визитера. Иерархия Мусора остается неприкасаемой. Это главная выгода от применения шаблона визитера: вы можете добавлять новую полиморфную функциональность в иерархию классов не касаясь этой иерархии (так был добавлен метода accept( )). Обратите внимание, что выгода полезна в данном случае, но это не совсем то, с чего мы начали, так что в первом приближении вы можете решить, что это не то решение, которое мы ожидали.
Но взгляните на то, что мы сделали: решение на основе визитера предотвращает сортировку из главной последовательности Мусора в индивидуальные типизированные последовательности. Таким образом, вы можете оставить все в единой главной последовательности и просто пройти по последовательности, используя подходящего визитера для достижения цели. Хотя такое поведения кажется побочным эффектом применения визитера, это дает нам то, что мы хотели (отказ от RTTI).
Двойная диспетчеризация в шаблоне визитера заботится об определении обоих типов и Мусора, и типа Визитера. В следующем примере есть две реализации Ввизитера: PriceVisitor, который служит для вычисления и суммирования стоимости, и WeightVisitor для хранения информации о весе.
Вы можете видеть, что все это реализовано в новой, улучшенной версии программы переработки.
Так же как и в DoubleDispatch.java, класс Мусора остается один и создается новый интерфейс для добавления метода accept( ):
//: refactor:trashvisitor:Visitable.java
// Интерфейс для добавления функциональности
визитера
// в иерархию Мусора без изменения
// базового класса.
package refactor.trashvisitor;
import refactor.trash.*;
interface Visitable {
// Новый метод:
void accept(Visitor v);
} // /:~
Так как в базовом классе Visitor нет ничего конкретного, он может быть создан в качестве интерфейса:
//: refactor:trashvisitor:Visitor.java
// Базовый интерфейс для визитеров.
package refactor.trashvisitor;
import refactor.trash.*;
interface Visitor {
void visit(Aluminum a);
void visit(Paper p);
void visit(Glass g);
void visit(Cardboard c);
} // /:~
Рефлексивный Декоратор (A Reflective Decorator)
В этом мести вы можете следовать тому же самому подходу, который был использован для двойной диспетчеризации, и создать новые подтипы для Aluminum, Paper, Glass и Cardboard, которые реализуют метод accept( ). Для примера, новый VisitableAluminum будет выглядеть примерно так:
//: refactor:trashvisitor:VAluminum.java
// Применяем предыдущий подход для
создания
// специализинованного Aluminum
для шаблона визитера.
package refactor.trashvisitor;
import refactor.trash.*;
public class VAluminum extends Aluminum implements Visitable {
public VAluminum(double wt) {
super(wt);
}
public void accept(Visitor v) {
v.visit(this);
}
} // /:~
Однако скоро мы столкнемся со "взрывным ростом интерфейсов": основной Мусор, специальная версия для двойной диспетчеризации, и еще более специальная версия для визитера. Конечно, этот "взрывной рост интерфейсов" условен - при этом просто помещается дополнительные методы в класс Мусора. Если мы проигнорируем это, то мы получаем возможность использовать шаблон Декоратора: это выглядит так, как будто вы можете создать Декоратор, который может стать оберткой обычного объекта Мусора и произвести точно такой же интерфейс, как и у Мусора и добавить метод accept( ). Фактически, это отличный пример значимости Декоратора.
Однако двойная диспетчеризация создает проблему. Так как она полагается на перегрузку обоих методов accept( ) и visit( ), это может быть выполнено с помощью специализированного кода для каждой версии метода accept( ). С помощью шаблонов C++ это было бы достаточно легко выполнить (так как шаблоны автоматически генерируют специфичный для типа код), но в Java нет такого механизма - по крайней мере пока. Однако рефлексия позволяет вам определять информацию о типе во время выполнения, и это помогает решить многие проблемы, которые, на первый взгляд, требует шаблонов (хотя это не просто). Вот декоратор, который выполняет этот фокус [13]:
//: refactor:trashvisitor:VisitableDecorator.java
// Декоратор, который адаптирует
общий класс Мусора
// к шаблону визитера.
// [ Использовать здесь Динамический
Прокси?? ]
package refactor.trashvisitor;
import refactor.trash.*;
import java.lang.reflect.*;
public class VisitableDecorator extends Trash implements Visitable {
private Trash delegate;
private Method dispatch;
public VisitableDecorator(Trash t) {
delegate = t;
try {
dispatch = Visitor.class.getMethod("visit", new Class[] { t
.getClass() });
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
public double getValue() {
return delegate.getValue();
}
public double getWeight() {
return delegate.getWeight();
}
public void accept(Visitor v) {
try {
dispatch.invoke(v, new Object[] { delegate });
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
} // /:~
[[Описание использование Рефлексии]]
[[Обратите внимание, что здесь также можно применить Динамический Прокси.]]
Единственно, нам нужен новый тип адаптера Fillable, который автоматический декорирует объекты при создании их из оригинального файла Trash.dat. Это может быть выполнено непосредственно, декорируя любой Fillable:
//: refactor:trashvisitor:FillableVisitor.java
// Adapter Decorator that adds the visitable
// decorator as the Trash objects are
// being created.
package refactor.trashvisitor;
import refactor.trash.*;
import java.util.*;
public class FillableVisitor implements Fillable {
private Fillable f;
public FillableVisitor(Fillable ff) {
f = ff;
}
public void addTrash(Trash t) {
f.addTrash(new VisitableDecorator(t));
}
} // /:~
Теперь вы можете использовать его, как обертку для любого существующего Fillable, или для любого нового типа, который будет создан.
Оставшаяся часть программы создает специфический тип Visitor и посылает его через единый список объектов Мусора:
//: refactor:trashvisitor:TrashVisitor.java
// The "visitor" pattern with VisitableDecorators.
package refactor.trashvisitor;
import refactor.trash.*;
import java.util.*;
import junit.framework.*;
// Specific group of algorithms packaged
// in each implementation of Visitor:
class PriceVisitor implements Visitor {
private double alSum; // Aluminum
private double pSum; // Paper
private double gSum; // Glass
private double cSum; // Cardboard
public void visit(Aluminum al) {
double v = al.getWeight() * al.getValue();
System.out.println("value of Aluminum= " + v);
alSum += v;
}
public void visit(Paper p) {
double v = p.getWeight() * p.getValue();
System.out.println("value of Paper= " + v);
pSum += v;
}
public void visit(Glass g) {
double v = g.getWeight() * g.getValue();
System.out.println("value of Glass= " + v);
gSum += v;
}
public void visit(Cardboard c) {
double v = c.getWeight() * c.getValue();
System.out.println("value of Cardboard = " + v);
cSum += v;
}
void total() {
System.out.println("Total Aluminum: $" + alSum + "n Total Paper: $"
+ pSum + "nTotal Glass: $" + gSum + "nTotal Cardboard: $"
+ cSum + "nTotal: $" + (alSum + pSum + gSum + cSum));
}
}
class WeightVisitor implements Visitor {
private double alSum; // Aluminum
private double pSum; // Paper
private double gSum; // Glass
private double cSum; // Cardboard
public void visit(Aluminum al) {
alSum += al.getWeight();
System.out.println("weight of Aluminum = " + al.getWeight());
}
public void visit(Paper p) {
pSum += p.getWeight();
System.out.println("weight of Paper = " + p.getWeight());
}
public void visit(Glass g) {
gSum += g.getWeight();
System.out.println("weight of Glass = " + g.getWeight());
}
public void visit(Cardboard c) {
cSum += c.getWeight();
System.out.println("weight of Cardboard = " + c.getWeight());
}
void total() {
System.out.println("Total weight Aluminum: " + alSum
+ "nTotal weight Paper: " + pSum + "nTotal weight Glass: "
+ gSum + "nTotal weight Cardboard: " + cSum
+ "nTotal weight: " + (alSum + pSum + gSum + cSum));
}
}
public class TrashVisitor extends TestCase {
Collection bin = new ArrayList();
PriceVisitor pv = new PriceVisitor();
WeightVisitor wv = new WeightVisitor();
public TrashVisitor() {
ParseTrash.fillBin("../trash/Trash.dat", new FillableVisitor(
new FillableCollection(bin)));
}
public void test() {
Iterator it = bin.iterator();
while (it.hasNext()) {
Visitable v = (Visitable) it.next();
v.accept(pv);
v.accept(wv);
}
pv.total();
wv.total();
}
public static void main(String args[]) {
junit.textui.TestRunner.run(TrashVisitor.class);
}
} // /:~
В методе Test( ) обратите внимание на то, как визитерство добавлено простым созданием другого типа корзины при использовании декоратора. Также обратите внимание, что адаптер FillableCollection имеет внешний вид декоратора (для ArrayList) в данной ситуации. Однако, он полностью меняет интерфейс ArrayList, в то время, когда назначение Декоратора состоит в том, чтобы интерфейс декорируемого класса оставался тем же самым после декорации.
Обратите внимание, что образ клиентского кода (показанный в классе Test) опять изменился, по сравнению с оригинальным подходом к проблеме. Теперь здесь есть только одна корзина Мусора. Два объекта Visitor получают доступ к каждому элементу последовательности, а затем выполняют операции. Визитеры содержат свои собственные внутренние данные для подсчета полной стоимости и веса.
И наконец, здесь нет определения типов во время выполнения отличного от неизбежного приведения к типу Trash при вытягивании вещей из последовательности. Это, опять таки, может быть устранено с помощью реализации параметризованных типов в Java.
Один из способов, которым вы можете различить такое решения от решения двойной диспетчеризации, описанного ранее, состоит в том, что двойная диспетчеризация использует только один перегруженный метод add( ), который перегружается при создании каждого подкласса, в то время, как в данном методе каждый экземпляр перегруженного метода visit( ) метод перегружается в каждом подклассе Visitor.
Больше связности?
Здесь есть большое количество кода и здесь есть определенная связь между иерархией Мусора и иерархией Визитера. Однако, здесь также спрятана связь внутри соответствующих наборов классов: они все делают только одно (Мусор описывает мусор, в то время, как Визитер описывает операции, совершаемые с Мусором), что является индикатором хорошего дизайна. Конечно, в этом случае это работает хорошо, только если вы добавляете новых Визитеров, но куда заведет нас путь, когда мы добавим новые типы Мусора.
Низкая связанность между классами и высокая связанность внутри классов является определенно важной целью дизайна. Применение бессмысленно, хотя, это может предотвратить достижение более элегантного дизайна. Это выглядит так, как будто некоторые классы неизбежно имеют определенную тесную связь с другими классами. Это часто происходит в парах, которые можно назвать куплетами. Например, контейнеры и итераторы. Пара Мусор-Визитер в приведенном выше примере является другим примером такого куплета.
RTTI относительно полезно?
Различные дизайны этой главы позволяли удалить RTTI, который может произвести впечатления, что оно "относительно полезно" (возражения, используемые для плохого, зловредного goto, который не был внесен в Java). Это не так. Злоупотребление RTTI является проблемой. Причина, по которой мы удалили RTTI из нашего дизайна, состоит в том, что злоупотребление этой возможностью делает невозможным расширение, а основной целью было предоставление возможности добавление новых типов в систему с минимальным изменением окружающего кода. Так как люди часто злоупотребляют RTTI, рассматривая каждый тип в вашей системе, это является причиной не расширяемости кода: когда вы добавляете новый тип, вы начинаете охотиться за всеми местами в коде, где используется RTTI, и если вы пропустите что-то, вы не получите помощь от компилятора.
Однако использование RTTI не означает, что автоматически получается не расширяемый код. Давайте взглянем еще раз на переработчик мусора. В этот раз будет введен новый инструмент, который я назвал TypeMap. Он содержит HashMap, которая хранит списки ArrayList, но имеет простой интерфейс: вы можете добавить (add( )) новый объект и вы можете получить (get( )) список (ArrayList), содержащий все объекты определенного типа. Ключами для HashMap являются типы из ассоциированного списка (ArrayList). Красота этого дизайна состоит в том, что TypeMap динамически добавляет новые пары в любое время, когда он обнаруживает новые типы, так что когда бы вы ни добавили новый тип в систему (даже если добавление происходит во время выполнения), он адаптируется.
Наш пример опять таки будет построен на структуре типов Мусора из пакета refractor.Trash (также используется файл Trash.dat без изменений):
//: refactor:dynatrash:DynaTrash.java
// Использование Карты Списков и
RTTI для автоматической сортировки
// мусора в ArrayLists. Решение,
несмотря на использование RTTI,
// является гибким.
package refactor.dynatrash;
import refactor.trash.*;
import java.util.*;
import junit.framework.*;
// Основные типы TypeMap работают
в любой ситуации:
class TypeMap {
private Map t = new HashMap();
public void add(Object o) {
Class type = o.getClass();
if (t.containsKey(type))
((List) t.get(type)).add(o);
else {
List v = new ArrayList();
v.add(o);
t.put(type, v);
}
}
public List get(Class type) {
return (List) t.get(type);
}
public Iterator keys() {
return t.keySet().iterator();
}
}
// Класс адаптера для обратного
вызова из
// ParseTrash.fillBin():
class TypeMapAdapter implements Fillable {
TypeMap map;
public TypeMapAdapter(TypeMap tm) {
map = tm;
}
public void addTrash(Trash t) {
map.add(t);
}
}
public class DynaTrash extends TestCase {
TypeMap bin = new TypeMap();
public DynaTrash() {
ParseTrash.fillBin("../trash/Trash.dat", new TypeMapAdapter(bin));
}
public void test() {
Iterator keys = bin.keys();
while (keys.hasNext())
Trash.sumValue(bin.get((Class) keys.next()).iterator());
}
public static void main(String args[]) {
junit.textui.TestRunner.run(DynaTrash.class);
}
} // /:~
Несмотря на силу, определение TypeMap достаточно просто. Здесь содержится HashMap и метода add( ), который делает основную работу. Когда вы добавляете новый объект, получается ссылка на объект типа Class для этого типа. Она используется в качестве ключа для определения содержит ли какой-либо из ArrayList объекты этого типа в HashMap. Если это так, то мы получаем соответствующий ArrayList и добавляем в него объект. Если нет, объект Class и новый ArrayList добавляются в качестве пары ключ-значение.
Вы можете получить Итератор для всех объектов типа Class из keys( ) и использовать каждый объект Class для получения соответствующего списка (ArrayList) с помощью get( ). Вот и все, что здесь сделано.
Метод filler( ) интересен, поскольку он развивает дизайн ParseTrash.fillBin( ), в результате чего метод не просто заполняет список (ArrayList), а вместо этого он реализует интерфейс Fillable с его методом addTrash( ). Все, что нужно сделать заполнителю filler( ), это вернуть ссылку на интерфейс, который реализует Fillable, а затем эта ссылка может быть использована в качестве аргумента метода fillBin( ) следующим образом:
ParseTrash.fillBin("Trash.dat", bin.filler());
Чтобы воспроизвести такую ссылку используется анонимный внутренний класс
(описано в главе
8 Thinking in Java,
2nd edition). Вам никогда не понадобится именованный класс,
чтобы реализовать интерфейс Fillable, вам просто нужна ссылка
на объект этого класса, так что это подходящее использование анонимного внутреннего
класса.
Интересным в этом дизайне является то, что хотя в задумках он создавался так, чтобы не заботится о сортировке, fillBin( ) выполняет сортировку при каждой вставке объекта Мусора в корзину.
Многое из класса DynaTrash может походить на предыдущий пример. В то же время, вместо помещения нового объекта Мусора в корзину типа списка (ArrayList) корзина является типа TypeMap, так что когда мусора помещен в корзину, он моментально сортируется внутренним механизмом TypeMap. Перебор TypeMap и операции над каждым индивидуальным списком (ArrayList) происходят аналогичным образом.
Как вы можете видеть, добавление нового типа в систему совсем не повлияет на этот код, а код в TypeMap полностью независим. Это, конечно, небольшое решение проблемы, и, бесспорно, более элегантное. Оно полностью завязано на RTTI, но обратите внимание, что каждая пара ключ-значение в HashMap проверяется только на один тип. Кроме того, здесь нет возможности "забыть" добавить правильный код в систему, когда вы добавляете новый тип, так как здесь нет какого-либо кода, который нужно было бы добавлять.
Заключение
Знакомство с такими дизайнами, как TrashVisitor.java, который содержит большое количество кода, по сравнению с ранними дизайнами, может показаться, что они контрпродуктивны. Это плата за то, чтобы заметить, что вы пробуете совершить с различными вариантами дизайна. Шаблоны дизайна в основном стараются отделить вещи, которые меняются от вещей, которые остаются теми же самыми. "Вещи, которые меняются" могут соотноситься со многими другими видами изменений. Возможно, изменения происходят по причине помещения программы в новое окружения или потому, что что-то в текущем окружении поменялось (это может быть: "Пользователь хочет добавить новый образ в диаграмму, находящуюся на экране"). Или, как в этом случае, TrashVisitor.java позволяет вам совершать эволюцию тела кода. Пока предыдущие версии примеров сортировки мусора акцентировали внимание на добавлении новых типов Мусора в систему, TrashVisitor.java позволил вам облегчить добавление функциональности без разрушения иерархии Мусора. В TrashVisitor.java есть гораздо больше кода, но добавление новой функциональности в Visitor не сложно. Если это происходит очень часто, то с этой сточки зрения дополнительный объем работ и дополнительный код сделает такую работу легче.
Исследование вектора изменений - это не тривиальная задача. Нет ничего, что обнаруживает анализ прежде, чем программа будет просмотрена в своем начальном дизайне. Необходимая информация, вероятно, не появится, пока вы не перейдете к следующим фазам проекта: иногда только в фазе дизайна или реализации вы обнаруживаете глубинные или едва различимые необходимости в вашей системе. В случае добавления новых типов (на чем были сфокусированы большинство "перерабатывающих" примеров) вы можете понять то, что вам нужна обычная иерархия наследования только когда вы перейдете к фазе ухода и начнете расширять систему!
Одна из наиболее важных вещей, которую вы выучите, изучая шаблоны дизайна, покажется поворотом от того, что было выдвинуто в этой книге на передний план. То есть: "ООП - это все, что относится к полиморфизму". Это высказывание может привести к синдрому "молотка двухлетней давности" (все выглядит, как гвозди). Другой путь состоит в достаточно тяжелом "получении" полиморфизма, и как только вы сделаете это, вы попробуете привести все ваши дизайны к одному определенному шаблону.
Шаблоны дизайнов говорят, что ООП это не все, что относится к полиморфизму. Это "отделение вещей, которые меняются, от вещей, которые остаются теми же самыми". Полиморфизма особенной важный путь, чтобы сделать это, и он приносит пользу, если язык программирования напрямую поддерживает полиморфизм (так что вам не нужно изабретать его самостоятельно, что может стать чрезвычайно дорогим занятием). Но шабоны дизайна в своей основе показывают другие пути достижения основной цели, и если ваши глаза открыты, вы начнете искать более креативные дизайны.
Так как появление книги "Design Patterns" послужило толчком, люди стали искать шаблоны. Вы можете ожидать появление все большего числа шаблонов с течением времени. Вот некоторые сайты, рекомендованные Джимом Коуплиеном (Jim Coplien), известного в C++ (http://www.bell-labs.com/~cope), являющегося главным пропагандистом продвижения шаблонов:
http://st-www.cs.uiuc.edu/users/patterns
http://c2.com/cgi/wiki
http://c2.com/ppr
http://www.bell-labs.com/people/cope/Patterns/Process/index.html
http://www.bell-labs.com/cgi-user/OrgPatterns/OrgPatterns
http://st-www.cs.uiuc.edu/cgi-bin/wikic/wikic
http://www.cs.wustl.edu/~schmidt/patterns.html
http://www.espinc.com/patterns/overview.html
Также обратите внимание на ежегодную конференцию по шаблонам дизайна, называемую PLOP, которая имеет публичную практику, треть которой идет позже 1997 (все опубликовано Addison-Wesley).
Упражнения
- Добавьте класс Plastic вTrashVisitor.java.
- Добавьте класс Plastic в DynaTrash.java.
- Создайте декоратор, наподобие VisitableDecorator, но для примера множественной диспетчеризации, аналогично с классом "адаптерного декоратора", который был создан для VisitableDecorator. Постройте оставшиеся примеры и покажите, как они работают.
← | Сложные состояния системы | Проекты | → |