161
{
//
Код, вызывающий вторичные эффекты
}
void Dad::Fn1()
{
//
Код, вызывающий другие вторичные эффекты
}
void AuntMartha::Fn1()
{
Grandpa::Fn1();
//
Прочее
}
Клиент
Grandpa
может полагаться на вторичные эффекты этого класса. Знаю, знаю, инкапсуляция и
все такое, на вторичные эффекты полагаться никогда не следует… но давайте спустимся на землю.
Функции, которые мы вызываем, выполняют различные действия — скажем, рисуют на экране,
создают объекты или записывают информацию в файле. Без этих вторичных эффектов толку от них
будет немного. Если
Grandpa
обладает некоторыми встроенными вторичными эффектами, клиенты
Grandpa
могут с полным правом надеяться, что эти эффекты сохранятся во всех производных классах.
Но вот
Dad
усомнился в авторитете
Grandpa
и в своем переопределении
Fn1()
не потрудился вызвать
Grandpa::Fn1()
. Вторичные эффекты
Grandpa::Fn1()
пропадают. Рано или поздно это начнет
беспокоить клиента
Grandpa
, которые, возможно, ждал от
Dad
совсем иного. А вот
AuntMartha
в
свом переопеределении вызывает
Grandpa::Fn1()
и потому сохраняет все вторичные эффекты
Grandpa::Fn1()
. Теперь
AuntMartha
может выполнять любые дополнительные действия в пределах
разумного — клиентов
Grandpa
это совершенно не интересует.
Если переопределенная функция вызывает версию базового класса, говорят, что она нормально
наследуется от этой функции. Не важно, где находится этот вызов — в начале, в конце или середине
переопределенной функции. Важно лишь то, что в какой-то момент он все же происходит. Если все
переопределенные функции производного класса наследуются нормально, говорят, что весь класс
наследуется нормально. Если все производные классы гомоморфного базового класса наследуются
нормально и ни один из них не обладает особо вопиющими вторичными эффектами, их можно
подставлять вместо друг друга.
Самый простой способ обеспечить взаимозаменяемость — сделать все функции
Grandpa
чисто
виртуальными. Это вырожденный случай нормального наследования; если функция базового класса
является чисто виртуальной, то все ее вторичные эффекты (которых на самом деле нет) сохраняются по
определению.
Инкапсуляция производных классов
Мы все еще не рассмотрели всех причин размещения чисто абстрактного базового класса во главе
иерархии. Взаимозаменяемость и нормальное наследование можно обеспечить как с переменными и
невиртуальными функциями в
Grandpa
, так и с виртуальными функциями, которые нормально
наследуются производными классами. Зачем настаивать, чтобы
Grandpa
был чисто виртуальным
базовым классом? Ответ состоит всего из одного слова: инкапсуляция. Если клиент имеет дело только с
чисто абстрактным базовым классом, содержащим только открытые функции, он получает абсолютный
минимум информации, необходимой для использования класса. Все остальное (в том числе и сами
производные классы) может быть спрятано от чужих глаз в файле .cpp.
// В файле .h
class Grandpa { ... };
// В файле(-ах) .cpp
class Dad : public Grandpa { ... };
class AuntMartha : public Grandpa { ... };
Инкапсуляция производных классов — одно из редких проявлений истинного просветления
программиста; верный признак того, что автор программы хорошо разбирается в том, что он делает.
162
Чтобы усилить эффект, закрытые классы можно объявить статистическими и тем самым ограничить их
пространтсво имен исходным файлом, в котором они находятся.
С инкапсулированными производными классами связаны определенные проблемы. Например, как
создать экземпляры таких классов, как
Dad
, которые не видны клиенту из файла .h? Эти проблемы
легко решаются с помощью идиом, описанных в двух следующих главах. А пока мы продолжим
публиковать производные классы в файле .h, зная, что существует возможность их полной
инкапсуляции.
Множественная передача
Самый распространенный пример гомоморфной иерархии — набор классов, соответствующих
различным видам чисел: целым, комплексным, вещественным и т.д. Класс-предок такой иерархии
может называться
Number
и иметь интерфейс следующего вида:
class Number {
public:
virtual Number operator+(const Number&) = 0;
virtual Number operator-(const Number&) = 0;
//
И т.д.
};
class Integer : public Number {
private:
int
i;
public:
Integer(int x) : i(x) {}
virtual Number operator+(const Number&);
//
И т.д.
};
На
бумаге
все
выглядит
проще,
чем
в
действительности.
Как
реализовать
Integer::operator+(Number&)
, если нам не известно, что в скобках находится вовсе не
Number
, а
некоторый производный класс? Для каждой пары типов, участвующих в сложении, существует свой
алгоритм. Суммирование
Complex + Integer
отличается от
Integer + Real
, которое, в свою
очередь, отличается от
Integer + ArbitraryPrecisionNumber
. Как программе разобраться, какой
из алгоритмов следует использовать? Что-что? Кто сказал: «Запросить у аргумента оператора + его
настоящий тип»? Немедленно встаньте в угол.
class Number {
protected:
int
type;
//
Хранит информацию о настоящем типе
int TypeOf() { return type; }
//
И т.д.
};
// Где-то в программе
switch (type) {
case kInteger: ...
case kComplex: ...
}
Именно этого знания типов мы постараемся избежать. Кроме того, все прямые реализации подобных
схем не отличаются особой элегантностью. Вы когда-нибудь видели код, генерируемый компилятором
для конструкции
switch/case
? Ни красоты, ни эффективности. Вместо этого мы объединим знания
компилятора о типах с чудесами современной технологии — v-таблицами.
Do'stlaringiz bilan baham: |