Два стиля
Ряд основных различий между понятиями, о которых шла речь, мы представили в виде таблицы.
Итак, есть два отношения - "быть потомком" и "быть клиентом"; две формы повторного использования - интерфейсов и реализаций; скрытие информации и его отсутствие; защита от изменений в поставляемых модулях и отсутствие таковой.
Наличие альтернатив в любом случае не вносит противоречий, и в зависимости от контекста каждый из вариантов вполне оправдан. Отважимся на смелый шаг и сведем эти противоположности в одно целое:
Клиент | Потомок |
---|---|
Таблица 16.1.Слияние четырех противоположностей
Возможно, есть и другие подходы к решению этой проблемы, но я не знаю ни одного столь же простого, доступного и практичного.
Выборочный экспорт
Говоря о наследовании и скрытии информации, нельзя обойти вопрос о выборочном экспорте компонентов. Класс A, выборочно экспортирующий f классу B:
class A feature {B, ...}
f...
...
делает f доступным в реализации собственных компонентов B. Потомки B, в свою очередь, имеют доступ к реализации предка, а потому они должны быть вправе обращаться ко всем доступным B возможностям, в том числе, к f.
Практические наблюдения подтверждают это теоретическое обоснование. Все, что необходимо классу, обычно требуется и его потомкам. Однако нам не хотелось бы с появлением очередного порожденного класса B возвращаться в A и расширять его предложение экспорта.
Согласно принципу Скрытия информации, а также принципу Открыт-Закрыт, разработчику A дано право решать, делать ли f доступным для B, однако, ему запрещено ограничивать свободу разработчика B. Тем самым, имеет место правило:
Правило наследования при выборочном экспорте
Выборочно экспортированный компонент доступен как самому классу, так и всем его потомкам.
Ключевые концепции
[x]. К инварианту класса автоматически добавляются инварианты его родителей.
[x]. В подходе Проектирования по Контракту наследование, переопределение и динамическое связывание приводят к идее субподрядов.
[x]. Повторное объявление подпрограммы (переопределение или создание реализации) может сохранить или ослабить предусловие, сохранить или усилить постусловие.
[x]. Повторное объявление утверждений может использовать только require else (при объединении с предусловием связкой "или") и ensure then (при объединении с постусловием связкой "и"). Применение require/ensure запрещено. В отсутствие названных предложений подпрограмма сохраняет исходные утверждения.
[x]. Универсальный класс GENERAL и допускающий настройку его наследник обеспечивают переопределяемые компоненты, представляющие общий интерес для всех создаваемых разработчиком классов. Класс NONE замыкает решетку наследования снизу.
[x]. Заморозив компонент, можно гарантировать его вечную семантическую уникальность.
[x]. Ограниченная универсальность дает возможность использовать только родовые параметры со специфическими свойствами.
[x]. Попытка присваивания позволяет динамически проверить, принадлежит ли объект ожидаемому типу. Эта операция не должна использоваться как замена динамического связывания.
[x]. Потомок вправе переопределять тип любой сущности (атрибута, результата функции, формального параметра подпрограммы). Повторное определение должно быть ковариантным - заменять исходные типы соответствующими, согласуясь с требованиями потомка.
[x]. Закрепленные объявления (like anchor) - это важная часть системы типов, облегчающая применение ковариантной типизации и позволяющая отказаться от избыточных повторных объявлений.
[x]. Наследование и скрытие информации - это независимые механизмы. Потомки могут скрывать экспортированные компоненты и экспортировать скрытые компоненты.
[x]. Компонент, доступный самому классу, доступен и его потомкам.
Библиографические замечания
Иную точку зрения на взаимосвязь наследования и скрытия информации см. в [Snyder 1986].
Упражнения
У16.1 Наследование: простота и эффективность
Перепишите и упростите ранее созданную реализацию защищенного стека, сделав класс STACK3 потомком, а не клиентом STACK, чтобы избежать излишних обходных путей. (Подсказка: см. правила взаимодействия наследования и скрытия информации.)
У16.2 Векторы
Напишите класс VECTOR, представляющий числовые вектора (кольцо) с обычными математическими операциями. Сам класс рекурсивно должен относиться к численному типу, допуская вектора векторов. Возможно, для этого вам придется самостоятельно дописать класс NUMERIC (или воспользоваться готовым из [M 1994a]).
У16.3 Экстракт?
В случае, когда x1 имеет тип X, y1 имеет тип Y, и Y является потомком X, оператор y1 := x1 будет недопустимым. Однако полезным мог бы показаться универсальный компонент extract, такой, что y1.extract (x1) копирует значения полей объекта x1 в соответствующие поля объекта y1 при условии, что ни в одной из этих ссылок не содержится Void.
Объясните, почему компонент extract стоит отвергнуть. (Подсказка: обратитесь к вопросам корректности, в частности, к понятию инварианта.) Выясните, можно ли спроектировать удовлетворительный механизм, решающий эту задачу каким-то иным способом.
Лекция 17. Типизация
Эффективное применение объектной технологии требует четкого описания в тексте системы типов всех объектов, с которыми она работает на этапе выполнения. Это правило, известное как статическая типизация (static typing), делает наше ПО: более надежным, позволяя компилятору и другим инструментальным средствам устранять несоответствия прежде, чем они смогут нанести вред; более понятным, обеспечивая точной информацией читателей: авторов клиентских систем и тех, кто будет сопровождать систему; более эффективным, поскольку информация о типах данных позволит компилятору сгенерировать оптимальный код. Хотя вопросами типизации данных активно занимались и вне объектной среды, да и сама статическая типизация применяется в языках, не поддерживающих ООП, особенно ярко эти идеи проявили себя именно при объектном подходе, во многом основанном на понятии типа, которое, сливаясь с понятием модуля, образует базовую ОО-конструкцию - класс.