Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Секреты программирования для Internet на Java

.pdf
Скачиваний:
181
Добавлен:
02.05.2014
Размер:
3.59 Mб
Скачать

Следующая глава далеко уходит от проектирования интерфейса пользователя. Глубже копнув в основные правила Java, мы покажем вам подробно, как работают исключения, и исследуем некоторые из проблем, встречающихся при разработке собственных исключений в полном объектно-ориентированном проекте. Кроме того, мы обсудим фундаментальные объекты, из которых получены все классы, и значение, которые они имеют при проектировании собственных иерархий классов.

Глава 10

Ⱦɚɧɧɚɹ ɜɟɪɫɢɹ ɤɧɢɝɢ ɜɵɩɭɳɟɧɚ ɷɥɟɤɬɪɨɧɧɵɦ ɢɡɞɚɬɟɥɶɫɬɜɨɦ %RRNV VKRS Ɋɚɫɩɪɨɫɬɪɚɧɟɧɢɟ ɩɪɨɞɚɠɚ ɩɟɪɟɡɚɩɢɫɶ ɞɚɧɧɨɣ ɤɧɢɝɢ ɢɥɢ ɟɟ ɱɚɫɬɟɣ ɁȺɉɊȿɓȿɇɕ Ɉ ɜɫɟɯ ɧɚɪɭɲɟɧɢɹɯ ɩɪɨɫɶɛɚ ɫɨɨɛɳɚɬɶ ɩɨ ɚɞɪɟɫɭ piracy@books-shop.com

Структура программы

Создание Java-пакетов Создание совместимых классов

Метод boolean equals(Object o) Метод String toString()

Создание повторно используемых компонентов Превращение проекта в работающий код Техника приведения типов объектов

Проверка кода на устойчивость Перехват исключений Генерация исключений

Информация об объектах при выполнении программы

К настоящему моменту у вас, должно быть, уже сложилось представление о том, как писать мощные апплеты. Вы поэкспериментировали с системой AWT (Abstract Windowing Toolkit) и с анимацией и научились эффективно обрабатывать ввод пользователя. Теперь настало время двинуться дальше, за пределы обыкновенных аплетов, и заставить работать всю остальную часть языка Java.

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

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

СОВЕТ Фрагменты кода, приводимые в качестве примеров в этой главе, помещены на диск CDROM, прилагаемый к книге. Этим диском могут пользоваться те из читателей, кто работает с Windows 95/NT или Macintosh; пользователи UNIX должны обращаться к Web-странице Online Companion, на которой собраны сопроводительные материалы к этой книге (адрес http://www.vmedia.com/java.html).

Создание Java-пакетов

Существующие средства интерфейса прикладного программирования (API) Java дают возможность писать приложения практически неограниченной сложности. Разумеется, для того чтобы использовать API по назначению, необходимо написать программу. И вот по мере того, как таких программ становится все больше и больше, у программиста возникает желание использовать в новой разработке фрагменты, уже написанные им ранее для чего-то другого. Раз так, то почему бы не перейти к использованию пакетов?

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

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

Прославьтесь в Сети - подарите обществу немного собственного кода

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

www.books-shop.com

массу благодарных электронных посланий; кроме того, кто-нибудь, вероятно, поделится с вами мыслями по поводу возможных усовершенствований. Сотрудничество - воистину то, на чем всегда держался Интернет, и ваша работа может стать частью этого замечательного процесса! На сервере Online Companion есть ссылки на интернетовские архивы с чужими разработками, куда всегда можно привнести что-то свое или позаимствовать уже сделанное другими.

Создание совместимых классов

В главе 6 мы показали, что в классе Object есть несколько методов, как правило, переопределяемых в других классах. Некоторые методы класса Object переопределяются в каждом производном классе и обязательно должны переопределяться в вашем собственном. Что же касается остальных методов, то решение, переопределять их или нет, - стратегически важный для проекта вопрос. Давайте рассмотрим несколько методов и решим, стоит ли переопределять их в каждом конкретном случае. Кроме того, мы рассмотрим последовательность действий, выполняемую в случае, если метод не переопределяется. Начнем с методов, переопределять которые необходимо всегда.

Метод boolean equals(Object o)

Если вы собираетесь сравнивать два объекта одного и того же класса, данный метод переопределяется обязательно. Если он не будет переопределен, объекты этого класса будут равны (метод возвратит true) лишь в одном случае - когда сравниваемые объекты будут на самом деле одним и тем же объектом. То, каким образом переопределять этот метод, в основном зависит от переменных внутри класса. Рассмотрим класс exampleClass. Предположим, что его конструкторы и прочие методы как-нибудь изменяют значения следующих переменных:

class exampleClass ( private String s; private int i; private int j;

//конструкторы

//методы

}

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

equals(Object obj) {

if (obj == null) return false; if (!obj instanceof exampleClass)

return false;

exampleClass ex = (exampleClass)obj; if (!ex.i != i) return false;

if (!ex.j != j) return false; return true;

}

Первое, что мы делаем, - проверяем, не передан ли методу нулевой указатель вместо самого объекта. Второй оператор проверяет, тот ли тип (exampleClass) у переданного для сравнения объекта. И если тип переданного объекта не совпадает с типом объекта, которому принадлежит описываемый метод, этот оператор возвращает false. Теперь, когда мы знаем, что сравниваются сопоставимые по типу объекты, мы приводим объект obj к типу exampleClass. Далее следуют три строки, операторы которых сравнивают значения переменных двух объектов. Если хотя бы одна переменная не равна другой, метод возвращает false. Обратите внимание на то, что мы имеем доступ к внутренним переменным благодаря тому, что действуем в рамках определения класса, которому они принадлежат.

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

www.books-shop.com

Метод String toString()

Проектирование метода toString не представляет сложности - он должен возвращать строку (тип String), отображающую текущее состояние объекта. Его основное назначение - отладка программы при помощи метода System.out.println. В следующем примере описывается класс box (прямоугольник), а метод toString возвращает его высоту и ширину.

class myBox{

int width = 0; int height = 0;

// конструкторы, методы

String toString() {

return "width = - + width + ", height = - + height;

}

Метод int hashCode()

Этот метод, так же как и два только что описанных, должен переопределяться всегда. Если этого не сделать, класс будет невозможно использовать совместно с классом java.util.Hashtable, рассмотренным в главе 6. Для того, чтобы научиться конструировать метод, и для того, чтобы понять, зачем он переопределяется, давайте рассмотрим подробнее структуру хeш-таблиц.

Быстрота поиска - одно из основных достоинств хeш-таблиц. Хeш-таблица никогда не просматривает содержимое всех ключевых полей при поиске. Чтобы найти компонент в таблице, состоящей из N пар "ключ-значение", необходимо произвести всего N операций сравнения. В Java это означает, что необходимо N раз вызвать метод сравнения. Если число N достаточно велико, извлечение элемента из таблицы может занять у системы ощутимое количество времени.

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

Как хеш-таблица решает, в какую корзину поместить пару "ключ-значение"? Для этого используется специальный алгоритм - хеш-функция (hash function). Ее задача - разместить пары "ключ-значение" по максимально большому количеству корзин и таким образом упростить дальнейший поиск, сведя количество ключей в одной корзине к минимуму. Как правило, при этом соблюдается принцип: "корзин больше, чем пар ключ-значение".

Здесь на сцену выступает метод hashCode. В качестве аргументов хеш-функции, входящей в состав java.util.Hashtable, передаются целые числа. Целые числа необходимы потому, что в одной и той же хеш-таблице могут храниться ключи разных типов. Теперь, когда у вас есть представление о назначении метода hashCode, давайте посмотрим на возможный вариант его корректной реализации. Предположим, что firstObject и secondObject - две отдельные реализации одного и того же класса.

Если firstObject.equals(secondObject) равно true, целые значения, возвращаемые firstObject.hashCode и secondObject.hashCode, должны быть равны.

Учитывая это ограничение, метод hashCode должен возвращать как можно более уникальное значение. Чтобы понять почему, давайте рассмотрим наипростейший метод hashCode, удовлетворяющий приведенному выше ограничению:

int hashCode() {return 1;}

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

Однако идеальное редко достижимо на практике. Можно только посоветовать не проводить массу бессонных ночей за разработкой идеального метода hashCode, удовлетворяющего абсолютно всем критериям - как правило, это невозможно. Поскольку hashCode возвращает целое число, существует только 4294967296 его возможных значений. Среди всех переменных типа String существует 9223372936854775897 возможных значений только для строки длиной

www.books-shop.com

всего лишь в два символа! Теперь, когда мы знаем, что получить оптимальный хеш-код для типа String невозможно, давайте посмотрим, чего добились разработчики типа String. Их главной целью было обеспечить получение как можно более уникального значения:

// метод hashCode класса java.lang.hashCode public int hashCode() {

int h = 0;

int off = offset; char val[] = value; int len = count;

if (len < 16) {

for (int i = len; i > 0; i -) { h = (h * 37) + val[off ++];

}

} else {

// берем лишь некоторые символы int skip = len / 8;

for (int i = len; i > 0; i -= skip, off += skip) { h = (h * 39) + val[off];

}

}

return h;

}

СОВЕТ Исходный текст класса String так же, как и текст остальных классов API, находится в JDK. Иногда бывает интересно и полезно заглянуть туда, чтобы познакомиться с точкой зрения разработчиков Java по тому или иному поводу.

Метод Object clone()

Возможно, вы помните из главы 2, что знак операции присваивания = не делает копий объектов. Вместо этого, если одним из его операндов является переменная-экземпляр, одному и тому же объекту присваиваются обе переменные. Предположим, что у нас есть простой связанный список, представленный классом, который содержит целое число и объект - данные списка. Метод setNext позволяет создать структуру данных, приведенную на рис. 10-1.

Рис. 10.1.

Обратите внимание на то, что мы переопределили методы класса Object, о которых говорилось выше:

public class Node {

private Object Data = null; private Node Next = null; private int num;

public void setData(Object obj) { Data = obj;}

public void setNum(int someNum) { num = someNum;}

public void setNext(Node node) {

www.books-shop.com

Next = node;}

public Object getData() {return Data;} public Node getNextNode() {

return Next;}

public boolean equals(Object obj) {

if (!(obj instanceof Node)) return false; Node N = (Node) obj;

return((N.num == num) && Data.equals(N.Data));

}

public int hashCode() {

return Data.hashCode() * num;} public String toString() {

String S = "Data = ";

S = S + Data.toString(); S = S + ", num = - + num; return S;

}

Если мы сделаем следующее:

Node A = new Node();

Node B = A;

Integer I = new Integer(32);

B.setNum(16);

B.setData(32);

мы не получим два различных экземпляра linkedListNode, но всего лишь один, на который ссылаются переменные A и B. Это значит, что

System.out.println(A);

выдаст результат "data=32, num=16". (Как вы помните, метод System.out. println, получив объект Object в качестве аргумента, вызывает метод toString и выводит результат в виде строки.) А что если нам действительно нужна копия экземпляра? Тут на сцену выходит метод clone класса Object. Он копирует экземпляр, включая текущие состояния всех его переменных. Мы можем сконструировать подкласс класса Node, который допускается клонировать:

public class cloneableNode extends Node implements Cloneable { public Object clone() {

try {

return super.clone();

}

catch (CloneNotSupportedException e) {

//этого не должно случиться, поскольку

//мы описали класс как "implements Cloneable" throw new InternalError();

}

}

Все, что мы здесь делаем, - это вызываем метод clone класса Object. Поскольку он генерирует исключение CloneNotSupportedException, мы вынуждены его обработать. Попробуем применить наш метод клонирования после того, как создан экземпляр класса cloneableNode:

cloneableNode A = new cloneableNode(); A.setNum(16);

Integer I = new Integer(32); A.setData(I);

cloneableNode B = (cloneableNode) A.clone();

Теперь переменные A и B относятся к разным объектам, как показано на рис. 10-2. Однако заметьте, что переменные A.data и B.data остаются теми же. Дело в том, что мы сделали так называемую неполную (shallow) копию экземпляра - его переменные-ссылки на самом деле не копировались. Теперь предположим, что у нас есть связанный список с большим количеством объектов cloneableNode, и мы хотим воспроизвести (реплицировать) его. Нам понадобится следующее определение метода clone:

www.books-shop.com

Рис. 10.2.

class List extends cloneableNode implements Cloneable { public Object clone() {

try {

List newList = super.clone();

cloneableNode oldNode = (cloneableNode) this; cloneableNode newNode = (cloneableNode) newList; while (oldNode.getNextNode() != null) {

oldNode = oldNode.getNextNode(); newNode.setNextoldNode.clone()); newNode = newNode.getNext();

}

return newList;

} catch (CloneNotSupportedException e) { throw new InternalError();

}

Поскольку метод clone объявлен с модификатором protected, мы не можем скопировать данные. Для того чтобы сделать это, метод необходимо объявить с модификатором public. На этапе разработки программисту нужно решить, должны ли классы, не входящие в состав пакета, иметь возможность клонировать объекты класса. Как правило, необходимость в этом отсутствует.

Рассмотрим класс Graphics, который мы будем обсуждать в последней главе. Экземпляр класса Graphics, в рамках которого можно рисовать, привязан к оболочке времени выполнения программы. На самом деле он представляет собой просто область экрана, на которую выводится рисунок. Клонирование объекта Graphics вовсе не приведет к ожидаемому эффекту - появлению нового экрана. По этой причине нам запрещено клонировать объекты класса Graphics. Точно так же, если вы разрабатываете внутренние по отношению к большой программе классы, вряд ли кому-нибудь пригодится возможность клонировать их, если только не клонируется вся программа целиком.

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

public class happilyCloning implements Cloneable { public Object Clone() {

try {

return super.Clone();

}

catch (CloneNotSupportedException e) { throw (new InternalError());

}

}

// что-нибудь еще

}

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

www.books-shop.com

NullPointerException. Вместо этого некрасивого способа мы предлагаем генерировать исключение

CloneNotSupportedException:

public class DontCloneMe {

public Object Clone() throws CloneNotSupportedException { throw (new CloneNotSupportedException());

}

// еще что-нибудь

}

Метод void finalize()

Как только экземпляры определенного класса перестают быть нужными программе, для них вызывается метод finalize. Он эквивалентен часто вызываемому для освобождения занятых ресурсов деструктору из языка C++; с его помощью, например, освобождаются выделенные ранее программе блоки памяти. В отличие от C++ для освобождения памяти в Java производится автоматическая сборка мусора (мы уже говорили об этом в главе 2). Если вы программировали на C++, вам, возможно, приходилось писать деструкторы, освобождающие память, почти для каждого класса программы. В Java необходимости в этом нет, поэтому метод finalize используется сравнительно редко.

Давайте вернемся к рассмотренному выше классу - связанному списку. Если выполнить следующую инструкцию, где список curList уже существует:

curList = curList.clone();

то не нужно беспокоиться по поводу памяти, занятой предыдущим экземпляром curList, - сборщик мусора самостоятельно распознает появление неиспользуемой области памяти и позаботится обо всем необходимом.

Таким образом, нам не нужно беспокоиться об освобождении памяти в системе и писать соответствующий метод finalize. Скорее он может пригодиться, когда программа имеет дело с файловой системой или сетевыми ресурсами. До сих пор мы не рассматривали работу с файлами или с сетью (апплеты в любом случае не могут работать с файлами). Поэтому сейчас мы не можем привести пример достойного использования метода finalize. Если все-таки желание увидеть finalize в действии непреодолимо, взгляните на исходный текст FileInputStream.java из пакета java.io. Исходный текст всех классов Java является частью JDK.

Создание повторно используемых компонентов

Итак, мы познакомились с методами класса Object и с тем, как правильно их переопределять. Пришла пора перейти к более общей концепции объектно-ориентированного программирования и поговорить о компонентах, готовых к повторному использованию.

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

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

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

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

www.books-shop.com

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

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

Что это за проблема?

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

Каковы составляющие проблемы и как они взаимодействуют между собой?

Это критическая стадия разделения проблемы на составляющие и победы над ней. Проблема обдумывается в терминах абстрактных, а не привязанных к конкретному языку объектов. Хорошо было бы сделать небольшую схему или чертеж, позволяющий представить проблему наглядным образом, наподобие приведенного на рис. 10-3.

Рис. 10.3.

Задача рисунка - схематично наметить пути взаимодействия элементов, объектов, из которых состоит решаемая проблема, и определить место каждого компонента в общем процессе. Повторим, на этой стадии еще рано думать об объектах со всеми их внутренними данными и методами. Достаточно составить список свойств, как это сделано в табл. 10-1. Для текущей стадии решения проблемы этого оказывается достаточно.

Таблица 10-1. Свойства элементов системы управления воздушными перевозками

Самолет

Имеет определенное положение и траекторию. Следует по некоторому маршруту.

 

Имеет время прибытия и отправления.

Маршрут

Может пересекаться с другими маршрутами. На него оказывают влияние погодные

 

условия.

www.books-shop.com

Ангар

Имеет заданную вместимость и координаты. Может быть заполнен или не заполнен.

ВПП

Имеет определенную длину. Может быть занята либо свободна. Некоторые типы

 

самолетов не могут использовать данную ВПП. Возможность использования ВПП

 

самолетами зависит от погодных условий.

Место для

Имеет заданную вместимость и координаты. На вместимость оказывают влияние

парковки

погодные условия.

Башня

Необходима для слежения за обстановкой на аэродроме и в воздухе. Необходима

слежения

для слежения за погодными условиями.

Чем данный проект похож на другие?

До того как приступить к решению проблемы, ее необходимо разделить на области, каждая из которых содержала бы более мелкую отдельную проблему. Проект необходимо структурировать так же, как структурируются большинство программ. Только таким образом достигается универсальность решений - проблемы, составляющие проект, тоже должны оставаться универсальными.

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

Как обобщить отдельные компоненты?

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

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

Как подогнать исходный текст под объектную ориентацию Java?

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

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

Не затеряйтесь в диаграммах!

Конструируя собственные классы, стоит периодически задавать себе пару вопросов: "Что может пойти не так, как ожидается?" и "Не делаю ли я лишней работы?" Первый вопрос должен привести вас к созданию собственной библиотеки исключений, а второй - сократить чересчур большое количество абстрактных классов и интерфейсов. Очарование универсальности программных компонентов часто приводит к чрезмерному увлечению абстракцией и тому, что никто, кроме вас самих, никогда не сможет воспользоваться этой универсальной сетью.

www.books-shop.com