Но что если класс 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
36. Предпочитайте предоставление абстрактных интерфейсов
Резюме
Вы любите абстракционизм? Абстрактные интерфейсы помогают вам сосредоточиться на проблемах правильного абстрагирования, не вдаваясь в детали реализации или управления состояниями. Предпочтительно проектировать иерархии, реализующие абстрактные интерфейсы, которые моделируют абстрактные концепции.
Обсуждение
Предпочитайте определять абстрактные интерфейсы и выполнять наследование от них. Абстрактный интерфейс представляет собой абстрактный класс, составленный полностью из (чисто) виртуальных функций и не обладающий состояниями (членами-данными), причем обычно без реализации функций-членов. Заметим, что отсутствие состояний в абстрактном интерфейсе упрощает дизайн всей иерархии (см. соответствующие примеры в [Meyers96]).
Желательно следовать принципу инверсии зависимостей (Dependency Inversion Principle, DIP; см. [Martin96a] и [Martin00]). Данный принцип состоит в следующем.
• Высокоуровневые модули не должны зависеть от низкоуровневых. И те, и другие должны зависеть от абстракций.
• Абстракции не должны зависеть от деталей; вместо этого детали должны зависеть от абстракций.
Из DIP следует, что корнями иерархий должны быть абстрактные классы, в то время как конкретные классы в этой роли выступать не должны (см. рекомендацию 35). Абстрактные базовые классы должны беспокоиться об определении функциональности, но не о ее реализации.
Принцип инверсии зависимостей имеет три фундаментальных преимущества при проектировании.
• Повышение надежности. Менее стабильные части системы (реализации) зависят от более стабильных частей (абстракций). Надежный дизайн — тот, в котором воздействие изменений ограничено. При плохом проектировании небольшие изменения в одном месте расходятся кругами по всему проекту и оказывают влияние на самые неожиданные части системы. Именно это происходит, когда проект строится на конкретных базовых классах.
Читать дальше