Техника наследования
Наследование и скрытие информации
Последний вопрос, оставшийся пока без ответа, как наследование взаимодействует с принципом Скрытия информации.
В отношениях между классом и его клиентами скрытие информации определяет разработчик класса. Именно он определяет политику в отношении каждого компонента класса: экспортируя его всем клиентам, разрешая выборочный экспорт, или делая компонент закрытым.
Кое-что о политике
Что происходит со статусом экспорта при передаче компонента потомку? Наследование и скрытие информации - ортогональные механизмы. Наследование определяет отношение между классом и его потомками, экспорт - между классом и его клиентами. Класс B может свободно экспортировать или скрывать любой из компонентов f, унаследованных им от класса A. При этом доступны все возможные комбинации:
- f экспортируется в классе A и в классе B (хотя и не обязательно одним и тем же клиентам);
- f скрыто в A и B ;
- f скрыто в A, но полностью или частично экспортируется в B ;
- f экспортируется в A, но скрыто в B.
Правило гласит: по умолчанию f сохраняет тот статус экспорта, которым компонент был наделен в A. Однако его можно изменить, добавив предложение export в предложение наследования класса. Например:
class B inherit A export {NONE} f end -- Скрыть f (возможно, экспортируемый в классе A) ...
или
class B inherit A export {ANY} f end -- Экспортировать f (возможно, скрытый в классе A) ...
или
class B inherit A export {X, Y, Z} f end -- Сделать f доступным определенным классам ...
Применение
Характерным примером является создание нескольких вариантов одной абстракции.
Представим себе GENERAL_ACCOUNT - класс, содержащий все необходимые операции для работы с банковскими счетами: процедуры open, withdraw, deposit, code (для снятия денег через банкомат), change_code и т.д.,- но не предназначенный для использования клиентами напрямую, а потому не экспортирующий никаких подпрограмм. Его потомки выступают как разные облики родителя: они не содержат новых компонентов и отличаются лишь предложениями экспорта. Один экспортирует open и deposit, второй, наряду с ними, - withdraw и code, и т. д.
Эта схема в обсуждении методологии наследования (см. лекцию 6 курса "Основы объектно-ориентированного проектирования") носит название наследования функциональных возможностей (facility inheritance).
Понятие облика (view) является классическим в области баз данных, где необходимо дифференцировать пользователей, работающих с данными, предоставляя им разные права.
Другой пример касается классов, введенных, когда речь шла о множественном наследовании. Компонент right класса CELL скрыт в нем или, точнее говоря, экспортируется лишь классу LIST. Фактически так обстоят дела со всеми компонентами CELL, поскольку этот класс был изначально нацелен на работу со списками. Однако в дереве (классе TREE ), потомке как CELL, так и LIST, right теперь означает доступ к правому брату и является респектабельным членом общества экспортируемых компонентов.
Зачем нужна такая гибкость?
Стратегия экспорта, согласно которой каждый потомок класса имеет свою политику, хотя и усложняет проверку типов, но придает необходимую гибкость действиям разработчика.
Предпринимались и иные попытки. Так, отдельные объектные языки определяют не только то, будет ли компонент экспортирован клиентам класса, но и то, будет ли он доступен его потомкам. Преимущества этого подхода неочевидны. В частности:
- мне не известно о публикации рекомендаций по применению этой возможности, неясно, когда компонент должен передаваться потомкам, а когда быть скрытым. Конструкции языка, за которыми нет ни малейшей теории, имеют весьма сомнительную ценность. (Для сравнения: правило, посвященное методологии скрытия информации, совершенно прозрачно: то, что принадлежит АТД, и надлежит экспортировать; прочее следует скрыть.)
- механизмы ограничения порожденных классов, введенные в языке Simula и др., редко используются разработчиками.
При близком рассмотрении отсутствие ясных методологических установок не удивляет. Наследование является воплощением принципа Открыт-Закрыт: позволяя выбрать готовый класс, написанный вами или другим программистом вчера или 20 лет назад, и обнаружить, что с ним можно делать нечто полезное, что даже не предполагалось при его проектировании.
Позволить создателю класса определять, что могут и что не могут использовать потомки класса, - значит лишиться основного свойства наследования.
Пример классов CELL и TREE характерен: при разработке CELL его целью была лишь поддержка работоспособности LIST, а потому right и put_right служили в нем исключительно внутренним целям. И лишь позднее этим компонентам нашли новое применение в классе-потомке TREE. Не будь этой открытости, наследование почти полностью утратило бы свой шарм.
Если нет основы для принятия решения об экспорте компонентов потомкам, то еще более абсурдно пытаться догадаться, что потомки могут экспортировать своим клиентам. Единственная задача разработчика порожденного класса - предоставление своим клиентам как можно более удобного для них класса. Наследование - это лишь средство, которое позволяет быстрее добиться желаемого результата. Все правила ОО-игры определяются утверждениями и ограничениями типизации, - не более того. Найти полезный для клиентов потомка компонент предка - это большая удача, ну а то, как поступал предок с этим компонентом, - экспортировал ли он его, это дело предка и волнует потомка меньше всего.
В итоге, единственной стратегией, сочетающейся с принципиальной открытостью наследования, нам кажется та, что была описана выше: предоставить каждому разработчику возможность самостоятельно решать, что делать с компонентами предка, выбирая собственную политику экспорта в интересах своих клиентов.
Интерфейс и повторное использование реализаций
Знакомясь с объектным подходом по другим источникам, вы могли видеть в них предостережения использования "наследования реализаций". Однако в нем нет ничего плохого.
Повторное использование имеет две формы: использование интерфейсов и использование реализаций. Любой класс - это реализация (возможно, частичная) АТД. Он содержит как интерфейс, выражающий спецификацию АТД и образующий лишь "вершину айсберга", так и набор решений, определяющих реализацию. Повторное использование интерфейса означает согласие со спецификацией, повторное использование реализации - ваше согласие положиться на свойства класса, а не только на АТД.
Совместно для одних и тех же целей эти две возможности не применяются. Если вы хотите получить некоторое множество возможностей только через их абстрактные свойства и хотите быть защищенными от будущих изменений реализации, выбирайте повторное использование интерфейсов. Но в некоторых случаях вам может понравиться определенная реализация, поскольку она обеспечивает нужную основу вашего решения.
Эти формы повторного использования взаимно дополняют друг друга и обе совершенно законны.
По сути, их воплощением являются два вида межмодульных отношений, имеющих место при ОО-проектировании программ: клиент обеспечивает повторное использование интерфейсов, наследование поддерживает повторное использование реализаций.
Повторно используя реализацию, вы безусловно принимаете более ответственное решение, так как не можете рассчитывать на неизменность реализации в перспективе. По этой причине, став наследником класса, вы свяжете себя более сильными обязательствами.
Слово в защиту реализаций
В чем же причина недоверия к наследованию реализаций? Я пришел к выводу, что ответ лежит в области психологии. Тридцатилетний программистский опыт оставил нам лишь сомнения насчет самой идеи реализаций. И даже слово "реализация" приобрело в отдельных кругах почти неприличный характер. По этой причине мы ведем речь о проектировании и анализе, а если и упоминаем реализацию, то начинаем разговор с "но", "лишь" или "только".
Объектная технология в корне меняет все: ОО-реализации настолько элегантны, полезны, с ясно выраженной корректностью, что уже можно забыть об неприятных оттенках этого слова в языке. Для многих из нас программа часто оказывается вещью наиболее абстрактной, дает описание на самом высоком уровне и наиболее понимаема, чем большая часть того, что в анализе и проектировании провозглашается "величайшим достижением мысли".
Два стиля
Ряд основных различий между понятиями, о которых шла речь, мы представили в виде таблицы.
Итак, есть два отношения - "быть потомком" и "быть клиентом"; две формы повторного использования - интерфейсов и реализаций; скрытие информации и его отсутствие; защита от изменений в поставляемых модулях и отсутствие таковой.
Наличие альтернатив в любом случае не вносит противоречий, и в зависимости от контекста каждый из вариантов вполне оправдан. Отважимся на смелый шаг и сведем эти противоположности в одно целое:
Клиент | Потомок |
---|---|
Повторное использование интерфейсов Информация скрывается Исходная реализация защищена |
Повторное использование реализаций Информация не скрывается Исходная реализация не защищена |
Возможно, есть и другие подходы к решению этой проблемы, но я не знаю ни одного столь же простого, доступного и практичного.
Выборочный экспорт
Говоря о наследовании и скрытии информации, нельзя обойти вопрос о выборочном экспорте компонентов. Класс A, выборочно экспортирующий f классу B:
class A feature {B, ...} f... ...
делает f доступным в реализации собственных компонентов B. Потомки B, в свою очередь, имеют доступ к реализации предка, а потому они должны быть вправе обращаться ко всем доступным B возможностям, в том числе, к f.
Практические наблюдения подтверждают это теоретическое обоснование. Все, что необходимо классу, обычно требуется и его потомкам. Однако нам не хотелось бы с появлением очередного порожденного класса B возвращаться в A и расширять его предложение экспорта.
Согласно принципу Скрытия информации, а также принципу Открыт-Закрыт, разработчику A дано право решать, делать ли f доступным для B, однако, ему запрещено ограничивать свободу разработчика B. Тем самым, имеет место правило:
Правило наследования при выборочном экспорте
Выборочно экспортированный компонент доступен как самому классу, так и всем его потомкам.