[x]. Закрытой, поскольку клиентам для выполнения их собственной разработки нужны службы модуля и, будучи один раз зафиксированы в некоторой его версии, они не должны изменяться при введении новых служб, в которых клиент не нуждается.
[x]. Открытой, так как нет никакой гарантии, что с самого начала в модуль были включены все службы, потенциально необходимые некоторому клиенту.
Эти два требования представляют дилемму, и классическая структура модулей не дает ключа к ее разгадке. Но наследование эту проблему решает. Класс закрыт, так как он может компилироваться, заноситься в библиотеку и использоваться классами-клиентами. Но он также открыт, поскольку любой новый класс может его использовать в качестве родителя, добавляя новые компоненты и меняя объявления некоторых унаследованных компонентов, при этом совершенно не нужно изменять исходный класс и беспокоить его клиентов. Это фундаментальное свойство при применении наследования к построению повторно используемого расширяемого ПО.
Одним из самых трудных вопросов, связанных с проектированием повторно используемых структур модулей, была необходимость использовать преимущества большой общности, которая может существовать у разных однотипных групп абстракций данных - у всех хеш-таблиц, всех последовательных таблиц и т. п. Используя структуры классов, связанных наследованием, можно получить выигрыш, зная логические соотношения между разными реализациями. Внизу на диаграмме представлен грубый и частичный набросок возможной структуры библиотеки для работы с таблицами. В этой схеме естественно используется множественное наследование, которое будет детально обсуждаться в следующей лекции.
Рис. 14.12. Набросок структуры библиотеки таблиц
Эта диаграмма наследования представляет только набросок, хотя на ней показаны типичные для этих структур связи по наследованию. Систематическую классификацию таблиц и других контейнеров, основанную на наследовании, см. в [M 1994a]. |
При таком взгляде требование повторного использования можно выразить весьма точно: идея состоит в том, чтобы передвинуть определение каждого компонента как можно выше в иерархии наследования так, чтобы он мог наследоваться максимально возможным числом классов-потомков. Можно представлять этот процесс как игру переиспользования, в которую играют на доске, представляющей иерархии наследования (такие, как на рис. 14.12), фигурами, представляющими компоненты. Выигрывает тот, кто сможет в результате открытия абстракций более высокого уровня передвинуть как можно больше компонентов как можно выше, и по пути, благодаря обнаружению общих свойств, сможет слить наибольшее число фигур.
Взгляд на класс как на тип
С точки зрения типов наследование адресуется и к повторному использованию, и к расширяемости, в частности, к тому, что в предыдущем обсуждении называлось непрерывностью. Здесь ключом является динамическое связывание.
Тип - это множество объектов, характеризуемых (как мы знаем из теории АТД) определенными операциями. INTEGER описывают множество целых чисел с арифметическими операциями, POLYGON - это множество объектов с операциями vertices, perimeter и другими.
Для типов наследование представляет отношение "является", например, во фразах "каждая собака является млекопитающим", "каждое млекопитающее является животным". Аналогично, прямоугольник является многоугольником.
Что означает это отношение?
[x]. Если рассматривать значения каждого типа, то это отношение является просто отношением включения множеств: собаки образуют подмножество множества животных, экземпляры класса RECTANGLE образуют подмножество экземпляров класса POLYGON. (Это следует из определения "экземпляра" в начале этой лекции, заметим, что прямой экземпляр класса RECTANGLE не является прямым экземпляром класса POLYGON).
[x]. Если рассматривать операции, применимые к каждому типу, то сказать, что B есть A, означает, что каждая операция, применимая к A применима также и к экземплярам B. (Однако при переопределении B может создать свою собственную реализацию, которая для экземпляров B заменит реализацию, предоставленную A.)
Используя это отношение можно описывать схемы отношения "является", представляющие многие варианты типов, например, все варианты класса FIGURE. Каждая новая версия таких подпрограмм как rotate и display определяется в классе, задающем соответствующий вариант типа. В случае таблиц, например, каждый класс на графе обеспечивает свою собственную реализацию операций search, insert, delete, разумеется, за исключением тех случаев, когда для него подходит реализация родителя.
Предостережение об использовании отношения "является" ("is a"). Начинающие - но я полагаю, ни один из читателей, добравшийся до этого места даже с минимумом внимания, - иногда путают наследование с отношением "экземпляр - образец", считая класс SAN_FRANCISCO наследником класса CITY. Это, как правило, ошибка: CITY - это класс, у которого может быть экземпляр, представляющий Сан Франциско. Чтобы избежать таких ошибок, достаточно помнить, что термин "является" означает не "x является одним из A" (например, "Сан Франциско является городом (CITY)), т.е. отношением между экземпляром и категорией, а выражает "всякий B является A" (например, "всякий ГОРОД является ГЕОГРАФИЧЕСКОЙ_ЕДИНИЦЕЙ"), т.е. отношение между двумя категориями, в программировании - двумя классами. Некоторые авторы предпочитают называть это отношение "является разновидностью" или "может действовать как" [Gore 1996]. Отчасти это дело вкуса (и частично этот предмет будет обсуждаться в лекции о методологии наследования), но поскольку мы уже знаем, как избежать тривиальной ошибки, то будем и далее использовать наиболее распространенное название "является", не забывая при этом, что оно относится к отношению между категориями.
Наследование и децентрализация
Имея динамическое связывание, можно создавать децентрализованные архитектуры ПО, необходимые для достижения целей повторного использования и расширяемости. Сравним ОО-подход, при котором самодостаточные классы предоставляют свои множества вариантов операций, с классическими подходами. В Паскале или Аде можно использовать тип записи с вариантами
type FIGURE =