Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Программирование на C / C++ / C++ for real programmers.pdf
Скачиваний:
262
Добавлен:
02.05.2014
Размер:
2.04 Mб
Скачать

Множественная

10

 

передача

 

Когда речь заходит об иерархии классов, сразу же хочется разразиться трескучей речью об объектноориентированном дизайне. Однако я справлюсь с искушением и ограничусь лишь той частью темы, которая развивает потенциал С++, а именно гомоморфными иерархиями. За длинным термином прячется простая концепция — иерархия классов с одинаковым открытым интерфейсом, унаследованным от общего базового класса. Суть проста, но возможности огромны.

Немедленно возникает первый вопрос: как выполнять передачу вызовов функций, когда единственное, что вам известно об аргументах, — все они происходят от некоторого общего предка? «Силовое» решение с конструкцией switch/case, нередко встречающееся в реальных программах, обычно удается заменить намного более элегантной, быстрой и простой в сопровождении архитектурой, известной под названием множественной передачи (multiple dispatch).

В этой и следующей главе мы временно отвлечемся от навязчивой темы — указателей. Поклонники указателей, не отчаивайтесь! В главе 12 гомоморфизм и умные указатели объединятся в новой разновидности умных указателей… настолько умных, что вы даже не будете подозревать об их присутствии.

Гомоморфные иерархии классов

Во главе гомоморфной иерархии классов всегда стоит абстрактный базовый класс, который определяет открытый интерфейс своих предков. Из чисто сентиментальных побуждений я назову этот класспредок «дедушкой» (Grandpa). Как правило, Grandpa является чисто абстрактным классом — то есть он не содержит ни одной переменной, а все его функции являются виртуальными.

class Grandpa {

public: // Закрытые и защищенные члены отсутствуют virtual void Fn1() = 0;

virtual void Fn2(int) = 0;

};

Разумеется, классу Grandpa не нужны конструкторы. Наличие чисто виртуальных членов гарантирует, что экземпляры Grandpa непосредственно никогда создаваться не будут. Для чисто абстрактных базовых классов я иногда использую другой, неформальный термин — «класс-пенсионер». Вероятно, такие классы делали что-то полезное на ранних стадиях цикла разработки, но теперь они служат всего лишь для абстрактного объединения семейства.

Это еще не обеспечивает гомоморфизма. Все зависит от того, как от Grandpa порождаются новые классы. Гомоморфными по отношению к Grandpa называются производные классы, в которые не добавляется никаких открытых членов. Они могут иметь любые закрытые и защищенные члены, но только не новые открытые.

class Dad : public Grandpa { private:

// О чем папа никогда не рассказывал

160

protected:

//Что было у папы, но не было у дедушки

public:

virtual void Fn1(); virtual void Fn2(int);

};

class AuntMartha : public Grandpa { private:

//Личная жизнь тетушки Марты

protected:

// То, что она передала моим кузенам public:

virtual void Fn1(); virtual void Fn2(int);

};

Иерархию можно продолжить и дальше (например, class Me : public Dad) при условии, что в открытый интерфейс не добавляется новых функций.

Мы рассмотрим множество примеров, демонстрирующих полезность гомоморфных иерархий, но сначала я приведу тройной аргумент в пользу этой концепции.

Взаимозаменяемость производных классов

Тот, кто пришел на объектно-ориентированную вечеринку раньше других, смог войти в историю. Некто Лисков заработал себе имя на следующей идее: если клиент имеет дело с базовым классом, его не должно интересовать, какой из производных классов на самом деле выполняет работу. Вы должны иметь возможность подставить экземпляр любого производного класса вместо экземпляра любого другого производного класса; клиенты базового класса просто пожимают плечами и продолжают работать так, словно ничего не произошло. Это называется «подстановочным критерием Лискова». Знатоки объектно-ориентированного программирования обычно сходятся на том, что это — Хорошая Мысль.

Строго говоря, данный критерий можно выполнить и без гомоморфизма. Если производный класс содержит дополнительные открытые функции, их можно просто не вызывать из клиента базового класса. Постойте-ка… а зачем добавлять открытые функции, если их не использовать? Если в одном производном классе были добавлены одни скрытые функции, а в другом — другие, со временем в вашей программе наверняка отыщется точка, в которой их нельзя свободно поменять местами.

Настоящая опасность заключается втом, чо без выполнения этого критерия клиентам придется думать о производных классах, а не только о базовом классе, который они знают и любят. Если бы в Dad присутствовали дополнительные открытые члены, клиента Grandpa со временем мог бы спросить свой объект: «Долой притворство — что ты представляешь собой в действительности?» В итоге было бы нарушено столько принципов модульного строения и инкапсуляции, что об этом можно было бы написать целую книгу.

Самый простой способ обеспечить взаимозаменяемость — воспользоваться гомоморфизмом. По крайней мере, для интерфейсов гомоморфизм обеспечивает взаимозаменяемость по определению, поскольку клиентам Grandpa не придется беспокоиться о существовании других функций, с которыми им положено работать.

Нормальное наследование

Класс Grandpa может содержать не чисто виртуальные функции и переменные и при всем этом обеспечивать полную взаимозаменяемость. Тем не менее, совпадение интерфейсов еще не означает взаимозаменяемости объектов. Приходится учитывать действие вторичных эффектов. Предположим, функция Fn1() класса Grandpa не является чисто виртуальной:

void Grandpa::Fn1()

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 { ... };

Инкапсуляция производных классов — одно из редких проявлений истинного просветления программиста; верный признак того, что автор программы хорошо разбирается в том, что он делает.