Шаблон прототипного создания


Проблема приведенного выше дизайна состоит в том, что в нем все еще требуется центральное расположение, при котором все типы объектов должны быть известны: внутри метода 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 в этом случае, не только по эстетической причине - при этом получится более легкий в уходе код.