А. Александреску - Современное проектирование на C++ (1119444), страница 62
Текст из файла (страница 62)
Следовательно, для его успешного применения нужны тщательная реализация и строгая дисциплина, В главе описывается обобщенная реализация шаблона ч1з1тог, оставляющая прикладному программисту очень мало работы. В главе обсуждаются следующие темы. е Принцип работы шаблона згзз1тог. ° Когда следует применять шаблон ц(ззтог и, что не менее важно, когда его применять не следует.
° Базовая реализация инспектора (предложенная группой СзоЕ). ° Как устранить некоторые недостатки базовой реализации шаблона Ф з(тог. ° Как реализовать решения, касающиеся реализации шаблона зг1в(тог, в виде библиотеки. ° Мощные обобщенные компоненты, облегчающие реализацию инспекторов. 10.1. Основы шаблона Ч!аког Рассмотрим иерархию классов„которую мы хотим наделить новыми функциональными возможностями. Для этого в нее можно добавлять либо новые классы, либо новые виртуальные функции-члены.
Добавить новые классы легко. Можно применить механизм наследования к терминальному классу и реализовать необходимые виртуальные функции. Для этого не нужно изменять или компилировать вновь существующие классы. Их коды остаются неизменными и готовы к повторному использованию. В противоположность этому, добавить новую виртуальную функцию трудно. Для того чтобы полиморфно манипулировать объектами (с помощью указателей на объекты базового класса), виртуальную функцию-член нужно добавить не только в базовый класс, но и, возможно, во многие другие классы, входящие в иерархию.
Это — основная операция. Она модифицирует базовый класс, от которого зависит вся иерархия и ее клиенты. В результате все придется повторно компилировать. Короче говоря, с точки зрения зависимостей между классами новые классы добавлять легко, а новые виртуальные функции-члены — трудно. Однако лопустим, что у нас есть иерархия, в которую редко добавляются новые классы и в то же время часто добавляются новые виртуальные функции-члены.
В этой ситуации возможность легко добавлять новые классы представляет собой ненужное преимущество. Вот тут-то и пригодится шаблон уз ззсог. Этот шаблон предоставляет программисту именно ту функциональную возможность, в которой он в данный момент нуждается, позволяя легко вставлять в иерархию новые виртуальные функции- члены, одновременно затрудняя вставку новых классов. За это придется заплатить всего-навсего лишним виртуальным вызовом.
Шаблон згзззсог лучше всего проявляет свои возможности, когда операции на объектами не связаны между собой. Парадигма "состояние и операции" для каждого класса становится не совсем уместной. В данной ситуации лучше придерживаться принципа отделекия типов от операций. Имеет смысл хранить разные реализации одной операции вместе, а не распределять их по иерархии классов.
Допустим, что мы разрабатываем текстовый редактор. Элементы документа, такие как абзацы и графические изображения, представляются в виде классов, производных от одного базового класса, скажем, оосв1 еюепс. Документ — это сгруктурированный набор указателей на объекты класса оосв1еюепт. Программисту нужна возможность перемешаться по этой структуре и выполнять операции, например, проверку орфографии, переформатирование и вычисление статистических показателей. В идеале следовало бы реализовывать эти операции, добавив новый код без модификации существующей программы. Более того, программу было бы легче сопровождать, если бы весь код, связанный, скажем, с вычислением статистических показателей, хранился в одном месте. К статистическим показателям могут относиться общее количество символов, значащих символов, слов и рисунков.
Все это естественным образом можно поместить в класс посвсасз. с1азз оосвсата ( цпзздпег) 1пс, спагз попв\апкС)загз ъмогоз зиадез риб11с: чозд дддспагз(цпэздпед зпт сбагзтодбд) ( спагз += с)загзтоддд; ) классы дг)оиогбз, дгЫгиадез и др. определяются так же О статистика отображается в диалоговом окне иотд Оззр1ауо; ); 256 Часть П. Компоненты Используя классический объектно-ориентированный подход к сбору статистики, можно определить в классе оосе1еюепс соответствуюшую виртуальную функцию. с1азз оосе1ешепс ( // эта функция-член предназначена для сбора статистики ч1гсца! чо1д црдасе5сатз(посвтаСМ зсасдзсдсз) = О; ); В этом случае каждый конкретный элемент документа может определять эту функцию по-своему. Например, классы аагайгарб и яазсегвзсшар, производные от класса ОосЕ1ещепс, могут реализовывать функцию црдасе5Сата следуюШим образом.
чо)д яагаогарб::црдасе5сасз(оосвсасзй зсасззсзсз) ( асас1зсз сз.дддсйагз(количество символов в абзаце); зсасз зс)сз.кддшогдз(количества слав в абзаце>; ) чозд яазсегвдсюар::црдасевтатз(посвтатзб зсас)зс)сз) ( // этот класс учитывает только рисунки и больше ничего // (символы и другие элементы игнорируются) зтатзэтдсз.дддтшайез(1); В итоге функция, управляюшая сбором статистических данных, может выглядеть так, чо)д ОосцшепС::О1зр1аувтаС1зтзсз0 ( оосвсасз зсасдзсдсз; Уог (каждый элемент документа) ( злемент->црдасеэсасз(ьсастэсз сэ); ) зсасззсзсз.оззр1ауО; ) Это очень хорошая реализация возможностей по сбору статистики, однако у нее есть ряд недостатков.
° Она требует, чтобы класс ОосЕ1ешепС и производные от него классы имели доступ к определению функции оос5СаСз. Следовательно, при кахцюй модификации класса оос5сасз придется компилировать заново всю иерархию класса ОосЕ1ешепт. ° Фактические операции по сбору статистики рассеяны по разным реализациям функции црдасе5сасз, При отладке или расширении возможностей, связанных со сбором статистики, придется искать и редактировать несколько файлов, ° Реализация не масштабируется по мере добавления новых операций, аналогичных сбору статистики. Для того чтобы добавить операцию "увеличить размер шрифта на один пункт", понадобится добавить в класс оосе1ешепс новую виртуальную функцию, преодолевая все связанные с этим трудности. Однако связь, сушествуюшую между классами оосе1ешепс и оосэтатз, можно разорвать, если переместить все операции внутрь класса оос5сасз и позволить ему самому определять, какую операцию выполнять для того или иного типа.
Для этого нужно, чтобы в 257 Глава 10. Шаблон Ч~вйог классе Оос5СаСз суШествовала фУнкииЯ-член чо1д ордасевкагз (Оос51евепсй). Тогда документ просто просматривал бы свои элементы и вызывал для каждого из них функцию црдасе5сасз. Это решение сразу скрывает класс оос5сасз от класса оос51евепс. Однако теперь класс оосвсасз зависит от каждого конкретного объекта класса оосд1евепс, который необходимо обработать. Если иерархия объектов более постоянна, чем множество операций, эта зависимость никому не мешает. Теперь проблема заключается в том, что реализация класса црдасе5Сагз должна использовать так называемое переключение типов ([уре зппсЬ).
Это переключение возникает при обрашении к полиморфному объекту по его конкретному типу и выполнении разных операций в зависимости от этого типа. Функция оосвсасз:: ордасе5сасз связана с этим переключением типов. чо1д Оос5Сатз:гцрдате5Сатз(пос51евептб е1ев) ( 15 (яагадгарЬ* р = дупав1с сазс<иагадгарЬ*>(йе1ев)) ( сЬагз += р->нивСЬагзо; вогдз += р->мивюогд50 е1зе )б (дупав)с сазс<яазсегв1свар*>Яе1ев)) ( ++двадез ) е)зе добавляем по одному оператору (г" для каждого пшпа инспектируемого объекта (Определение указателя р внутри оператора дг вполне законно, поскольку это мало известное свойство языка СЧ-+.
Переменную можно определять и проверять непосредственно внутри оператора 11. Область видимости этой переменной ограничена оператором дб и его частью е1зе. Несмотря на то что эта возможность не представляется нам сушественной, и создавать остроумные коды в качестве самоцели не рекомендуется, отметим, что она предназначена именно для переключения типов, так почему бы не воспользоваться этим?) Когда программист видит что-то подобное, у него в голове должен сразу же сработать предохранитель.
Переключение типов не всегда желательно. (В главе 8 приводится детальная аргументация этого утверждения.) Код, основанный на переключении типов, трудно понимать, расширять и сопровождать. Он не зашишен от случайных ошибок. Например, что произойдет, если поставить вызов оператора дупав1с сазс для базового класса перед вызовом оператора дупавд с сазс для производного класса? Во время первой проверки будут сопоставляться производные классы, поэтому вторая проверка никогда не будет выполнена. Одна из целей полиморфизма — избежать переключения типов и связанных с ним проблем. Посмотрим, в каких ситуациях может оказаться полезным шаблон ч151сог.
Допустим, нам нужно, чтобы новые функции работали виртуально, но мы не хотим вводить новые виртуальные функции для каждой операции. Для этого нужно реализовать переключающую виртуальную функцию (Ьоппс1пд ч(цпа1 (ппс11оп), единственную во всей иерархии класса Оос51евепс. Эта функция будет "пересылать" задание в другую иерархию, иерархия оос51евепс называется инспектируемой (ч)зйед), а операции принадлежат новой иерархии инспектора (у)з)гог). Каждая реализация переключаюшей виртуальной функции вызывает другую функцию в иерархии инспектора.
Так выбираются инспектируемые типы. Функции в ие- 258 Часть Н. Компоненты рархии инспектора, вызываемые переключиощсй функцией, являются виртуальными. Так выбираются операции. Эту идею иллюстрирует приведенный ниже фрагмент кода. Во-первых, мы определяем абстрактный класс оосе1евепсч1«1сог, в котором определяется операция для каждого типа объектов в иерархии класса оосв1евепс. с1а«« оосе1евептч1«1тог ( риЫ(с: ч1гтца1 чо10 ч1«1срагадгарб(рагадгарпв) = О; ч(гсца1 чо1б ч(«(саа«тегв(свар(аа«сегв)гварв) = О; ...
другие подобные функции ... Затем в иерархию класса оосе1евепт добавляется переключающая виртуальная функция под названием десерт, получаошая параметр оосе1евептч1«1согй и вызывающая соответствующую функцию ч(«(тххх с1а«« оосе1евепт ( рнЫ1с: ч1гтца1 чо10 десерт(цосе)евептч1«1согб) = О„ чо1д рагадгарЫ гдссерт(оосе1евептч1«1согб ч) ч.ч1«1трагадгарб(*тИ«); чо1д аа«тегв1твар:гдссерс(оосе1евепсч1«1согб ч) ( ч.ч1«1таа«сегв)свар(*сИ «): ) А вот и функция оос«тат«во всей своей красе. с1а««оос«тат«: риЫ(с оосд1евептч1«1тог роЫ(с: ч1гтиа1 чо1д ч1«1срагадгарб(рагадгарпв раг) ( сваг« += раг.нцвспаг«Д; иогб« += раг.нцвиогб«О; ч(гтца1 чозб ч(«1таа«сегв(гвар(аа«сегв(гварб) ( ь+1ваде« ) (Разумеется, реальную реализацию этой функции следует поместить в самостоятельный файл, отделив ее от определения класса.) Этот небольшой пример иллюстрирует недостаток шаблона ч1«1тог: виртуальная функция-член на самом деле не добавляется.
Настоящие виртуальные функции имеют полный доступ к каждому объекту, для которого они определены, в то время как из функции ч1«1тограгадгарб можно получить доступ лишь к открытой части класса рагадгаро. 259 Глава 10. Шаблон Чю1ог Управляющая функция оосивепс:: ог зр1ау5сасг зсг сз создает объект класса оос5сасз н вызывает функцию лссерс для каждого объекта класса оосе1евепс, передавая его качестве параметра. Поскольку обьект класса оосвсасз инспектирует разные конкретные объекты класса оосе1евепс, он отлично собирает все данные, и переключение типов совсем не нужно.