Предположим, что вы работаете над приложением, в котором есть объекты класса Widget, и решили как следует разобраться с тем, как они используются. Например, интересно не только знать, насколько часто вызываются функции-члены Widget, но еще и как частота обращений к ним изменяется во времени. Программы, в которых есть несколько разных фаз исполнения, могут вести себя по-разному в каждой фазе. Например, функции, используемые компилятором на этапе синтаксического анализа, значительно отличаются от функций, вызываемых во время оптимизации и генерации кода.
Мы решаем модифицировать класс Widget так, чтобы отслеживать, сколько раз вызывалась каждая функция-член. Во время исполнения мы будем периодически считывать эту информацию, возможно, вместе со значениями каждого объекта Widget и другими данными, которые сочтем необходимым. Для этого понадобится установить таймер, который будет извещать нас о том, когда наступает время собирать статистику использования.
Предпочитая повторное использование существующего кода написанию нового, мы тщательно просмотрим наш набор инструментов и найдем следующий класс:
class Timer {
public:
explicit Timer(int tickFrequency);
virtual void onTick() const; // автоматически вызывается
// при каждом тике
...
};
Это как раз то, что мы искали. Объект Timer можно настроить для срабатывания с любой частотой, и при каждом «тике» будет вызываться виртуальная функция. Мы можем переопределить эту виртуальную функцию так, чтобы она проверяла текущее состояние Widget. Отлично!
Для того чтобы класс Widget переопределял виртуальную функцию Timer, он должен наследовать Timer. Но открытое наследование в данном случае не подходит. Ведь Widget не является разновидностью Timer. Пользователи Widget не должны иметь возможности вызывать onTick для объекта Widget, потому что эта функция не является частью концептуального интерфейса этого класса. Если разрешить вызов подобной функции, то пользователи получат возможность работать с интерфейсом Widget некорректно, что очевидно нарушает рекомендацию из правила 18 о том, что интерфейсы должно быть легко применять правильно и трудно – неправильно. Открытое наследование в данном случае не подходит.
Потому мы будем наследовать закрыто:
class Widget: private Timer {
private:
virtual void onTick() const; // просмотр данных об использовании
... // Widget и т. п.
};
Благодаря закрытому наследованию открытая функция onTick класса Timer становится закрытой в Widget, и после переопределения мы ее такой и оставим. Опять же, если поместить onTick в секцию public, то это введет в заблуждение пользователей, заставляя их думать, будто ее можно вызвать, а это идет вразрез с правилом 18.
Это неплохое решение, но стоит отметить, что закрытое наследование не является здесь строго необходимым. Никто не мешает вместо него использовать композицию. Мы просто объявим закрытый вложенный класс внутри Widget, который будет открыто наследовать классу Timer и переопределять onTick, а затем поместим объект этого типа внутрь Widget. Вот эскиз такого подхода:
class Widget {
private:
class WidgetTimer: public Timer {
public:
virtual void onTick() const;
...
};
WidgetTimer timer;
...
};
Этот дизайн сложнее того, что использует только закрытое наследование, потому что здесь используются и открытое наследование, и композиция, а ко всему еще и новый класс (WidgetTimer). Честно говоря, я показал этот вариант в первую очередь для того, чтобы напомнить о существовании различных подходов к решению одной задачи. Стоит привыкать к тому, чтобы не ограничиваться единственным решением (см. также правило 35). Тем не менее я могу представить две причины, по которым иногда имеет смысл предпочесть открытое наследование в сочетании с композицией закрытому наследованию.
Во-первых, вы можете спроектировать класс Widget так, чтобы ему можно было наследовать, но при этом запретить производным классам переопределять функцию onTick. Если Widget наследуется от Timer, то это невозможно, даже в случае закрытого наследования. (Напомню, что согласно правилу 35 производные классы могут переопределять виртуальные функции, даже если не могут вызывать их). Но если WidgetTimer – это закрытый класс внутри Widget, который наследует Timer, то производные от Widget классы не имеют доступа к WidgetTimer, а значит, не могут ни наследовать ему, ни переопределять его виртуальные функции. Если вам приходилось программировать на языках Java или C# и вы не обратили внимания на то, как можно запретить производным классам переопределять функции базового (с помощью ключевого слова final в Java или sealed в C#), то теперь вы знаете, как добиться примерно того же эффекта в C++.