- •38 Наследование и проектирование
- •Глава 6 Наследование и объектно-ориентированное проектирование
- •Правило 32: Используйте открытое наследование для моделирования отношения «является»
- •Virtual void fly(); // птицы умеют летать
- •Virtual void healthValue() const; // возвращает жизненную силу персонажа
- •Реализация паттерна««Шаблонный метод» с помощью идиомы невиртуального интерфейса
- •Реализация паттерна «Стратегия» посредством указателей на функции
- •Реализация паттерна «Стратегия» посредством класса tr::function
- •Int operator()(const GameCharacter&) const // объектов, вычисляющих
- •«Классический» паттерн «Стратегия»
- •Void mf(); // скрывает b:mf; см. Правило 33
- •Virtual void onTick() const; // автоматически вызывается
- •Virtual void onTick() const; // просмотр данных об использовании
- •Что следует помнить
Правило 32: Используйте открытое наследование для моделирования отношения «является»
Вильям Демент (William Dement) в своей книге «Кто-то должен бодрствовать, пока остальные спят» (W. H. Freeman and Company, 1974) рассказывает о том, как он пытался донести до студентов наиболее важные идеи своего курса. Утверждается, говорил он своей группе, что средний британский школьник помнит из уроков истории лишь то, что битва при Хастингсе произошла в 1066 году. Даже если ученик почти ничего не запомнил из курса истории, подчеркивает Демент, 1066 год остается в его памяти. Демент пытался внушить слушателям несколько основных идей, в частности ту любопытную истину, что снотворное вызывает бессонницу. Он призывал своих студентов запомнить ряд ключевых фактов, даже если забудется все, что обсуждалось на протяжении курса, и в течение семестра возвращался к нескольким фундаментальным заповедям.
Последним на заключительном экзамене был вопрос: «Напишите, какой факт из тех, что обсуждались на лекциях, вы запомните на всю жизнь». Проверяя работы, Демент был ошеломлен. Почти все упомянули 1066 год.
Теперь я с трепетом хочу провозгласить, что самое важное правило в объектно-ориентированном программировании на C++ звучит так: открытое наследование означает «является». Твердо запомните это.
Если вы пишете класс D (derived – «производный») открыто наследует классу B («base» – «базовый»), то тем самым сообщаете компилятору C++ (а заодно и людям, читающим ваш код), что каждый объект типа D является также объектом типа B, но не наоборот. Вы говорите, что B представляет собой более общую концепцию, чем D, а D – более конкретную концепцию, чем B. Вы утверждаете, что везде, где может быть использован объект B, можно использовать также объект D, потому что D является объектом типа B. С другой стороны, если вам нужен объект типа D, то объект B не подойдет, поскольку каждый D «является разновидностью» B, но не наоборот.
Такой интерпретации открытого наследования придерживается C++. Рассмотрим следующий пример:
class Person {...};
class Student: public Person {...};
Здравый смысл и опыт подсказывают нам, что каждый студент – человек, но не каждый человек – студент. Именно такую связь подразумевает данная иерархия. Мы ожидаем, что всякое утверждение, справедливое для человека – например, что у него есть дата рождения, – справедливо и для студента, но не все, что верно для студента – например, что он учится в каком-то определенном институте, – верно для человека в общем случае.
Применительно к C++ это выглядит следующим образом: любая функция, которая принимает аргумент типа Person (или указатель на Person, или ссылку на Person), примет объект типа Student (или указатель на Student, или ссылку на Student):
void eat(const Person& p); // все люди могут есть
void study(const Student& s); // только студент учится
Person p; // p – человек
Student s; // s – студент
eat(p); // правильно, p есть человек
eat(s); // правильно, s – это студент,
// и студент также является человеком
study(s); // правильно
study(p); // ошибка! p – не студент
Все сказанное верно только для открытого наследования. C++ будет вести себя так, как описано выше, только в случае, если Student открыто наследует Person. Закрытое наследование означает нечто совсем иное (см. правило 39), а смысл защищенного наследования ускользает от меня по сей день.
Идея тождества открытого наследования и понятия «является» кажется достаточно очевидной, но иногда интуиция нас подводит. Рассмотрим следующий пример: пингвин – это птица, птицы умеют летать. Если вы по наивности попытаетесь выразить это на C++, то вот что получится:
class Bird {
public:
