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

converted to PDF by BoJIoc

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

Среда разработки также обеспечивает средства низкого уровня. Например, среда LAF предоставляет графические примитивы общего назначения, возможность редактирования текста и т. д.

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

Упражнения

1.Постройте в среде LAF простое приложение с тремя типами кнопок.

2.Какие три группы методов можно выделить в типичном приложении среды? Приведите примеры методов каждой категории для класса application в среде LAF.

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

Глава 20

Новый взгляд на классы

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

Что же в точности представляет собой класс? Ответ на этот вопрос зависит, в частности, от того, какой язык программирования вы рассматриваете. В широком смысле есть две основные школы. Одни языки, такие как C++ или Object Pascal, рассматривают класс как тип данных, подобный целым числам или записям. Другие языки, такие как Smalltalk или Objective-C, считают, что класс это объект. В следующих двух разделах мы рассмотрим некоторые вариации этих двух основных точек зрения.

20.1. Классы как типы

Чтобы понять смысл высказывания «классы суть типы», мы прежде всего должны попытаться уяснить значение термина тип в языках программирования. К сожалению, понятие типа данных используется для очень многих целей. Следующий список, взятый из [Wegner 1986], иллюстрирует некоторые ответы на вопрос «Что такое тип данных?»:

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

converted to PDF by BoJIoc

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

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

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

Верификационная точка зрения. Типы данных определяют инварианты поведения, которым удовлетворяют экземпляры.

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

Точка зрения реализации. Типы определяют способы распределения памяти для значений.

Приведенный выше список не претендует на полноту. Однако с него можно начать обсуждение классов как типов данных.

Такие языки программирования, как C++ и Object Pascal, рассматривают классы как обобщение структуры или записи. Класс также определяет поля, и каждый экземпляр класса содержит свои собственные значения полей. В отличие от записи класс имеет поля нового типа, которые представляют собой функции или процедуры (в отличие от полей данных, имеется только одна копия такого поля, совместно используемая всеми экземплярами класса).

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

защиты неструктурированных битов в памяти компьютера от прямых манипуляций со стороны программиста.

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

20.1.1. Как наследование усложняет понятие типа

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

converted to PDF by BoJIoc

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

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

1 Верифицирование специальная техника разработки программ, подробно описанная в [Gries 1981]. При таком подходе в программу добавляются специальные условия, оформленные в виде псевдокомментариев. Выполнение условий считается необходимым для правильной работы программы. Они используются (явно или неявно) программистом при разработке и отладке

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

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

Пример: пасьянс

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

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

converted to PDF by BoJIoc

Рис. 20.1. Правильное и искаженное отображение стопки карт

описанным нами в главе 7, делают такую замену легкой для программирования. Все, что требуется это создать новый класс (например, ParallelCard), который наследует все из класса Card и переопределяет только метод draw. Описание такого класса показано ниже. Затем программист может сменить ссылки на класс Card в инициализирующей части программы, чтобы использовать вместо него класс ParallelCard.

class ParallelCard : public Card

{

// наследует все полностью, но изменяет рисование public:

void draw()

{ if (!fork()) // порождаем процесс

{ Card::draw(); exit(0); }

}

};

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

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

Данный пример иллюстрирует проблемы, возникающие с появлением наследования. Исходная программа была разработана и тщательно протестирована, и, возможно, некий программист поддастся соблазну думать, что до тех пор, пока выдерживается критерий «быть экземпляром», ранее проведенное тестирование будет оставаться справедливым. К сожалению, данное предположение в общем случае несправедливо, и любое изменение должно подвергаться регрессионному тестированию [Perry 1990].

converted to PDF by BoJIoc

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

Объектно-ориентированный язык программирования Eiffel [Meyer 1988a, Rist 1995] по крайней мере частично решает эту проблему. К методам присоединяются так называемые условия. Они наследуются и не могут переопределяться в подклассах (хотя могут дополняться). Компилятор генерирует код, проверяющий выполнение этих условий во время выполнения программы. Тем самым минимальный уровень функциональности метода гарантируется независимо от любых возможных переопределений. Конечно, иногда затруднительно сформулировать некоторые условия такие, как утверждение о том, что игральная карта полностью нарисована перед выходом из метода.

Заметим, что Java предлагает интересный вариант решения этой проблемы: использование модификатора final. Класс, который объявлен как final, не может порождать подклассы, и тем самым гарантируется сохранение его функциональности.

20.1.2. Наследование и память

Наконец рассмотрим связь между типами данных и управлением памятью. Здесь тоже наследование приводит к тонким нюансам, которые отсутствуют для традиционных типов данных (таких, как записи и массивы). Поскольку с точки зрения «классы суть записи»

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

или при присваивании обрезались поля подкласса, которые не помещаются в области памяти адресата;

или память для переменной должна выделяться из «кучи», но не из стека.

Суммируя, заключаем, что понятие класса почти точно соответствует большинству наших интуитивных представлений о типах, но совпадение не является полным. Второй взгляд на вещи — «классы суть объекты» — устраняет некоторые проблемы, вообще обходя стороной дискуссию о типах данных.

20.2. Классы как объекты

Мы подчеркивали выше, что основная философия объектно-ориентированного программирования делегирование полномочий индивидуальному объекту. Он

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

converted to PDF by BoJIoc

Рис. 20.2. CRC-карточка для класса Class

Рис. 20.3. Связь экземпляров и связь подклассов

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

20.2.1. Фабрики по созданию объектов

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