34. Предпочитайте композицию наследованию
34. Предпочитайте композицию наследованию
Резюме
Избегайте "налога на наследство": наследование — вторая по силе после отношения дружбы взаимосвязь, которую можно выразить в С++. Сильные связи нежелательны, и их следует избегать везде, где только можно. Таким образом, следует предпочитать композицию наследованию, кроме случаев, когда вы точно знаете, что делаете и какие преимущества дает наследование в вашем проекте.
Обсуждение
Наследованием часто злоупотребляют даже опытные разработчики. Главное правило в разработке программного обеспечения — снижение связности. Если взаимоотношение можно выразить несколькими способами, используйте самую слабую из возможных взаимосвязей.
Известно, что наследование — практически самое сильное взаимоотношение, которое можно выразить средствами С++; сильнее его только отношение дружбы, и пользоваться им следует только при отсутствии функционально эквивалентной более слабой альтернативы. Если вы можете выразить отношения классов с использованием только лишь композиции, следует использовать этот способ.
В данном контексте "композиция" означает простое использование некоторого типа в виде переменной-члена в другом типе. В этом случае вы можете хранить и использовать объект таким образом, который обеспечивает вам контроль над степенью взаимосвязи.
Композиция имеет важные преимущества над наследованием.
• Большая гибкость без влияния на вызывающий код: закрытые члены-данные находятся под полным вашим контролем. Вы можете хранить их по значению, посредством (интеллектуального) указателя или с использованием идиомы Pimpl (см. рекомендацию 43), при этом переход от одного способа хранения к другому никак не влияет на код вызывающей функции: все, что при этом меняется, — это реализация функций-членов класса, использующих упомянутые члены-данные. Если вы решите, что вам требуется иная функциональность, вы можете легко изменить тип или способ хранения члена при полной сохранности открытого интерфейса. Если же вы начнете с открытого наследования, то скорее всего вы не сможете легко и просто изменить ваш базовый класс в случае необходимости (см. рекомендацию 37).
• Большая обособленность в процессе компиляции, уменьшение времени компиляции. Хранение объекта посредством указателя (предпочтительно — интеллектуального указателя), а не в виде непосредственного члена или базового класса позволяет также снизить зависимости заголовочных файлов, поскольку объявление указателя на объект не требует полного определения класса этого объекта. Наследование, напротив, всегда требует видимости полного определения базового класса. Распространенная методика состоит в том, чтобы собрать все закрытые члены воедино посредством одного непрозрачного указателя (идиома Pimpl, см. рекомендацию 43).
• Меньше странностей. Наследование от некоторого типа может вызвать проведение поиска имен среди функций и шаблонов функций, определенных в том же пространстве имен, что и упомянутый тип. Этот тонкий момент с трудом поддается отладке (см. также рекомендацию 58).
• Большая применимость. Не все классы проектируются с учетом того, что они будут выступать в роли базовых (см. рекомендацию 35). Однако большинство классов вполне могут справиться с ролью члена.
• Большая надежность и безопасность. Более сильное связывание путем наследования затрудняет написание безопасного в смысле ошибок кода (см. [Sutter02] §23).
• Меньшая сложность и хрупкость. Наследование приводит к дополнительным усложнениям, таким как сокрытие имен и другим, возникающим при внесении изменений в базовый класс.
Конечно, это все не аргументы против наследования как такового. Наследование предоставляет программисту большие возможности, включая заменимость и/или возможность перекрытия виртуальных функций (см. рекомендации с 36 по 39 и подраздел исключений данной рекомендации). Но не платите за то, что вам не нужно; если вы можете обойтись без наследования, вам незачем мириться с его недостатками.
Исключения
Используйте открытое наследование для моделирования заменимости (см. рекомендацию 37).
Даже если от вас не требуется предоставление отношения заменимости вызывающим функциям, вам может понадобиться закрытое или защищенное наследование в перечисленных далее ситуациях (мы постарались хотя бы грубо отсортировать их в порядке уменьшения распространенности).
• Если вам требуется перекрытие виртуальной функции.
• Если вам нужен доступ к защищенному члену.
• Если вам надо создавать объект до используемого, а уничтожать — после, сделайте его базовым классом.
• Если вам приходится заботиться о виртуальных базовых классах.
• Если вы знаете, что получите выгоду от оптимизации пустого базового класса и что в вашем случае она будет выполнена используемым вами компилятором (см. рекомендацию 8).
• Если вам требуется управляемый полиморфизм, т.е. отношение заменимости, которое должно быть видимо только определенному коду (посредством дружбы).
Ссылки
[Cargill92] pp. 49-65, 101-105 • [Cline99] §5.9-10, 8.11-12, 37.04 • [Dewhurst03] §95 • [Lakos96] §1.7, §6.3.1 • [McConnell93] §5 • [Meyers97] §40 • [Stroustrup00] §24.2-3 • [Sutter00] §22-24, §26-30 • [Sutter02] §23
Более 800 000 книг и аудиокниг! 📚
Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением
ПОЛУЧИТЬ ПОДАРОКЧитайте также
14. Предпочитайте ошибки компиляции и компоновки ошибкам времени выполнения
14. Предпочитайте ошибки компиляции и компоновки ошибкам времени выполнения РезюмеНе стоит откладывать до выполнения программы выявление ошибок, которые можно обнаружить при ее сборке. Предпочтительно писать код, который использует компилятор для проверки инвариантов
28. Предпочитайте канонический вид ++ и --, и вызов префиксных операторов
28. Предпочитайте канонический вид ++ и --, и вызов префиксных операторов РезюмеОсобенность операторов инкремента и декремента состоит в том, что у них есть префиксная и постфиксная формы с немного отличающейся семантикой. Определяйте операторы operator++ и operator-- так, чтобы они
33. Предпочитайте минимальные классы монолитным
33. Предпочитайте минимальные классы монолитным РезюмеРазделяй и властвуй: небольшие классы легче писать, тестировать и использовать. Они также применимы в большем количестве ситуаций. Предпочитайте такие небольшие классы, которые воплощают простые концепции, классам,
36. Предпочитайте предоставление абстрактных интерфейсов
36. Предпочитайте предоставление абстрактных интерфейсов РезюмеВы любите абстракционизм? Абстрактные интерфейсы помогают вам сосредоточиться на проблемах правильного абстрагирования, не вдаваясь в детали реализации или управления состояниями. Предпочтительно
44. Предпочитайте функции, которые не являются ни членами, ни друзьями
44. Предпочитайте функции, которые не являются ни членами, ни друзьями РезюмеТам, где это возможно, предпочтительно делать функции не членами и не друзьями классов.ОбсуждениеФункции, не являющиеся членами или друзьями классов, повышают степень инкапсуляции путем
48. В конструкторах предпочитайте инициализацию присваиванию
48. В конструкторах предпочитайте инициализацию присваиванию РезюмеВ конструкторах использование инициализации вместо присваивания для установки значений переменных-членов предохраняет от ненужной работы времени выполнения при том же объеме вводимого исходного
55. Предпочитайте канонический вид присваивания
55. Предпочитайте канонический вид присваивания РезюмеПри реализации оператора operator= предпочитайте использовать канонический вид — невиртуальный с определенной сигнатурой.ОбсуждениеПредпочтительно объявлять копирующее присваивание для типа T с одной из следующих
80. Предпочитайте push_back другим способам расширения последовательности
80. Предпочитайте push_back другим способам расширения последовательности РезюмеИспользуйте push_back везде, где это возможно. Если для вас не важна позиция вставки нового объекта, лучше всего использовать для добавления элемента в последовательность функцию push_back. Все прочие
81. Предпочитайте операции с диапазонами операциям с отдельными элементами
81. Предпочитайте операции с диапазонами операциям с отдельными элементами РезюмеПри добавлении элементов в контейнер лучше использовать операции с диапазонами (т.е. функцию insert, которая получает пару итераторов), а не последовательность вызовов функции для вставки
84. Предпочитайте вызовы алгоритмов самостоятельно разрабатываемым циклам
84. Предпочитайте вызовы алгоритмов самостоятельно разрабатываемым циклам РезюмеРазумно используйте функциональные объекты. В очень простых случаях написанные самостоятельно циклы могут оказаться более простым и эффективным решением. Тем не менее, вызов алгоритма
Правило 2: Предпочитайте const, enum и inline использованию #define
Правило 2: Предпочитайте const, enum и inline использованию #define Это правило лучше было бы назвать «Компилятор предпочтительнее препроцессора», поскольку #define зачастую вообще не относят к языку C++. В этом и заключается проблема. Рассмотрим простой пример; попробуйте написать
Правило 20: Предпочитайте передачу по ссылке на const передаче по значению
Правило 20: Предпочитайте передачу по ссылке на const передаче по значению По умолчанию в C++ объекты передаются в функции и возвращаются функциями по значению (свойство, унаследованное от C). Если не указано противное, параметры функции инициализируются копиями реальных
Правило 23: Предпочитайте функциям-членам функции, не являющиеся ни членами, ни друзьями класса
Правило 23: Предпочитайте функциям-членам функции, не являющиеся ни членами, ни друзьями класса Возьмем класс для представления Web-браузера. В числе прочих такой класс может предлагать функции, который очищают кэш загруженных элементов, очищают историю посещенных URL и