Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Объектно-ориентированное программирование.PDF
Скачиваний:
208
Добавлен:
01.05.2014
Размер:
3.64 Mб
Скачать

converted to PDF by BoJIoc

4.Почему использованный в следующем рассуждении пример не является верной иллюстрацией наследования?

Видимо, наиболее важным понятием в объектно-ориентированном программировании является наследование. Объекты могут наследовать свойства других объектов, тем самым ликвидируется необходимость написания какого-либо кода! Предположим, например, что программа должна обрабатывать комплексные числа, состоящие из вещественной и мнимой частей. Для комплексных чисел вещественная и мнимая части ведут себя как вещественные величины, поэтому все операции (+, –, /, *, sqrt, sin, cos и т. д.) могут быть наследованы от класса Real вместо того, чтобы писать новый код. Это, несомненно, окажет большое влияние на продуктивность работы программиста.

Глава 8

Учебный пример: Пасьянс

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

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

переопределением интенсивно используется для упрощения разработки этих компонент и обеспечения их единообразия.

8.1. Класс игральных карт Card

В предыдущих главах мы обсуждали абстрактный класс Card. Повторим некоторые важные моменты.

Каждый экземпляр класса Card (листинг 8.1) наделен мастью и рангом. Чтобы предотвратить их изменение, поля данных (переменные экземпляра) объявлены закрытыми, и сделать что-либо с ними можно только посредством функций доступа.

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

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

converted to PDF by BoJIoc

Листинг 8.1. Описание класса card

class Card {

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

Card (int sv, int rv)

{

s = sv; r = rv; faceup = false;

}

//доступ к атрибутам карты public int rank ()

{ return r; } public int suit () { return s; }

public boolean faceUp() { return faceup; } public void flip()

{ faceup = ! faceup; } public int color()

{

if (suit() == heart || suit == diamond) return red;

return black;

}

public void draw (Graphics g, int x, int y)

{

...

}

//статические поля данных для цвета и масти

final static int width

= 50;

final static int heigth

= 70;

final static int red

= 0;

final static int black

= 1;

final static int heart

= 0;

final static int spade

= 1;

final static int diamond

= 2;

final static int club

= 3;

// поля данных

 

private boolean faceup;

 

private int r;

 

private int s;

 

}

Итак, все действия, которые может выполнить карта (кроме установки и возврата состояния), — это переворачивание и показ себя. Функция flip() состоит из одной строчки, которая просто обращает значение, содержащееся в переменной экземпляра faceup, на противоположное. Функция рисования draw() сложнее: она использует графические средства, предоставляемые стандартной библиотекой приложений Java. Библиотека приложений поставляет тип данных, называемый Graphics, который обеспечивает множество методов рисования линий и фигур, а также раскрашивание. В качестве аргумента функции рисования передается значение типа Graphics, а также целочисленные координаты, соответствующие верхнему левому углу карты.

Графические изображения карт рисунки из простых линий, как показано ниже. Черви и бубны нарисованы красным, а пики и крести черным. Штриховка рубашки выполнена желтым цветом. Фрагмент процедуры рисования игральной карты показан в

листинге 8.2.

converted to PDF by BoJIoc

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

Листинг 8.2. Процедура рисования игральной карты

class Card { ...

public void draw (Graphics g, int x, int y)

{ String names[] = {"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"};

//Очистить прямоугольник, нарисовать границу g.clearRect(x, y, width, height); g.setColor(Color.black);

g.drawRect(x, y, width, height);

//нарисовать тело карты

if (faceUp) // лицевой стороной вверх

{ if (color() == red) g.setColor(Color.red); else g.setColor(Color.blue);

g.drawString(names[rank()], x+3, y+15); if (suit() == heart)

{g.drawLine(x+25, y+30, x+35, y+20); g.drawLine(x+35, y+20, x+45, y+30); g.drawLine(x+45, y+30, x+25, y+60); g.drawLine(x+25, y+60, x+5, y+30); g.drawLine(x+5, y+30, x+15, y+20);

g.drawLine(x+15, y+20, x+25, y+30);

}

else if (suit() == spade )

{... }

else if (suit() == diamond )

{... }

else if (suit() == club )

{g.drawOval(x+20, y+25, 10, 10); g.drawOval(x+25, y+35, 10, 10); g.drawOval(x+15, y+35, 10, 10); g.drawOval(x+23, y+45, x+20, y+55); g.drawOval(x+20, y+55, x+30, y+55);

g.drawOval(x+30, y+55, x+27, y+45);

}

}

else // картинкой вниз

{g.setColor(Color.yellow); g.drawLine(x+15, y+5, x+15, y+65);

g.drawLine(x+35, y+5, x+35, y+65); g.drawLine(x+5, y+20, x+45, y+20); g.drawLine(x+5, y+35, x+45, y+35);

converted to PDF by BoJIoc

g.drawLine(x+5, y+50, x+45, y+50);

}

}

}

8.2. Связные списки

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

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

В абстракции связного списка задействованы два класса. Класс LinkedList это «фасад» списка, то есть класс, с которым взаимодействует пользователь. В действительности значения хранятся в экземплярах класса List. Обычно пользователь даже не догадывается о существовании класса List. Оба класса показаны в лист. 8.3.

Так как контейнер данных на основе связного списка является абстракцией общего назначения и ничего не знает о типе объекта, который он будет содержать, то тип данных, приписываемый объекту-значению,это класс всех объектов Object. Переменная, объявленная с типом данных Object (в частности, поле данных value в классе Link), является полиморфной она может содержать значение любого типа.

Класс LinkedList обеспечивает: добавление элемента в список, проверку списка на наличие в нем элементов, доступ к первому элементу списка, удаление первого элемента списка.

Листинг 8.3. Классы Link и LinkedList

class Link

{

public Link (Object newValue, Link next)

{

valueField = newValue; nextLink = next;

}

public Object value ()

{return valueField; } public Link next ()

{return nextLink; } private Object valueField; private Link nextLink;

}

class LinkedList

{

public LinkedList ()

{firstLink = null; }

public void add (Object newValue)

{firstLink = new Link(newValue, firstLink); } public boolean empty ()

{return firstLink == null; }

public Object front ()

{

converted to PDF by BoJIoc

if (firstLink == null) return null;

return firstLink.value();

}

public void pop ()

{

if (firstLink != null) firstLink = firstLink.next();

}

public ListIterator iterator()

{ return new ListIterator (firstLink); } private Link firstLink;

}

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

необходимости их удаления и без знания детальной информации о внутренней структуре списка (в данном случае без сведений о классе Link). Как мы увидим в главе 16, такие возможности часто обеспечиваются разработчиками класса «список» через доступ к специальной разновидности объектов, называемых итераторами. Итератор скрывает

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

ListIterator itr = aList.iterator(); while (! Itr.atEnd() )

{

... do something list itr.current() ...

itr.next();

}

Обратите внимание на то, как сам список возвращает итератор в результате вызова

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

Листинг 8.4. Класс ListIterator

class ListIterator

{

public ListIterator (Link firstLink)

{

currentLink = firstLink;

}

public boolean atEnd ()

{

return currentLink == null;

}

public void next ()

{

if (currentLink != null) currentLink = currentLink.next();

}

public Object current ()

{

if (currentLink == null) return null;

return currentLink.value();

}

private Link currentLink:

}

converted to PDF by BoJIoc

8.3. Правила пасьянса

Версия пасьянса, которую мы будем описывать, известна под названием «Косынка» (или Klondike). Бесчисленные вариации этой игры делают ее, возможно, наиболее распространенной версией пасьянса, так что когда вы говорите слово «пасьянс», многие люди думают о «косынке». Версия, которую мы будем использовать здесь, описана в книге [Morehead 1949]. В упражнениях мы рассмотрим некоторые распространенные разновидности этого пасьянса.

Расположение карт показано на рис. 8.1. Используется одна стандартная колода из 52 карт. Расклад пасьянса (tableau) состоит из 28 карт в 7 стопках. Первая стопка состоит из 1 карты, вторая из 2 и т. д. до 7. Верхняя карта в каждой стопке изначально лежит картинкой вверх; все остальные картинкой вниз.

Рис. 8.1. Начальный расклад пасьянса

Стопки мастей (иногда называемые основаниями (foundations)) строятся от тузов до королей по мастям. Они создаются сверху расклада по мере того, как нужные карты становятся доступными. Цель игры сложить все 52 карты в основания по мастям.

Те карты, которые не выложены в стопки, изначально находятся в колоде (deck). Карты там лежат картинкой вниз, они достаются из колоды по одной и кладутся картинкой вверх в промежуточную стопку (discard pile). Оттуда они перемещаются на расклад или в основания. Карты достаются из колоды, пока она не опустеет. Игра заканчивается, если дальнейшие перемещения карт невозможны.

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

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

converted to PDF by BoJIoc

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

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

8.4. Стопки карт — наследование в действии

Значительная часть поведения, которое мы связываем со стопкой карт, является общим для всех типов стопок в игре. Например, каждая стопка содержит связный список карт; операции добавления и удаления элементов из этого связного списка тоже похожи. Другие операции, которым приписано поведение «по умолчанию» от класса CardPile, иногда переопределяются для разных подклассов. Класс CardPile показан в

листинге 8.5.

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

Три функции top(), pop() и empty(), манипулирующие списком карт, используют интерфейс, предоставляемый классом LinkedList. Новая карта добавляется в список путем вызова addCard(Card). Она модифицируется внутри подклассов. Обратите внимание: метод класса front() связного списка возвращает значение типа Object. Оно должно быть преобразовано к типу данных Card в функциях top() и pop().

Листинг 8.5. Описание класса CardPile

class CardPile

{CardPile (int x1,int y1)

{

x= x1; y = y1; cardFile = new LinkedList();

}

public Card top()

{

return (Card) cardList.front();

}

public boolean empty()

{

return cardList.empty();

}

public Card pop()

{

Card result = (Card) cardList.front(); cardList.pop();

return result;

}

//нижеследующие иногда переопределяются

public boolean includes (int tx, int ty)

{

return x <= tx && tx <= x + Card.width && y <= ty && ty <= y + Card.height;

}

public void select (int tx, ty) { } public void display (Graphics G)

{

converted to PDF by BoJIoc

g.setColor(Color.black); if (cardList.empty())

g.drawRect(x, y, Card.width, Card.height); else

top().draw(g, x, y);

}

public boolean canTake (Card aCard)

{return false; }

// координаты стопки карт protected int x; protected int y;

protected LinkedList cardList;

}

Оставшиеся пять операций являются типичными с точки зрения нашей абстракции стопки игральных карт. Однако они различаются в деталях в каждом отдельном случае. Например, функция canTake(Card) запрашивает, можно ли положить карту в данную стопку. Карта может быть добавлена к основанию, только если она следует по старшинству и имеет ту же масть, что и верхняя карта основания (или если карта туз, а стопка пуста). С другой стороны, карта может быть добавлена в стопку расклада, только если 1) цвет карты противоположен цвету текущей верхней карты в стопке и 2) карта имеет следующее по рангу младшее значение, чем верхняя карта в стопке или 3) стопка пуста, а карта является королем.

Действия пяти виртуальных функций, определенных в классе CardPile, могут быть охарактеризованы так:

includes

определяет, содержатся ли координаты, переданные в качестве аргументов, внутри границ стопки. Действие по умолчанию просто проверяет самую верхнюю карту стопки. Для стопки DeckPile это действие переопределено как проверка всех карт, содержащихся в стопке.

canTake

сообщает, можно ли положить данную карту в стопку. Только стопка DeckPile и основания SuitPile могут принимать карты, поэтому действие по умолчанию вернуть «нет». В двух вышеупомянутых классах стопок карт это действие переопределяется.

addCard

добавляет карту к списку карт (к стопке). Для промежуточной стопки карт DiscardPile это действие переопределяется так, чтобы гарантировать, что карта лежит картинкой вверх.

display

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

select

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

Следующая таблица иллюстрирует пользу наследования. Даны пять операторов и пять классов, так что имеется 25 потенциальных методов, которые мы должны были бы определить. Используя наследование, мы должны реализовать только 13 методов. Более