1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/wizardforcel-thinking-in-java-zh

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
В этом репозитории не указан файл с открытой лицензией (LICENSE). При использовании обратитесь к конкретному описанию проекта и его зависимостям в коде.
Клонировать/Скачать
16.4 改进设计.md 40 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
Отправлено 11.03.2025 09:15 d56454c

16.4 Улучшение дизайна

Концепция всех решений в книге "Design Patterns" строится вокруг вопроса "Что будет меняться при эволюции программы?". Для любого дизайна это может быть самым важным вопросом. Ответив на этот вопрос, можно создать систему, которая будет легче поддерживать (и дешевле) и будет генерировать объекты, которые могут повторно использоваться, что делает конструирование связанных систем также дешевле. Это преимущество объектно-ориентированной программирования, но оно не проявляется автоматически. Оно требует глубокого понимания проблемы, которую мы пытаемся решить. В этом разделе мы покажем вам, как это сделать, шаг за шагом улучшая нашу систему.

Для текущей системы сбора отходов ответ на вопрос "что будет меняться?" очень простым является: будут добавлены новые типы. Поэтому цель дизайна заключается в том, чтобы максимально упростить добавление новых типов. В нашей системе сбора отходов мы собираемся упаковать все места, где используется информация о конкретных типах. Таким образом (не говоря уже о других причинах), любые изменения будут локализованы внутри этих упаковок. Такой подход также делает остальную часть кода особенно чистой.

16.4.1 "Создание большего количества объектов"Это приводит нас к одной из обычных рекомендаций при объектно-ориентированном программировании, которую я услышал впервые от Градди Буччи: "Если дизайн слишком сложен, создайте больше объектов". Хотя это может звучать немного двусмысленно и просто до смеха, это действительно одна из самых полезных рекомендаций, которые я знаю (вы заметите, что "создание большего количества объектов" часто эквивалентно "добавлению еще одного уровня абстракции"). Обычно, если вы видите место, которое заполнено большим количиством сложного кода, вам следует рассмотреть возможность создания нового класса, который сделает его более чистым. Такое организование системы обычно приводит к лучшей структуре и более гибкому коду.Рассмотрим сначала создание объекта Trash, которое происходит в первом месте, где он создаётся — это switch statement в методе main():

for (int i = 0; i < 30; i++) {
  switch ((int) (Math.random() * 3)) {
    case 0:
      bin.addElement(new Aluminum(Math.random() * 100));
      break;
    case 1:
      bin.addElement(new Paper(Math.random() * 100));
      break;
    case 2:
      bin.addElement(new Glass(Math.random() * 100));
  }
}

Этот код явно "слишком сложен" и является местом, где при добавлении нового типа требуется изменение кода. Если часто добавляются новые типы, то лучше всего создать независимый метод, который будет получать все необходимые данные и создавать ссылку на объект правильного типа, уже преобразованного в объект типа Trash. В книге "Design Patterns" это называется грубым образом как "паттерн создания". Конкретный специализированный паттерн, применяемый здесь, представляет собой вариант метода Factory. Здесь метод Factory является статическим членом класса Trash, но более распространенным подходом является его реализация как переопределённый метод в производном классе.Основной идеей метода Factory является передача информации, необходимой для создания объекта, а затем возврат ссылки (уже преобразованной до базового типа) в виде значения. После этого объект можно рассматривать с точки зрения полиморфизма. Таким образом, нет необходимости знать точный тип созданного объекта. На самом деле, метод Factory скрывается таким образом, что его невозможно заметить, что предотвращает случайное неверное использование. Если требуется использовать объект без применения полиморфизма, необходимо явно использовать RTTI и указать преобразование.Однако существует ещё одна маленькая проблема, особенно когда используется более сложный метод в базовом классе (не такой, как тот, который показан здесь), и этот метод переопределяется (перегружается) в производном классе. Если информация, запрошенная в производном классе, требует больше или других параметров, что делать? "Создание большего количества объектов" решает эту проблему. Для реализации метода Factory, класс Trash использует новый метод, названный factory. Чтобы скрыть информацию о создании данных, мы используем новый класс Info, содержащий всю информацию, необходимую для создания соответствующего объекта типа Trash методом factory(). Вот простое выполнение класса Info:

```java
class Info {
  int type;
  // Нужно изменить это, чтобы добавить другой тип:
  static final int MAX_NUM = 4;
  double data;
  Info(int typeNum, double dat) {
    type = typeNum % MAX_NUM;
    data = dat;
  }
}

Объект класса `Info` выполняет единственную задачу — хранить информацию для метода `factory()`. Теперь, если метод `factory()` нуждается в большем объёме или других типах информации для создания объекта типа `Trash`, нет необходимости менять сам метод `factory()`. Добавив новые данные и конструкторы, можно модифицировать класс `Info` или использовать подклассы для более типичной формы объектно-ориентированного программирования.
Для этого простого примера метод `factory()` выглядит следующим образом:```  
static Мусор завода(Информация i) {
    переключатель(i.type) {
      по умолчанию: // чтобы успокоить компилятор
      случай 0:
        возвращаемый новый Алюминий(i.data);
      случай 1:
        возвращаемый новый Бумага(i.data);
      случай 2:
        возвращаемый новый Стекло(i.data);
      // Добавьте две строки здесь:
      случай 3:
        возвращаемый новый Коробка(i.data);
    }
}

Здесь точный тип объекта легко можно определить. Но мы можем представить себе более сложные ситуации, где метод factory() будет использовать сложный алгоритм. В любом случае, ключевой момент заключается в том, что этот метод скрыт где-то внутри системы, и мы знаем, куда обращаться при добавлении новых типов.

Создание нового объекта в методе main() теперь становится очень простым и чистым:

для(целое i = 0; i < 30; i++) 
бин.addElement(
Trash.factory(
новый Информация(
(целое)(Мат.random() * Информация.MAX_NUM),
Мат.random() * 100)));

Здесь мы создаем объект Info, который передает данные в метод factory(). Последний создает какой-либо объект типа Trash в куче памяти и возвращает ссылку, которая добавляется внутрь Vector bin. Конечно, если меняются количество и типы аргументов, все равно потребуется изменение этой строки. Однако, если создание объекта Info происходит автоматически, это может избавить от необходимости таких изменений. Например, можно передать Vector с параметрами конструктору объекта Info (или непосредственно в вызов метода factory()). Это требует анализа и проверки параметров во время выполнения, но обеспечивает высокую гибкость.Из этого кода видно, как "ведущие изменения" решаются с помощью шаблона Factory: если в систему добавлен новый тип (произошло изменение), единственным местом, которое требуется изменить, является внутри самого Factory, поэтому Factory изолирует влияние этих изменений.

16.4.2 Шаблон для создания прототиповОдним из недостатков вышеописанной модели является необходимость наличия центрального места, где должны быть известны все типы объектов: внутри метода factory(). Если часто добавляются новые типы, каждый раз придётся изменять метод factory() для каждого нового типа. Если эта проблема действительно вас беспокоит, попробуйте углубиться и переместить всю информацию, связанную с типами — включая процесс их создания — внутрь класса, представляющего данный тип. Таким образом, при каждом добавлении нового типа единственной вещью, которую вам нужно сделать, будет наследование от одного класса. Для перемещения информации о создании типов в специальный контейнер типа Trash следует использовать паттерн "Прототип" (prototype). Этот подход описан в книге "Design Patterns". Основная идея заключается в том, что мы имеем последовательность контролирующих объектов, каждый из которых представляет собой прототип для определённого типа. Объекты этой последовательности используются только для создания новых объектов, используя механизм, аналогичный методу clone(), встроенному в корневой класс Java Object. В данном случае метод клонирования назван tClone(). При подготовке к созданию нового объекта собирается информация некоторого типа, которая затем используется для создания желаемого типа объекта.Затем производится проход по основной последовательности, где информация сравнивается с информацией внутри прототипов. Если найден подходящий прототип, он клонируется.Используя этот подход, нам не требуется жёстко закодировать информацию о создании объектов. Каждый объект знает, как раскрыть необходимую информацию и как выполнить клонирование себя. Поэтому при добавлении нового типа в систему метод factory() не требует никаких изменений.

Чтобы решить проблему создания прототипов, можно было бы добавить множество методов для поддержки создания новых объектов. Однако в Java 1.1 наличие ссылки на объект типа Class уже предоставляет возможность создания новых объектов. Используя технологию "рефлексии", доступную в Java 1.1 (описанной в главе 11), даже если у нас есть только ссылка на объект типа Class, мы можем вызвать конструктор. Это является идеальным решением проблемы прототипов.

Список прототипов будет представлен списком ссылок на объекты типа Class, которые хотят создать. Кроме того, если процесс создания прототипа завершился ошибкой, метод factory() попытается загрузить отсутствующий Class объект. Таким образом, динамическая загрузка прототипов позволяет классу Trash не знать заранее, какие именно типы он будет манипулировать. Соответственно, при добавлении новых типов не требуется никаких изменений. Таким образом, мы можем легко повторно использовать его в остальной части данной главы.```java //: Trash.java // Базовый класс для примеров использования контейнера Trash package c16.trash; import java.util.; import java.lang.reflect.;


```java
public abstract class Trash {
    private double weight;

    public Trash(double wt) {
        weight = wt;
    }

    public Trash() {}

    public abstract double value();

    public double вес() {
        return weight;
    }

    // Суммирует ценность отходов в корзине:
    public static void сумма_значения(Vector корзина) {
        Enumeration e = корзина.elements();
        double val = 0.0f;
        while (e.hasMoreElements()) {
            // Один вид RTTI:
            // Динамически проверяемое преобразование типа:
            Trash t = (Trash) e.nextElement();
            val += t.weight() * t.value();
            System.out.println(
                "веса " +
                // Используем RTTI для получения информации о типе класса:
                t.getClass().getName() +
                " = " + t.weight());
        }
        System.out.println("Общая стоимость = " + val);
    }

    // Остаток класса предоставляет поддержку для прототипирования:
    public static class PrototypeNotFoundException extends Exception {}
    
    public static class CannotCreateTrashException extends Exception {}

    private static Vector trashTypes = new Vector();

    public static Trash фабрика(Info информация)
            throws PrototypeNotFoundException,
                   CannotCreateTrashException {
        for (int i = 0; i < trashTypes.size(); i++) {
            // Каким-то образом определяем новый тип для создания и создаем его:
            Class tc =
                    (Class) trashTypes.elementAt(i);
            if (tc.getName().indexOf(информация.id) != -1) {
                try {
``````java
          // Получаем динамический конструктор метод, который принимает аргумент типа double:
           Конструктор ctor =
             tc.конструктор(new Класс[]{Double.класс()});
           // Вызываем конструктор для создания нового объекта:
           вернуть (Trash)ctor.новый_объект(
             новые Объект[]{новый Double(информация.data)});
         } захват (Исключение ex) {
           ex.printStackTrace();
           бросить новое CannotCreateTrashException();
         }
       }
     }
     // Класс не был найден в списке. Попытаемся загрузить его, но он должен находиться в вашем пути классов!
     попробовать {
       вывод.println("Загружено " + информация.id);
       trashTypes.добавить(
         Класс.по_названию(информация.id));
     } захват (Исключение e) {
       e.printStackTrace();
       бросить новое PrototypeNotFoundException();
     }
     // Успешная загрузка. Рекурсивный вызов должен работать теперь:
     вернуть фабрика(информация);
   }
   публичный статический класс Info {
     публичное String id;
     публичное double data;
     публичный Info(String имя, double данные) {
       id = имя;
       this.данные = данные;
     }
   }
 }
 ///:~

```Основной класс Trash и метод `sumValue()` остаются такими же, как обычно. Оставшаяся часть этого класса поддерживает паттерн прототипа. Вначале вы видите два вложенных класса (установленных как статические свойства, чтобы они служили только целями организации кода), которые описывают возможные исключения. За ними следует вектор `Vector trashTypes`, который используется для хранения ссылок на классы.В методе `Trash.factory()`, объект `Info id` (ещё один вариант класса `Info`, отличающийся от ранее обсуждаемого) содержит строку, которая представляет тип создаваемого объекта `Trash`. Эта строка сравнивается с именами классов в списке. Если найдено совпадение, то это будет тип создаваемого объекта. Конечно, существует множество способов определения того, какой именно объект мы хотим создать. Этот подход выбран потому, что информация, считанная из файла, может быть преобразована в объект.

После того как определён тип Trash (мусора), следующий шаг — использование механизма рефлексии. Метод getConstructor() требует получения своих аргументов — массива ссылок на классы. Этот массив представляет различные аргументы и располагает их в правильном порядке для вызова конструктора. Здесь этот массив создаётся динамически с использованием синтаксиса создания массивов Java Yöntemi 1.1:

new Class[] { double.class }

Этот код предполагает, что все типы Trash имеют конструктор, принимающий значение типа double (обратите внимание, что double.class отличается от Double.class). Для более гибкого решения можно использовать метод getConstructors(), который вернёт массив доступных конструкторов.


Исправлено:

  • "Yöntemi 1.1" заменено на "Java 1.1".
  • Удалены лишние пробелы после некоторых пунктуационных знаков.
  • Корректировка согласования времен глаголов.
  • Улучшение согласования родовых слов.Массив, возвращённый методом getConstructors(), представляет собой ссылку на объект Constructor (часть библиотеки java.lang.reflect). Мы используем метод newInstance() для динамического вызова конструктора. Этот метод требует передачи массива объектов, содержащего фактические аргументы. Этот массив также создаётся с использованием синтаксиса создания массивов Java 1.1:```java new Object[] { new Double(info.data) }

В данном случае число типа `double` должно быть помещено внутри контейнерного класса, чтобы стать частью массива объектов. Вызов метода `newInstance()` извлекает значение `double`, но может показаться сложным вопрос о том, что параметры могут быть либо `double`, либо `Double`, но при вызове должны передаваться как `Double`. К счастью, эта проблема актуальна только для примитивных данных. Понимание конкретного процесса позволяет создать новый объект, предоставив ему лишь одну ссылку на `Класс`. В данном случае всё становится очень простым. В текущей ситуации вложенный цикл никогда не выполнит команду `return`, поэтому мы выходим из него в конце. Здесь программа динамически загружает объект `Класс` и добавляет его в список `trashTypes` (тип мусора), пытаясь исправить эту проблему. Если истинная причина проблемы так и не найдена, но загрузка прошла успешно, метод `factory` повторно вызывается для попытки решения задачи заново.

Как вы можете видеть, главным преимуществом данного паттерна проектирования является то, что нет необходимости вносить изменения в код. Он работает корректно в любых условиях (при условии, что все подклассы `Trash` содержат конструктор, который принимает один параметр типа `double`).

(1) Подклассы `Trash`Чтобы адаптироваться к механизму прототипирования, единственным требованием для каждого нового подкласса `Trash` является наличие конструктора, который принимает один параметр типа `double`. Механизм "рефлексии" Java версии 1.1 выполняет остальную работу.Ниже приведены различные типы `Trash`, каждый из которых представлен отдельным файлом и является частью пакета `Trash` (как и прежде, для удобства использования в этой главе):

```java
//: Aluminum.java
// Класс Aluminum с использованием прототипирования
package c16.trash;

public class Aluminum extends Trash {
  private static double val = 1.67;
  public Aluminum(double wt) { super(wt); }
  public double value() { return val; }
  public static void setValue(double newVal) {
    val = newVal;
  }
} ///:~

Вот ещё один новый тип Trash:

//: Cardboard.java
// Класс Cardboard с использованием прототипирования
package c16.trash;

public class Cardboard extends Trash {
  private static double val = 0.23;
  public Cardboard(double wt) { super(wt); }
  public double value() { return val; }
  public static void setValue(double newVal) {
    val = newVal;
  }
} ///:~

Как можно заметить, эти классы ничем особым не отличаются, кроме конструктора.

(2) Парсинг Trash из внешнего файла

Информация, связанная с объектами Trash, будет считываться из внешнего файла. Для каждого аспекта Trash в файле указаны все необходимые данные — каждая строка представляет собой один аспект и имеет фиксированную форму тип_отходов:значение. Например:

c16.Trash.Glass:54
c16.Trash.Paper:22
c16.Trash.Paper:11
c16.Trash.Glass:17
c16.Trash.Aluminum:89
c16.Trash.Paper:88
c16.Trash.Aluminum:76
c16.Trash.Cardboard:96
c16.Trash.Aluminum:25
c16.Trash.Aluminum:34
c16.Trash.Glass:11
c16.Trash.Glass:68
c16.Trash.Glass:43
c16.Trash.Aluminum:27
c16.Trash.Cardboard:44
c16.Trash.Aluminum:18
c16.Trash.Paper:91
c16.Trash.Glass:63
c16.Trash.Glass:50
c16.Trash.Glass:80
c16.Trash.Aluminum:81
c16.Trash.Cardboard:12
c16.Trash.Glass:12
c16.Trash.Glass:54
c16.Trash.Aluminum:36
c16.Trash.Aluminum:93
c16.Trash.Glass:93
c16.Trash.Paper:80
c16.Trash.Glass:36
c16.Trash.Glass:12
c16.Trash.Glass:60
c16.Trash.Paper:66
c16.Trash.Aluminum:36
c16.Trash.Cardboard:22
```Обратите внимание, что при указании имени класса путь к нему также должен быть указан, в противном случае класс может не быть найден.

Для его парсинга каждая строка считывается, а метод `indexOf()` используется для создания индекса `:`. Сначала с помощью метода `substring()` выделяется имя типа мусора, затем с использованием статического метода `Double.valueOf()` получается значение, которое преобразуется в тип `double`. Метод `trim()` используется для удаления лишних пробелов с обоих концов строки.

Парсер `Trash` помещён в отдельный файл, так как он будет использоваться в течение всей главы. Вот пример:

```java
//: ParseTrash.java
// Открывает файл и парсит его содержимое в объекты класса Trash, каждый из которых добавляется в вектор
package c16.trash;
import java.util.*;
import java.io.*;

public class ParseTrash {
  public static void
  fillBin(String filename, Fillable bin) {
    try {
      BufferedReader data =
        new BufferedReader(
          new FileReader(filename));
      String buf;
      while ((buf = data.readLine()) != null) {
        String type = buf.substring(0, buf.indexOf(':')).trim();
        double weight = Double.valueOf(
          buf.substring(buf.indexOf(':') + 1).trim()).doubleValue();
        bin.addTrash(
          Trash.factory(new Trash.Info(type, weight)));
      }
      data.close();
    } catch (IOException e) {
      e.printStackTrace();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  // Специальный случай для работы с векторами:
  public static void
  fillBin(String filename, Vector bin) {
    fillBin(filename, new FillableVector(bin));
  }
} ///:~
```В `RecycleA.java` мы используем вектор для хранения объектов класса `Trash`. Однако можно рассмотреть использование других типов коллекций. Для этого первый вариант метода `fillBin()` принимает ссылку на объект типа `Fillable`, который представляет собой интерфейс, поддерживающий метод `addTrash()`.```java
//: Fillable.java
// Любой объект, который может быть заполнен объектами класса Trash
package c16.trash;

public interface Fillable {
  void addTrash(Trash t);
} ///:~

Любой объект, реализующий этот интерфейс, может быть использован вместе с fillBin(). Конечно, Vector сам по себе не реализует интерфейс Fillable, поэтому он не работает. Поскольку Vector будет использоваться во многих примерах, лучшим решением является добавление еще одного перегруженного метода fillBin(), который принимает в качестве параметра вектор. Это достигается с помощью адаптерного класса FillableVector, который позволяет использовать вектор как объект типа Fillable:

package c16.trash;

public class FillableVector implements Fillable {
  private final Vector vector;

  public FillableVector(Vector v) {
    this.vector = v;
  }

  @Override
  public void addTrash(Trash t) {
    vector.addElement(t);
  }
}
//: FillableVector.java
// Адаптер, который делает Vector заполняемым
package c16.trash;
import java.util.*;

public class FillableVector implements Fillable {
  private Vector v;
  public FillableVector(Vector vv) { v = vv; }
  public void addTrash(Trash t) {
    v.addElement(t);
  }
} ///:~

Как видно из этого класса, его единственная задача — обеспечивать связь между методом addTrash() интерфейса Fillable и методом addElement() класса Vector. Этот класс позволяет переопределенному методу fillBin() в ParseTrash.java использовать объект типа Vector:

  public static void
  fillBin(String filename, Vector bin) {
    fillBin(filename, new FillableVector(bin));
  }
```Этот подход применим ко всем часто используемым коллекциям. Кроме того, коллекции могут предоставлять свои собственные адаптеры и реализовывать интерфейс `Fillable` (как это будет показано позже в классе `DynaTrash.java`).(3) Повторное использование прототипного механизма

Теперь можно рассмотреть переработанную версию `RecycleA.java`, которая использует прототипную технологию:

```java
//: RecycleAP.java
// Восстановление с использованием RTTI и прототипов
package c16.recycleap;
import c16.trash.*;
import java.util.*;

public class RecycleAP {
  public static void main(String[] args) {
    Vector bin = new Vector();
    // Заполняем корзину отходами:
    ParseTrash.fillBin("Trash.dat", bin);
    Vector
      glassBin = new Vector(),
      paperBin = new Vector(),
      alBin = new Vector();
    Enumeration sorter = bin.elements();
    // Сортировка отходов:
    while(sorter.hasMoreElements()) {
      Object t = sorter.nextElement();
      // Использование RTTI для проверки принадлежности класса:
      if(t instanceof Aluminum)
        alBin.addElement(t);
      if(t instanceof Paper)
        paperBin.addElement(t);
      if(t instanceof Glass)
        glassBin.addElement(t);
    }
    Trash.sumValue(alBin);
    Trash.sumValue(paperBin);
    Trash.sumValue(glassBin);
    Trash.sumValue(bin);
  }
} ///:~

Все объекты класса Trash и поддерживающие классы теперь являются частью пакета c16.trash, поэтому они могут быть легко импортированы.Независимо от того, открывается ли файл данных, содержащий описание отходов, или же этот файл анализируется, все операции уже заключены в статический метод ParseTrash.fillBin(). Поэтому он больше не является ключевой частью нашего дизайна. В остальной части этой главы вы будете часто наблюдать, как метод ParseTrash.fillBin() продолжает работать независимо от добавляемых новых типов классов, что является отличным примером хорошего дизайнерского решения.

Касательно создания объектов, этот подход действительно локализовал изменения, необходимые для включения нового типа в систему. Однако при использовании RTTI возникает серьёзная проблема, которая здесь явно проявляется. Программа кажется работающей корректно, но она всегда ошибочно распознаёт новый тип отходов `Cardboard`, даже если такой тип присутствует в списке! Это происходит именно потому, что используется RTTI. RTTI ищет только те типы, которые мы указали ему найти. В данном случае неправильное использование RTTI заключается в том, что он проверяет каждый тип в системе, а не один конкретный тип или подмножество типов. Как вы увидите позже, при проверке каждого типа можно использовать полиморфизм по-другому. Но если слишком часто использовать RTTI таким образом и добавить новый тип в свою систему, легко забыть сделать соответствующие изменения в программе, что может привести к труднообнаруживаемым ошибкам.

Касательно создания объектов, этот подход действительно локализовал изменения, необходимые для включения нового типа в систему. Однако при использовании RTTI возникает серьёзная проблема, которая здесь явно проявляется. Программа кажется работающей корректно, но она всегда ошибочно распознаёт новый тип отходов `Cardboard`, даже если такой тип присутствует в списке! Это происходит именно потому, что используется RTTI. RTTI ищет только те типы, которые мы указали ему найти. В данном случае неправильное использование RTTI заключается в том, что он проверяет каждый тип в системе, а не один конкретный тип или подмножество типов. Как вы увидите позже, при проверке каждого типа можно использовать полиморфизм по-другому. Но если слишком часто использовать RTTI таким образом и добавить новый тип в свою систему, легко забыть сделать соответствующие изменения в программе, что может привести к труднообнаруживаемым ошибкам.Поэтому важно избегать использования RTTI в таких случаях, это не просто вопрос чистоты — это также способствует созданию более легко поддерживаемого кода.

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/wizardforcel-thinking-in-java-zh.git
git@api.gitlife.ru:oschina-mirror/wizardforcel-thinking-in-java-zh.git
oschina-mirror
wizardforcel-thinking-in-java-zh
wizardforcel-thinking-in-java-zh
master