35. Избегайте наследования от классов, которые не спроектированы для этой цели
35. Избегайте наследования от классов, которые не спроектированы для этой цели
Резюме
Классы, предназначенные для автономного использования, подчиняются правилам проектирования, отличным от правил для базовых классов (см. рекомендацию 32). Использование автономных классов в качестве базовых является серьезной ошибкой проектирования и его следует избегать. Для добавления специфического поведения предпочтительно вместо функций-членов добавлять обычные функции (см. рекомендацию 44). Для того чтобы добавить состояние, вместо наследования следует использовать композицию (см. рекомендацию 34). Избегайте наследования от конкретных базовых классов.
Обсуждение
Использование наследования там, где оно не требуется, подрывает доверие к мощи объектно-ориентированного программирования. В C++ при определении базового класса следует выполнить некоторые специфические действия (см. также рекомендации 32, 50 и 54), которые весьма сильно отличаются (а зачастую просто противоположны) от действий при разработке автономного класса. Наследование от автономного класса открывает ваш код для массы проблем, причем ваш компилятор в состоянии заметить только их малую часть.
Начинающие программисты зачастую выполняют наследование от классов-значений, таких как класс string (стандартный или иной) просто чтобы "добавить больше функциональности". Однако определение свободной функции (не являющейся членом) существенно превосходит создание класса super_string по следующим причинам.
• Свободные функции хорошо вписываются в существующий код, который работает с объектами string. Если же вместо этого вы предоставляете класс super_string, вам придется вносить изменения в ваш код, заменяя типы и сигнатуры функций.
• Функции интерфейса, которые получают параметры типа string, при использовании наследования должны сделать одно из трех: а) отказаться от дополнительной функциональности super_string (бесполезно), б) копировать свои аргументы в объекты super_string (расточительно) или в) преобразовать ссылки на string в ссылки на super_string (затруднительно и потенциально некорректно).
• Функции-члены super_string не должны получить больший доступ к внутреннему устройству класса string, чем свободные функции, поскольку класс string, вероятно, не имеет защищенных (protected) членов (вспомните — этот класс не предназначался для работы в качестве базового).
• Если класс super_string скрывает некоторые из функций класса string (а переопределение невиртуальных функций в производном классе не является перекрытием — это просто сокрытие), это может вызвать неразбериху в коде, работающем с объектами string, которые создаются автоматическим преобразованием из класса super_string.
Словом, лучше добавлять новую функциональность посредством новых свободных (не являющихся членами) функций (см. рекомендацию 44). Чтобы избежать проблем поиска имен, убедитесь, что вы поместили функции в то же пространство имен, что и тип, для расширения функциональности которого они предназначены (см. рекомендацию 57). Некоторые программисты не любят свободные функции из-за их синтаксиса Fun(str) вместо str.Fun(), но это не более чем вопрос привычки.
Но что если класс super_string наследуется из класса string для добавления состояний, таких как кодировка или кэшированное значение количества слов? Открытое наследование не рекомендуется и в этом случае, поскольку класс string не защищен от срезки (см. рекомендацию 54), и любое копирование super_string в string молча уберет все старательно хранимые дополнительные состояния.
И наконец, наследование класса с открытым невиртуальным деструктором рискует получить эффект неопределенного поведения при удалении указателя на объект типа string, который на самом деле указывает на объект типа super_string (см. рекомендацию 50). Это неопределенное поведение может даже оказаться вполне допустимым при использовании вашего компилятора и распределителя памяти, но оно все равно рано или поздно выявится в виде затаившихся ошибок, утечек памяти, разрушенной кучи и кошмаров переноса на другую платформу.
Примеры
Пример 1. Композиция вместо открытого или закрытого наследования. Что делать, если вам нужен тип lосаlized_string, который "почти такой же, как и string, но с дополнительными данными и функциями и небольшими переделками имеющихся функций-членов string", и при этом реализация многих функций остается неизменной? В этом случае реализуйте ее с помощью класса string, но не наследованием, а комбинированием (что предупредит срезку и неопределенное полиморфное удаление), и добавьте транзитные функции для того, чтобы сделать видимыми функции класса string, оставшиеся неизменными:
class localized_string {
public:
// ... Обеспечьте транзитные функции для тех
// функций-членов string, которые остаются неизменными
// (например, определите функцию insert, которая
// вызывает impl_.insert) ...
void clear(); // Маскирует/переопределяет clear()
bool is_in_klingon() const; // добавляет функциональность
private:
std::string impl_;
// ... дополнительные данные-члены ...
};
Конечно, писать транзитные функции для всех функций-членов, которые вы хотите сохранить, — занятие утомительное, но зато такая реализация существенно лучше и безопаснее, чем использование открытого или закрытого наследования.
Пример 2. std::unary_function. Хотя класс std::unary_function не имеет виртуальных функций, на самом деле он создан для использования в качестве базового класса и не противоречит рассматриваемой рекомендации. (Однако класс unary_function может быть усовершенствован добавлением защищенного деструктора — см. рекомендацию 50.)
Ссылки
[Dewhurst03] §70, §93 • [Meyers97] §33 • [Stroustrup00] §24.2-3, §25.2
Более 800 000 книг и аудиокниг! 📚
Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением
ПОЛУЧИТЬ ПОДАРОКЧитайте также
Принцип 5. Избегайте заблуждений, которые могут погубить продажи
Принцип 5. Избегайте заблуждений, которые могут погубить продажи Мы о многом судим неверно. Такова уж наша природа. Порой трудно увидеть истинные причины происходящих событий, к тому же часто ситуацию усугубляет наше нетерпение: «Я в курсе, можешь не продолжать. Давай
Проблема наследования
Проблема наследования Если существует необходимость наследовать от класса Singleton, то следует придерживаться определенных правил.Во-первых, класс-наследник должен переопределить метод Instance(), так, чтобы создавать экземпляр производного класса. Если не предполагается, что
Глава 8. Изучение наследования
Глава 8. Изучение наследования НаследованиеНаследованием (inheritance) называется такое отношение между классами, когда один класс использует часть структуры и/или поведения другого или нескольких классов. При наследовании создается иерархия абстракций, в которой подкласс
Правило 40: Продумывайте подход к использованию множественного наследования
Правило 40: Продумывайте подход к использованию множественного наследования Когда речь заходит о множественном наследовании (multiple inheritance – MI), сообщество разработчиков на C++ разделяется на два больших лагеря. Одни полагают, что раз одиночное исследование (SI) – это хорошо,
Второй принцип: поддержка наследования в C#
Второй принцип: поддержка наследования в C# Теперь, после исследования различных подходов, позволяющих создавать классы с хорошей инкапсуляцией, пришло время заняться построением семейств связанных классов. Как уже упоминалось, наследование является принципом ООП,
Запрет наследования: изолированные классы
Запрет наследования: изолированные классы Создавая отношения базовых классов и подклассов, вы получаете возможность использовать поведение существующих типов. Но что делать, когда нужно определить класс, не позволяющий получение подклассов? Например, предположим, что
Цепочка наследования типа Page
Цепочка наследования типа Page Как вы только что убедились, готовый генерируемый класс, представляющий файл *.aspx, получается из System.Web.UI.Page. Подобно любому базовому классу, этот тип обеспечивает полиморфный интерфейс всем производным типам. Однако тип Page является не
4.5.3. Функции, которые создают новые конфигурации из существующих 4.5.3.1. Функции геометрии, которые производят новые конфигурации
4.5.3. Функции, которые создают новые конфигурации из существующих 4.5.3.1. Функции геометрии, которые производят новые конфигурации Раздел "4.5.2. Функции Geometry" обсуждает несколько функций, которые создают новые конфигурации из
18.6. Пример множественного виртуального наследования A
18.6. Пример множественного виртуального наследования A Мы продемонстрируем определение и использование множественного виртуального наследования, реализовав иерархию шаблонов классов Array (см. раздел 2.4) на основе шаблона Array (см. главу 16), модифицированного так, чтобы он
19. Применение наследования в C++
19. Применение наследования в C++ При использовании наследования указатель или ссылка на тип базового класса способен адресовать объект любого производного от него класса. Возможность манипулировать такими указателями или ссылками независимо от фактического типа
2.2.4. Типы сущностей и иерархия наследования
2.2.4. Типы сущностей и иерархия наследования Как было указано выше, связи определяют, является ли сущность независимой или зависимой. Различают несколько типов зависимых сущностей.Характеристическая - зависимая дочерняя сущность, которая связана только с одной
Просмотр дерева наследования классов
Просмотр дерева наследования классов ClassView предоставляет очень интересную и полезную возможность просмотра дерева наследования классов приложения. Для этого выберите название интересующего вас класса из списка классов и откройте временное меню, нажав правую кнопку
Смысл наследования
Смысл наследования Мы уже рассмотрели основные способы наследования. Многое еще предстоит изучить, в частности, множественное наследование и детали того, что происходит с утверждениями в контексте наследования (понятие субконтрактов).Но вначале следует поразмышлять
Примеры множественного наследования
Примеры множественного наследования Множественное наследование это, по сути, прямое приложение уже рассмотренных принципов наследования, - класс вправе иметь произвольное число родителей. Однако, изучая этот вопрос более внимательно, можно обнаружить две интересные
Лекция 16. Техника наследования
Лекция 16. Техника наследования Наследование - ключевая составляющая ОО-подхода к повторному использованию и расширяемости. В этой лекции нам предстоит исследовать новые возможности, разнородные, но демонстрирующие замечательные следствия красоты базисных идей.
Глобальная структура наследования
Глобальная структура наследования Ранее мы уже ссылались на универсальные (universal) классы GENERAL и ANY, а также на безобъектный (objectless) класс NONE. Пришло время пояснить их роль и представить глобальную структуру