Стандарты программирования на С++. 101 правило и рекомендация
Шрифт:
Обычно для того, чтобы обеспечить полиморфное удаление, при проектировании отдается предпочтение открытому виртуальному деструктору (см. рекомендацию 50), если только вы не используете брокер объектов (наподобие СОМ или CORBA), который использует альтернативный механизм управления памятью.
Будьте осторожны при использовании множественного наследования классов, которые не являются абстрактными интерфейсами. Дизайн с использованием множественного наследования может быть очень выразительным, но он труднее в разработке и более подвержен ошибкам. В частности, особенно сложным при использовании множественного наследования становится управление состояниями.
Как указывалось в рекомендации 34, наследование от некоторого типа может привести и к проблемам
Пример. Программа резервного копирования. При прямом, наивном проектировании высокоуровневые компоненты зависят от низкоуровневых деталей, например, плохо спроектированная программа резервного копирования может иметь архивирующий компонент, который непосредственно зависит от типов или подпрограмм для чтения структуры каталогов, и других, которые записывают данные на ленту. Адаптация такой программы к новой файловой системе и аппаратному обеспечению для резервного копирования потребует существенной переработки проекта.
Если же логика системы резервного копирования построена на основе тщательно спроектированных абстракций файловой системы и устройства резервного копирования, то переработка не потребуется — достаточно будет добавить в систему новую реализацию абстрактного интерфейса. Естественное решение подобной проблемы состоит в том, что новые требования должны удовлетворяться новым кодом, но старый код при этом изменяться не должен.
Оптимизация пустого базового класса представляет собой один из примеров, когда наследование (предпочтительно не открытое) используется для сугубо оптимизационных целей (но см. рекомендацию 8).
Может показаться, что в дизайне на основе стратегий высокоуровневые компоненты зависят от деталей реализации (стратегий). Однако это всего лишь использование статического полиморфизма. Здесь имеются абстрактные интерфейсы, просто они не указаны явно посредством чисто виртуальных функций.
[Alexandrescu01] • [Cargill92] pp. 12-15, 215-218 • [Cline99] §5.18-20, 21.13 • [Lakos96] §6.4.1 • [Martin96a] • [Martin00] • [Meyers96] §33 • [Stroustrup00] §12.3-4, §23.4.3.2, §23.4.3.5, §24.2-3, §25.3, §25.6 • [Sutter04] §17
37. Открытое наследование означает заменимость. Наследовать надо не для повторного использования, а чтобы быть повторно использованным
Открытое наследование позволяет указателю или ссылке на базовый класс в действительности обращаться к объекту некоторого производного класса без изменения существующего кода и нарушения его корректности.
Не применяйте открытое наследование для того, чтобы повторно использовать код (находящийся в базовом классе); открытое наследование необходимо для того, чтобы быть повторно использованным (существующим кодом, который полиморфно использует объекты базового класса).
Несмотря на двадцатилетнюю историю объектно-ориентированного проектирования, цель и практика открытого наследования часто понимается неверно, и многие применения наследования оказываются некорректными.
Открытое наследование в соответствии с принципом подстановки Лисков (Liskov Substitution Principle [Liskov88]) всегда должно моделировать отношение "является" ("работает как"): все контракты базового класса должны быть выполнены, для чего все перекрытия виртуальных функций-членов не должны требовать большего или обещать меньше, чем их базовые версии. Код, использующий указатель или ссылку на
Неверное использование наследования нарушает корректность. Обычно некорректно реализованное наследование не подчиняется явным
Наиболее часто в этой связи упоминается следующий пример. Рассмотрим два класса —
Описание "является" для открытого наследования оказывается неверно понятым при использовании аналогий из реального мира: квадрат "является" прямоугольником в математическом смысле, но с точки зрения поведения
Открытое наследование действительно связано с повторным использованием, но опять же не так, как привыкло думать множество программистов. Как уже указывалось, цель открытого наследования — в реализации заменимости (см. [Liskov88]). Цель отрытого наследования не в том, чтобы производный класс мог повторно использовать код базового класса для того, чтобы с его помощью реализовать свою функциональность. Такое отношение "реализован посредством" вполне корректно, но должно быть смоделировано при помощи композиции или, только в отдельных случаях, при помощи закрытого или защищенного наследования (см. рекомендацию 34).
Вот еще одно соображение по поводу рассматриваемой темы. При корректности и целесообразности динамического полиморфизма композиция "эгоистична" в отличие от "щедрого" наследования. Поясним эту мысль.
Новый производный класс представляет собой частный случай существующей более общей абстракции. Существующий (динамический) полиморфный код, который использует
Новые требования, естественно, должны удовлетворяться новым кодом, но они не должны требовать внесения изменений в существующий код (см. рекомендацию 36).
До объектно-ориентированного программирования было легко решить вопрос вызова старого кода новым. Открытое наследование упростило прозрачный и безопасный вызов нового кода старым (так что применяйте шаблоны, предоставляющие возможности статического полиморфизма, который хорошо совместим с полиморфизмом динамическим — см. рекомендацию 64).