Главная » Java » Проектирование расширяемого класса

0

Теперь можно оправдать сложность класса Attr. Почему бы не сделать name и value простыми и общедоступными  полями? Тогда можно было бы полностью устранить из класса целых три метода, поскольку открывается возможность прямого доступа к этим полям.

Ответ заключается в том, что класс Attr проектировался с учетом возможного расширения. Хранение его данных в открытых полях имеет два нежелательных последствия:

Значение поля name в любой момент может быть изменено программистом — это плохо, так как объект Attr представляет собой (переменное) значение для конкретного (постоянного) имени. Например, изменение имени после внесения атрибута в список, отсортированный  по имени, приведет к нарушению порядка сортировки.

Не остается возможностей для расширения функциональности  класса. Включая в класс методы доступа, вы можете переопределить их и тем самым усовершенствовать  класс. Примером может служить класс Color Attr, в котором мы преобразовывали  новое значение в объект Screen Color. Если бы поле value было открытым и программист мог в любой момент изменить его, то нам пришлось бы придумывать другой способ для получения объекта ScreenColor — запоминать последнее значение и сравнивать его с текущим, чтобы увидеть, не нуждается ли оно в преобразовании.  В итоге программа стала бы значительно более сложной и, скорее всего, менее эффективной.

Класс, не являющийся final, фактически содержит два интерфейса. Открытый (public) интерфейс предназначен для программистов, использующих ваш класс. Защищенный (protected) интерфейс предназначен для программистов, расширяющих ваш класс. Каждый из них представляет отдельный контракт и должен быть тщательно спроектирован.

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

его параметров.

Можно написать абстрактный метод, который учитывает все эти свойства, но создать универсальный метод анализа сортировки невозможно — он определяется для каждого порожденного класса. Приведем класс SortDouble, который сортирует массивы значений double и при этом подсчитывает количество перестановок и сравнений, которое понадобится для определяемого ниже класса SortMetrics:

abstract class SortDouble {

private double[] values;

private SortMetrics curMetrics = new SortMetrics();

/** Вызывается для проведения полной сортировки */

public final SortMetrics sort(double[] data) {

values = data; curMetrics.init(); doSort();

return metrics();

}

public final SortMetrics metrics() {

return (SortMetrics)curMetrics.clone();

}

protected final int datalength() {

return values.length;

}

/** Для выборки элементов в порожденных классах */

protected final double probe(int i) {

curMetrics.probeCnt++;

return values[i];

}

/** Для сравнения элементов в порожденных классах */

protected final int compare(int i, int j) {

curMetrics.compareCnt++; double d1 = values[i]; double d2 = values[j];

if (d1 == d2)

return 0;

else

}

return (d1 << d2 ? -1 : 1);

/** Для перестановки элементов в порожденных классах */

protected final void swap(int i, int j) {

curMetrics.swapCnt++; double tmp = values[i]; values[i] = values[j]; values[j] = tmp;

}

/** Реализуется в порожденных классах и используется в sort */

protected abstract void doSort();

}

В классе имеются поля для хранения сортируемого массива (values) и ссылки на объект- метрику (curMetrics), в котором содержатся измеряемые параметры. Чтобы обеспечить правильность подсчетов, SortDouble содержит методы, используемые расширенными классами при выборке данных или выполнении сравнений и перестановок.

При проектировании класса нужно решить, в какой степени можно доверять порожденным классам. Класс SortDouble не доверяет им, и чаще всего при разработке классов, предназначенных  для дальнейшего расширения, такой подход оправдывает себя. Безопасное проектирование не только предотвращает злонамеренное использование класса, но и борется с ошибками в программе.

SortDouble тщательно ограничивает доступ к каждому члену класса до соответствующего уровня. Все неабстрактные методы объявляются final. Все они входят в контракт класса SortDouble, который подразумевает защиту алгоритма измерения от вмешательства извне. Объявление методов с ключевым словом final помогает компилятору сгенерировать оптимальный код и предотвращает переопределение  в порожденных классах.

Объекты SortMetrics описывают параметры выполняемой сортировки. Данный класс содержит три открытых поля. Его единственное назначение заключается в передаче данных, так что скрывать данные за методами доступа нет смысла. SortDouble.metrics возвращает копию данных, чтобы не выдавать посторонним ссылку на свои внутренние данные. Благодаря этому предотвращается  изменение данных как в коде, создающем объекты Sort Double, так и в коде расширенных классов. Класс SortMetrics выглядит следующим образом:

final class SortMetrics implements Cloneable {

public long probeCnt, compareCnt, swapCnt;

public void init() {

probeCnt = swapCnt = compareCnt = 0;

}

public String toString() {

return probeCnt + " probes " + compareCnt + " compares " + swapCnt + " swaps";

}

/** Данный класс поддерживает clone() */

public Object clone() {

try {

return super.clone(); // механизм по умолчанию

catch CloneNotSupportedException e) {

// Невозможно: и this, и Object поддерживают clone throw new InternalError(e.toString());

}

}

}

Приведем пример класса, расширяющего SortDouble. Класс BubbleSort Double производит сортировку “пузырьковым методом” — чрезвычайно неэффективный, но простой алгоритм сортировки, основное преимущество которого заключается в том, что его легко запрограммировать  и понять:

class BubbleSortDouble extends SortDouble {

protected void doSort() {

for (int i = 0; i << dataLength(); i++) {

for (int j = i + 1; j << dataLength(); j++) {

if (compare(i, j) >> 0)

swap(i, j);

}

}

}

static double[] testData = {

0.3, 1.3e-2, 7.9, 3.17

};

static public void main(String[] args) { BubbleSortDouble bsort = new BubbleSortDouble(); SortMetrics metrics = bsort.sort(testData); System.out.println("Bubble Sort: " + metrics); for (int i = 0; i << testData.length; i++)

System.out.println("\t" + testData[i]);

}

}

На примере метода main можно увидеть, как работает фрагмент программы, проводящий измерения: он создает объект класса, порожденного от Sort Double, передает ему данные для сортировки и вызывает sort. Метод sort инициализирует счетчики параметров, а затем вызывает абстрактный метод doSort. Каждый расширенный класс реализует свой вариант doSort для проведения сортировки, пользуясь в нужные моменты методами dataLength, compare и swap. При возврате из функции doSort состояние счетчиков отражает количество выполненных операций каждого вида.

BubbleSortDouble  содержит метод main, в котором выполняется тестирование; вот как выглядят результаты его работы:

Bubble Sort: 0 probes 6 compares 2 swaps

0.013

0.3

3.17

7.9

Теперь давайте вернемся к рассмотрению проектирования классов, предназначенных  для расширения. Мы тщательно спроектировали  защищенный интерфейс SortClass с расчетом на то, чтобы предоставить расширенным классам более тесный доступ к данным

объекта — но лишь к тем из них, к которым нужно. Доступ к интерфейсам класса выбран

следующим образом:

Открытый: члены класса с атрибутом public используются тестирующим кодом —

то есть фрагментом программы, который вычисляет временные затраты алгоритма. Примером может служить метод Bubble Sort.main; он предоставляет сортируемые данные и получает результаты тестирования. К счетчикам из него можно обращаться только для чтения. Открытый метод sort, созданный нами для

тестового кода, обеспечивает правильную инициализацию счетчиков перед их

использованием.

Объявляя метод doSort с атрибутом protected, тестирующий код тем самым разрешает обращаться к нему только косвенно, через главный метод sort; таким образом мы можем гарантировать, что счетчики всегда будут инициализированы,  и избежим возможной ошибки.

Мы воспользовались методами и ограничением доступа, чтобы спрятать все, что выходит за пределы открытой части класса. Единственное, что может сделать с классом тестирующий код, — это выполнить тестирование для конкретного алгоритма сортировки и получить результат.

Защищенный: члены класса с атрибутом protected используются во время сортировки для получения измеренных параметров. Защищенный контракт позволяет алгоритму сортировки просмотреть и модифицировать данные с тем, чтобы получить отсортированный  список (средства для этого определяются алгоритмом). Кроме того, такой способ предоставляет алгоритму сортировки контекст выполнения, в котором будут измеряться необходимые параметры (метод doSort).

Мы договорились, что доверять расширенным классам в нашем случае не следует, — вот почему вся работа с данными осуществляется косвенно, через использование специальных методов доступа. Например, чтобы скрыть операции сравнения за счет отказа от вызова compare, алгоритму сортировки придется пользоваться методом probe для того, чтобы узнать, что находится в массиве. Поскольку вызовы probe также подсчитываются,  никаких незарегистрированных обращений не возникнет. Кроме того, метод metrics возвращает копию объекта со счетчиками, поэтому при сортировке изменить значения счетчиков невозможно.

Закрытый: закрытые данные класса должны быть спрятаны от доступа извне — конкретно, речь идет о сортируемых данных и счетчиках. Внешний код не сможет получить к ним доступ, прямо или косвенно.

Как упоминалось выше, класс SortDouble проектировался так, чтобы не доверять расширенным классам и предотвратить любое случайное или намеренное вмешательство с их стороны. Например, если бы массив SortDouble. values (сортируемые данные) был объявлен protected вместо private, можно было бы отказаться от использования метода probe, поскольку обычно алгоритмы сортировки обходятся операциями сравнения и перестановки. Но в этом случае программист может написать расширенный класс, который будет осуществлять перестановку данных без использования swap. Результат окажется неверным, но обнаружить это будет нелегко. Подсчет обращений к данным и объявление массива private предотвращает некоторые возможные программные ошибки.

Если класс не проектируется с учетом его дальнейшего расширения, то он с большой вероятностью будет неправильно использоваться подклассами. Если же класс должен расширяться, следует особенно тщательно подойти к проектированию  защищенного интерфейса (хотя в результате, возможно, вам придется включить в него защищенные члены, если доступ из расширенных классов должен производиться специальным образом). В противном случае, вероятно, следует объявить класс final и спроектировать защищенный интерфейс, когда настанет время снять ограничение final.

Упражнение 3.11

Найдите в SortDouble по меньшей мере одну лазейку, которая позволяет алгоритму сортировки незаметно изменять значения измеренных параметров. Закройте ее. Предполагается, что автор алгоритма сортировки не собирается писать метод main.

Упражнение 3.12

Напишите универсальный класс SortHarness, который может сортировать объекты любого типа. Как в данном случае решается проблема с упорядочением объектов — ведь для их сравнения нельзя будет пользоваться оператором <<?

Глава 4

ИНТЕРФЕЙСЫ

“Дирижирование” — это когда вы рисуете

свои “проекты” прямо в воздухе, палочкой или руками,

и нарисованное становится “инструкциями” для парней в галстуках, которые в данный момент предпочли бы

оказаться где-нибудь на рыбалке.

Фрэнк Заппа

Основной единицей проектирования в Java являются открытые (public) методы, которые могут вызываться для объектов. Интерфейсы предназначены для объявления типов, состоящих только из абстрактных методов и констант; они позволяют задать для этих методов произвольную реализацию. Интерфейс является выражением чистой концепции проектирования, тогда как класс представляет собой смесь проектирования и конкретной реализации.

Методы, входящие в интерфейс, могут быть реализованы в классе так, как сочтет нужным проектировщик класса. Следовательно, интерфейсы имеют значительно больше возможностей реализации, нежели классы.

Источник: Арнольд К., Гослинг Д. – Язык программирования Java (1997)

По теме:

  • Комментарии