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

0

 

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

Разумно начать проектирование с создания абстрактного класса, предусматривающего возможность дальнейшей реализации всех названных особенностей, но при этом не следует писать исходный код метода сортировки – подобную обязанность целесообразно возложить на производные классы. Ниже приведен текст класса SortDoublе, который предоставляет средства сортировки массивов значений типа doublе, а также отслеживания числа операций обращения к элементам, их взаимной замены и сравнения – эта информация потребуется классу SortMetrics, который мы рассмотрим далее.

abstract class SortDouble {

 private double[] values;

abstract class SortDouble{

      private double[] values;

      private final SortMetrics curMetrix = new SortMetrics();

     

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

      public final SortMetrics sort(double[] data){

                  values = data;

                  curMetrics.init();

                  doSort();

                  return getMetrics();

      }

     

      public final SortMetrics getMetrics(){

                  return (SortMetrics)curMetrics.clone();

      }

     

      /** Число сортируемых элементов –

       * для использования в производных классах*/

      protected final int getDataLength(){

                  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;           

      }

     

      /**Метод сортировки -

       * для использования в производных классах*/

      protected abstract void doSort();

}

В объявлении класса содержатся поля для хранения массива сортируемых данных (values) и ссылка на объект класса sortMetrics (Metrics), содержащий данные измерений количественных показателей процедуры сортировки. Для обеспечения корректности данных о параметрах сортировки в составе класса предусмотрены собственные методы. Они должны быть использованы к производных классах при необходимости обращения к элементам, а также выполнения операций их сравнения и взаимной замены (swap).

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

В классе мы ограничиваем доступ к каждому члену тщательно и избирательно. Члены, не являющиеся абстрактными, помечены модификатором final, – все они служат частью контракта класса. Так, например, мы защищаем от подделки методы, обеспечивающие выполнение базовых операций сортировки – сравнения и замены элементов – и получение результатов измерений. Используя признак final, мы гарантируем, что соответствующие методы не будут переопределены в производных классах, и позволяем компилятору и исполняющей системе обеспечить настолько высокий уровень оптимальности кода, на-

сколько это возможно.

Объекты класса SortMetrics  содержат информацию о затратах, с которыми

сопряжен процесс сортировки. Класс содержит три поля с признаком public. Единственное назначение класса – хранение данных, и поэтому не имеет смысла скрывать их посредством методов доступа. Метод SortDouble.getMetrics с помощью операции клонирования возвращает копию объекта SortMetrics, что препятствует возможности изменения внутренних данных со стороны внешнего кода, в котором создаются объекты SortDoublе, и производных классов. Так выглядит объявление класса SortMetrics:

 

final class SortMetrics implements Cloneable {

              public      long probeCnt,          // число обращений к элементам

                             compareCnt,              // число операций сравнения

                             swapCnt;                   // число операций замены

public void init() {

probecnt = swapCnt = compareCnt = 0;

}

public String toString() {

                  return         "обращений" + probeCnt +

сравнений " + compareCnt + " замен" + 5wapCnt;

}

/** Класс поддерживает операцию клонирования */

public Object clone() {

try {

return super.clone();

// вариант клонирования по умолчанию

} catch (CloneNotSupportedException е) {

      // Это произойти не может 

// операция клонирования поддерживается

 throw new InternalError(e.toString());

}

}

}

Следующий класс является производным от SortDouble. В теле метода doSort класса реализован довольно неэффективный алгоритм сортировки посредством выбора, единственное неоспоримое преимущество которого состоит в простоте программирования и восприятия кода.

class SimpleSortDouble extends SortDouble { protected void doSort () {

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

for (int j = i + 1; j < getDataLength(); j++) { if (compare(i, j) > О)

swap(i, j);

}

}

}

}

Теперь настал черед создания класса, позволяющего тестировать различные алгоритмы сортировки. Класс должен быть реализован таким образом, чтобы количество изменений, которые потребуется вносить при переходе к очередному алгоритму, было минимально. Ниже приведен вариант такого класса применительно к задаче тестирования процедуры сортировки посредством выбора, реализованной в классе 5impleSortDouble.

public class TestSort {

static double[] testData = {

0.3, 1.3е-2, 7.9, 3.17,

} ;

public static void main(String[] args) {

SortDouble bsort = new SimpleSortDouble();

SortMetrics metrics = bsort.sort(testData);

Sуstеm.оut.ргiпtln("Результат измерений:" + metrics);

for (int i = 0; i < testData.length; i++)

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

}

}

Метод main демонстрирует один из возможных вариантов процесса тестирования: создается объект класса, производного от SortDouble, и вызывается соответствующий метод sort, которому в качестве аргумента передается массив значений, Подлежащих сортировке. Метод sort обеспечивает сохранность данных, Создает и инициализирует объект класса SortMetrics и вызывает переопределенный метод doSort. В каждом классе, расширяющем SortDouble, реализуется собственная версия метода doSort, в которой при необходимости вызываются Методы getDataLength, и swap базового класса. Метод sort возвращает объект класса SortMetrics, содержащий информацию о количестве операций, выполненных в процессе сортировки. Чтобы протестировать другой алгоритм, достаточно изменить имя производного класса, указанное после оператора new в первой строке тела метода та; п. Вот так может выглядеть результат одного сеанса работы программы:

Результат измерений: обращений 0 сравнений 6 Замен 2

0.013

0.3

3.17

7.9

Давайте вновь вернемся к общей проблеме проектирования классов, подлежащих наследованию, вооружившись только что рассмотренными при мерами. Как вы могли убедиться, интерфейс ргоtесtеd-членов класса SortDoubl е спланирован особо тщательно: он открывает для производных классов возможности более полного доступа к внутренним данным – но только к тем из них, которые мы как авторы базового класса посчитали нужным предать огласке. МЫ внимательно рассмотрели каждый из членов класса и скрупулезно проанализировали, каким признаком доступа его следует обозначить.

·       publiс. Те члены класса SortDoubl е, которые помечены модификатором public, предназначены для использования в коде, обеспечивающем процесс тестирования характеристик процедуры сортировки. Пример кода, выполняющего тестирование, приведен в тексте метода TestSort.main. Код содержит исходные данные, подлежащие сортировке, и позволяет представить в наглядной форме ее итоги. Объект, содержащий результаты измерений, не может быть изменен тестирующим кодом. В public-методе sort предусмотрены инструкции инициализации объекта с результатами измерений, гарантирующие его корректность.

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

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

·      protected. Члены класса, обозначенные как protected, спроектированы с целью применения в коде, который обеспечивает выполнение сортировки и измерение ее результатов. Соответствующий ргоtесtеd-контракт класса SortDoubl е позволяет производным классам, реализующим сортировку, проверять и изменять данные таким образом, как это предусмотрено конкретным алгоритмом. Контракт дает возможность процедуре сортировки действовать в заведомо правильном контексте. Этот контекст – тело метода doSort.

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

Следует также повторить, что метод getMetrics, возвращая во внешний код результаты измерений параметров сортировки, выполняет Клонирование (копирование) соответствующего объекта SortMetrics, что препятствует намеренному или случайному искажению данных.

·      рrivate, . Класс тщательно оберегает от внешнего воздействия данные, подлежащие сортировке, и результаты измерений параметров процесса. Внешнему коду запрещено обращаться к полям values и curMetrics прямо или косвенно.

Как уже говорилось, мы придерживаемся политики "недоверия" по отношению к производным классам, чтобы предотвратить любой возможный умышленный или случайный ущерб, и класс спроектирован именно таким образом. Если бы, например, поле (массив данных, подлежащих сортировке) было обозначено как protected вместо рrivate, , мы могли бы, как кажется, просто выбросить метод probe, обеспечивающий возможность обращения к элементу, за ненадобностью, поскольку при тестировании алгоритмов сортировки внимание обычно обращается только на количество операций сравнения и взаимной замены. Но поступи мы так, и программист, создающий производный класс, будет в состоянии запросто обойтись, скажем, без предусмотренного нами метода 5wap, осуществляющего замену элементов, и написать свой собственный аналогичный метод. Результаты могут оказаться совершенно неожиданными и, вообще говоря, неправильными. Средства учета количества обращений к элементам, предусмотренные в методе probe, и ‘модификатор private, которым помечен массив исходных данных, препятствуют возникновению возможных ошибок и попыткам преднамеренной фальсификации данных.

Если при проектировании класса не учтена вероятность его наследования, это Может привести к неправильному толкованию контракта и ошибкам в. производных классах. Если класс будет расширяться, вы должны тщательно продумать дизайн его ргоtесtеd-членов. В конечном итоге таких членов может не быть вовсе, коль скоро в производных классах не предусматриваются какие-либо специальные действия. Если же о наследовании вы вообще не задумываетесь, не пользуйтесь модификатором protected бездумно и хаотично – в таком случае производным Классам, если таковые все-таки будут создаваться, придется уповать только на общий контракт класса, определяемый совокупностью членов public.

Упражнение 3.11. Попробуйте отыскать хотя бы одну брешь в системе защиты класса SortDoublе, которая позволяет производному классу незаметно "сжульничать" в отношении результатов эффективности сортировки. Исправьте Допущенный нами промах.

Упражнение 3.12. Создайте общий класс SortHarne55 (инструментарий сортировки), который способен выполнять сортировку объектов любых типов. Каким Образом вы могли бы обеспечить возможность упорядочения объектов, если пользоваться оператором < (меньше) для сопоставления их содержимого нельзя?

 

Источник: Арнолд, Кен, Гослинг, Джеймс, Холмс, Дэвид. Язык программирования Java. 3-е изд .. : Пер. с англ. – М. : Издательский дом «Вильяме», 2001. – 624 с. : ил. – Парал. тит. англ.

По теме:

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