Герб Саттер - Стандарты программирования на С++. 101 правило и рекомендация
В этом разделе мы считаем самой важной рекомендацию 33 — "Предпочитайте минимальные классы монолитным".
32. Ясно представляйте, какой вид класса вы создаете
Существует большое количество различных видов классов, и следует знать, какой именно класс вы создаете.
ОбсуждениеРазличные виды классов служат для различных целей и, таким образом, следуют различным правилам.
Классы-значения (например, std::pair, std::vector) моделируют встроенные типы. Эти классы обладают следующими свойствами.
• Имеют открытые деструктор, копирующий конструктор и присваивание с семантикой значения.
• Не имеют виртуальных функций (включая деструктор).
• Предназначены для использования в качестве конкретных классов, но не в качестве базовых (см. рекомендацию 35).
• Обычно размещаются в стеке или являются непосредственными членами другого класса.
Базовые классы представляют собой строительные блоки иерархии классов. Базовый класс обладает следующими свойствами.
• Имеет деструктор, который является либо открытым и виртуальным, либо защищенным и невиртуальным (см. рекомендацию 50), а также копирующий конструктор и оператор присваивания, не являющиеся открытыми (см. рекомендацию 53).
• Определяет интерфейс посредством виртуальных функций.
• Обычно объекты такого класса создаются динамически в куче как часть объекта производного класса и используются посредством (интеллектуальных) указателей.
Говоря упрощенно, классы свойств представляют собой шаблоны, которые несут информацию о типах. Класс свойств обладает следующими характеристиками.
• Содержит только операторы typedef и статические функции. Класс не имеет модифицируемого состояния или виртуальных функций.
• Обычно объекты данного класса не создаются (конструкторы могут быть заблокированы).
Классы стратегий (обычно шаблоны) являются фрагментами сменного поведения. Классы стратегий обладают следующими свойствами.
• Могут иметь состояния и виртуальные функции, но могут и не иметь их.
• Обычно объекты данного класса не создаются, и он выступает в качестве базового класса или члена другого класса.
Классы исключений представляют собой необычную смесь семантики значений и ссылок. При генерации исключений они передаются по значению, но должны перехватываться по ссылке (см. рекомендацию 73). Классы исключений обладают следующими свойствами.
• Имеют открытый деструктор и конструкторы, не генерирующие исключений (в особенности копирующий конструктор, генерация исключения в котором приводит к завершению работы программы).
• Имеют виртуальные функции и часто реализуют клонирование (см. рекомендацию 54).
• Предпочтительно делать их производными от std::exception.
Вспомогательные классы обычно поддерживают отдельные идиомы (например, RAII — см. рекомендацию 13). Важно, чтобы их корректное использование не было сопряжено с какими-либо трудностями и наоборот — чтобы применять их некорректно было очень трудно (например, см. рекомендацию 53).
Ссылки[Abrahams01b] • [Alexandrescu00a] • [Alexandrescu00b] • [Alexandrescu01] §3 • [Meyers96] §13 • [Stroustrup00] §8.3.2, §10.3, §14.4.6, §25.1 • [Vandevoorde03] §15
33. Предпочитайте минимальные классы монолитным
Разделяй и властвуй: небольшие классы легче писать, тестировать и использовать. Они также применимы в большем количестве ситуаций. Предпочитайте такие небольшие классы, которые воплощают простые концепции, классам, пытающимся реализовать как несколько концепций, так и сложные концепции (см. рекомендации 5 и 6).
ОбсуждениеРазработка больших причудливых классов — типичная ошибка новичка в объектно-ориентированном проектировании. Перспектива иметь класс, который предоставляет полную и сложную функциональность "в одном флаконе", может оказаться очень привлекательной. Однако подход, состоящий в разработке небольших, минимальных классов, которые легко комбинировать, на практике по ряду причин оказывается более успешен для систем любого размера и сложности.
• Минимальный класс воплощает одну концепцию на соответствующем уровне детализации. Монолитный класс обычно включает несколько отдельных концепций, и использование только одной из них влечет за собой излишние накладные расходы (см. рекомендации 5 и 11).
• Минимальный класс легче понять и проще использовать (в том числе повторно).
• Минимальный класс проще в употреблении. Монолитный класс часто должен использоваться как большое неделимое целое. Например, монолитный класс Matrix может попытаться реализовать и использовать экзотическую функциональность — такую как вычисление собственных значений матрицы — даже если большинству пользователей этого класса требуются всего лишь азы линейной алгебры. Лучшим вариантом будет реализация различных функциональных областей в виде функций, не являющихся членами, которые работают с минимальным типом Matrix. Тогда эти функциональные области могут быть протестированы и использованы отдельно только теми пользователями, кто в них нуждается (см. рекомендацию 44).
• Монолитные классы снижают инкапсуляцию. Если класс имеет много функций-членов, которые не обязаны быть членами, но тем не менее являются таковыми (таким образом обеспечивается излишняя видимость закрытой реализации), то закрытые члены-данные класса становятся почти столь же плохими с точки зрения дизайна, как и открытые переменные.
• Монолитные классы обычно являются результатом попыток предсказать и предоставить "полное" решение некоторой проблемы; на практике же такие действия почти никогда не приводят к успешному результату.
• Монолитные классы сложнее сделать корректными и безопасными в связи с тем, что при их разработке зачастую нарушается принцип "Один объект — одна задача" (см. рекомендации 5 и 44).
Ссылки[Cargill92] pp. 85-86, 152, 174-177 • [Lakos96] §0.2.1-2, §1.8, §8.1-2 • [Meyers97] §18 • [Stroustrup00] §16.2.2, §23.4.3.2, §24.4.3 • [Sutter04] §37-40
34. Предпочитайте композицию наследованию
Избегайте "налога на наследство": наследование — вторая по силе после отношения дружбы взаимосвязь, которую можно выразить в С++. Сильные связи нежелательны, и их следует избегать везде, где только можно. Таким образом, следует предпочитать композицию наследованию, кроме случаев, когда вы точно знаете, что делаете и какие преимущества дает наследование в вашем проекте.
ОбсуждениеНаследованием часто злоупотребляют даже опытные разработчики. Главное правило в разработке программного обеспечения — снижение связности. Если взаимоотношение можно выразить несколькими способами, используйте самую слабую из возможных взаимосвязей.
Известно, что наследование — практически самое сильное взаимоотношение, которое можно выразить средствами С++; сильнее его только отношение дружбы, и пользоваться им следует только при отсутствии функционально эквивалентной более слабой альтернативы. Если вы можете выразить отношения классов с использованием только лишь композиции, следует использовать этот способ.
В данном контексте "композиция" означает простое использование некоторого типа в виде переменной-члена в другом типе. В этом случае вы можете хранить и использовать объект таким образом, который обеспечивает вам контроль над степенью взаимосвязи.
Композиция имеет важные преимущества над наследованием.
• Большая гибкость без влияния на вызывающий код: закрытые члены-данные находятся под полным вашим контролем. Вы можете хранить их по значению, посредством (интеллектуального) указателя или с использованием идиомы Pimpl (см. рекомендацию 43), при этом переход от одного способа хранения к другому никак не влияет на код вызывающей функции: все, что при этом меняется, — это реализация функций-членов класса, использующих упомянутые члены-данные. Если вы решите, что вам требуется иная функциональность, вы можете легко изменить тип или способ хранения члена при полной сохранности открытого интерфейса. Если же вы начнете с открытого наследования, то скорее всего вы не сможете легко и просто изменить ваш базовый класс в случае необходимости (см. рекомендацию 37).
• Большая обособленность в процессе компиляции, уменьшение времени компиляции. Хранение объекта посредством указателя (предпочтительно — интеллектуального указателя), а не в виде непосредственного члена или базового класса позволяет также снизить зависимости заголовочных файлов, поскольку объявление указателя на объект не требует полного определения класса этого объекта. Наследование, напротив, всегда требует видимости полного определения базового класса. Распространенная методика состоит в том, чтобы собрать все закрытые члены воедино посредством одного непрозрачного указателя (идиома Pimpl, см. рекомендацию 43).