Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ
Шрифт:
Ясно, что закрытое наследование не означает «является». А что же тогда оно означает?
«Стоп! – восклицаете вы. – Прежде чем говорить о значении, давайте поговорим о поведении. Как ведет себя закрытое наследование?» Первое из правил, регламентирующих закрытое наследование, вы только что наблюдали в действии: в противоположность открытому наследованию компиляторы в общем случае не преобразуют объекты производного класса (такие как Student) в объекты базового класса (такие как Person). Вот почему вызов eat для объекта s ошибочен. Второе правило состоит в том, что члены, наследуемые от закрытого базового класса, становятся закрытыми, даже если в базовом классе они были объявлены как защищенные или открытые.
Это то, что касается поведения. А теперь вернемся к значению. Закрытое наследование означает «реализовано посредством…». Делая класс D закрытым наследником класса B, вы поступаете так потому, что заинтересованы в использовании некоторого когда, уже написанного для B, а не потому, что между объектами B и D существует некая концептуальная взаимосвязь. Таким образом, закрытое наследование – это исключительно прием реализации. (Вот почему все унаследованное от закрытого базового класса становится закрытым и в вашем классе: это не более чем деталь реализации). Используя терминологию из правила 34, можно сказать, что закрытое наследование означает наследование одной только реализации, без интерфейса. Если D закрыто наследует B, это означает, что объекты D реализованы посредством объектов B, и ничего больше. Закрытое наследование ничего не означает в ходе проектирования программного обеспечения и обретает смысл только на этапе реализации.
Утверждение, что закрытое наследование означает «реализован посредством», вероятно, слегка вас озадачит, поскольку в правиле 38 указывалось, что композиция может означать то же самое. Как же сделать выбор между ними? Ответ прост: используйте композицию, когда можете, а закрытое наследование – когда обязаны так поступить. А в каких случаях вы обязаны использовать закрытое наследование? В первую очередь тогда, когда на сцене появляются защищенные члены и/или виртуальные функции, хотя существуют также пограничные ситуации, когда соображения экономии памяти могут продиктовать выбор в пользу закрытого наследования.
Предположим, что вы работаете над приложением, в котором есть объекты класса Widget, и решили как следует разобраться с тем, как они используются. Например, интересно не только знать, насколько часто вызываются функции-члены Widget, но еще и как частота обращений к ним изменяется во времени. Программы, в которых есть несколько разных фаз исполнения, могут вести себя по-разному в каждой фазе. Например, функции, используемые компилятором на этапе синтаксического анализа, значительно отличаются от функций, вызываемых во время оптимизации и генерации кода.
Мы решаем модифицировать класс Widget так, чтобы отслеживать, сколько раз вызывалась каждая функция-член. Во время исполнения мы будем периодически считывать эту информацию, возможно, вместе со значениями каждого объекта Widget и другими данными, которые сочтем необходимым. Для этого понадобится установить таймер, который будет извещать нас о том, когда наступает время собирать статистику использования.
Предпочитая повторное использование существующего кода написанию нового, мы тщательно просмотрим наш набор инструментов и найдем следующий класс:
Это как раз то, что мы искали. Объект Timer можно настроить для срабатывания с любой частотой, и при каждом «тике» будет вызываться виртуальная функция. Мы можем переопределить эту виртуальную функцию так, чтобы она проверяла текущее состояние Widget. Отлично!
Для того чтобы класс Widget переопределял виртуальную функцию Timer, он должен наследовать Timer. Но открытое наследование в данном случае не подходит. Ведь Widget не является разновидностью Timer. Пользователи Widget не должны иметь возможности вызывать onTick для объекта Widget, потому что эта функция не является частью концептуального интерфейса этого класса. Если разрешить вызов подобной функции, то пользователи получат возможность работать с интерфейсом Widget некорректно, что очевидно нарушает рекомендацию из правила 18 о том, что интерфейсы должно быть легко применять правильно и трудно – неправильно. Открытое наследование в данном случае не подходит.
Потому мы будем наследовать закрыто:
Благодаря закрытому наследованию открытая функция onTick класса Timer становится закрытой в Widget, и после переопределения мы ее такой и оставим. Опять же, если поместить onTick в секцию public, то это введет в заблуждение пользователей, заставляя их думать, будто ее можно вызвать, а это идет вразрез с правилом 18.
Это неплохое решение, но стоит отметить, что закрытое наследование не является здесь строго необходимым. Никто не мешает вместо него использовать композицию. Мы просто объявим закрытый вложенный класс внутри Widget, который будет открыто наследовать классу Timer и переопределять onTick, а затем поместим объект этого типа внутрь Widget. Вот эскиз такого подхода: