std::unary_function
не имеет виртуальных функций, на самом деле он создан для использования в качестве базового класса и не противоречит рассматриваемой рекомендации. (Однако класс unary_function
может быть усовершенствован добавлением защищенного деструктора — см. рекомендацию 50.)
36. Предпочитайте предоставление абстрактных интерфейсов
Вы любите абстракционизм? Абстрактные интерфейсы помогают вам сосредоточиться на проблемах правильного абстрагирования, не вдаваясь в детали реализации или управления состояниями. Предпочтительно проектировать иерархии, реализующие абстрактные интерфейсы, которые моделируют абстрактные концепции.
Предпочитайте определять абстрактные интерфейсы и выполнять наследование от них. Абстрактный интерфейс представляет собой абстрактный класс, составленный полностью из (чисто) виртуальных функций и не обладающий состояниями (членами-данными), причем обычно без реализации функций- членов. Заметим, что отсутствие состояний в абстрактном интерфейсе упрощает дизайн всей иерархии (см. соответствующие примеры в [Meyers96]).
Желательно следовать принципу инверсии зависимостей (Dependency Inversion Principle, DIP; см. [Martin96a] и [Martin00]). Данный принцип состоит в следующем.
• Высокоуровневые модули не должны зависеть от низкоуровневых. И те, и другие должны зависеть от абстракций.
• Абстракции не должны зависеть от деталей; вместо этого детали должны зависеть от абстракций.
Из DIP следует, что корнями иерархий должны быть абстрактные классы, в то время как конкретные классы в этой роли выступать не должны (см. рекомендацию 35). Абстрактные базовые классы должны беспокоиться об определении функциональности, но не о ее реализации.
Принцип инверсии зависимостей имеет три фундаментальных преимущества при проектировании.
•
•
•
Закон Второго Шанса гласит: 'Самая важная вещь — интерфейс. Все остальное можно подправить и позже, но если интерфейс разработан неверно, то может оказаться так, что у вас уже не будет второго шанса его исправить' [Sutter04].
Обычно для того, чтобы обеспечить полиморфное удаление, при проектировании отдается предпочтение открытому виртуальному деструктору (см. рекомендацию 50), если только вы не используете брокер объектов (наподобие СОМ или CORBA), который использует альтернативный механизм управления памятью.
Будьте осторожны при использовании множественного наследования классов, которые не являются абстрактными интерфейсами. Дизайн с использованием множественного наследования может быть очень выразительным, но он труднее в разработке и более подвержен ошибкам. В частности, особенно сложным при использовании множественного наследования становится управление состояниями.
Как указывалось в рекомендации 34, наследование от некоторого типа может привести и к проблемам поиска имен, поскольку в таком случае в поиске участвуют функции из пространства имен наследуемого типа (см. также рекомендацию 58).
Если же логика системы резервного копирования построена на основе тщательно спроектированных абстракций файловой системы и устройства резервного копирования, то переработка не потребуется — достаточно будет добавить в систему новую реализацию абстрактного интерфейса. Естественное решение подобной проблемы состоит в том, что новые требования должны удовлетворяться новым кодом, но старый код при этом изменяться не должен.
Оптимизация пустого базового класса представляет собой один из примеров, когда наследование (предпочтительно не открытое) используется для сугубо оптимизационных целей (но см. рекомендацию 8).
Может показаться, что в дизайне на основе стратегий высокоуровневые компоненты зависят от деталей реализации (стратегий). Однако это всего лишь использование статического полиморфизма. Здесь имеются абстрактные интерфейсы, просто они не указаны явно посредством чисто виртуальных функций.
37. Открытое наследование означает заменимость. Наследовать надо не для повторного использования, а чтобы быть повторно использованным
Открытое наследование позволяет указателю или ссылке на базовый класс в действительности обращаться к объекту некоторого производного класса без изменения существующего кода и нарушения его корректности.
Не применяйте открытое наследование для того, чтобы повторно использовать код (находящийся в базовом классе); открытое наследование необходимо для того, чтобы быть повторно использованным (существующим кодом, который полиморфно использует объекты базового класса).