Анимация
JavaScript
|
Главная Библионтека 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() Код, вызывающий вторичные эффекты 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 { ... }; Инкапсуляция производных классов - одно из редких проявлений истинного просветления программиста; верный признак того, что автор программы хорошо разбирается в том, что он делает. Чтобы усилить эффект, закрытые классы можно объявить статистическими и тем самым ограничить их пространтсво имен исходным файлом, в котором они находятся. С инкапсулированными производными классами связаны определенные проблемы. Например, как создать экземпляры таких классов, как 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-таблицами. 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 [ 49 ] 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |