Анимация
JavaScript
|
Главная Библионтека классе DocStats существовала функщш-член void updatestats(DocElement&). Тогда документ просто просматривал бы свои элементы и вызывал для каждого из них фушщию updateStats. Это рещение сразу скрывает класс DocStats от класса DocElement. Однако теперь класс DocStats зависит от каждого конкретного объекта класса DocElement, который необходимо обработать. Если иерархия объектов более постоянна, чем множество операций, эта зависимость никому не мещает. Теперь проблема заключается в том, что реализация класса updateStats должна использовать так называемое переключение типов (type switch). Это переключение возникает при обращении к полиморфному объекту по его конкретному типу и выполнении разных операций в зависимости от этого типа. Функция DocStats:: UpdateStats связана с этим переключением типов. void DocStats::UpdateStats(DocElement* el em) { if (Paragraph* p = dynamic cast<Paragraph*>(&elem)) { chars += p->NumChars(); words += p->NumwordsO; else if (dynamic cast<RasterBitmap*>(&elem)) { ++images ; else ... добавляем no одному оператору if для каждого типа инспектируемого объекта (Определение указателя р внутри оператора if вполне законно, поскольку это мало известное свойство языка С-Ы-. Переменную можно определять и проверять непосредственно внутри оператора if. Область видимости этой переменной офаничена оператором if и его частью else. Несмофя на то что эта возможность не представляется нам существенной, и создавать осфоумные коды в качестве самоцели не рекомендуется, отметим, что она предназначена именно для переключения типов, так почему бы не воспользоваться этим?) Когда профаммист видит что-то подобное, у него в голове должен сразу же сработать предохранитель. Переключение типов не всегда желательно. (В главе 8 приводится детальная аргументация этого утверждения.) Код, основанный на переключении типов, фудно понимать, расширять и сопровождать. Он не защищен от случайных ошибок. Например, что произойдет, если поставить вызов оператора dynamic cast для базового класса перед вызовом оператора dynamic cast для производного класса? Во время первой проверки будут сопоставляться производные классы, поэтому вторая проверка никогда не будет выполнена. Одна из целей полиморфизма - избежать переключения типов и связанных с ним проблем. Посмотрим, в каких ситуациях может оказаться полезным шаблон visitor. Допустим, нам нужно, чтобы новые функции работали виртуально, но мы не хотим вводить новые виртуальные функции для каждой операции. Для этого нужно реализовать переключающую виртуальную функцию (bouncing virtual function), единственную во всей иерархии класса DocElement. Эта функция будет "пересылать" задание в другую иерархию. Иерархия DocElement называется инспектируемой (visited), а операции принадлежат новой иерархии инспектора (visitor). Каждая реализация переключающей виртуальной функции вызывает другую функцию в иерархии инспектора. Так выбираются инспектируемые типы. Функции в ие- рархии инспектора, вызываемые переключающей функцией, являются виртуальными. Так выбираются операции. Эту идею иллюстрирует приведенный ниже фрагмент кода. Во-первых, мы определяем абстрактный класс DocElementvisitor, в котором определяется операция для каждого типа объектов в иерархии класса DocElement. class DocElementvisitor { public: virtual void visitParagraph(Paragraph*) = 0; virtual void visitRasterBitmap(RasterBirmap&) = 0; ... другие подобные функции ... Затем в иерархию класса DocEl ement добавляется переключающая виртуальная функция под названием Accept, получающая параметр DocElementvisitor* и вызывающая соответствующую функцию visitAA-x. class DocElement { public: virtual void Accept(DocElementvisitor*) = 0; void Paragraph::Accept(DocElementvisitor* v) { v.visitParagraph(*this); void RasterBitmap::Accept(DocElementvisi tor* v) { v.visitRasterBitmap(*this); A вот и функция DocStats во всей своей красе. class DocStats : public DocElementvisitor { public: virtual void visitParagraph(Paragraph* par) { chars += par.NumCharsO; words += par.NumWordsQ; virtual void visitRasterBirmap(RasterBirmap*) { ++images ; };" (Разумеется, реальную реализацию этой функции следует поместить в самостоятельный файл, отделив ее от определения класса.) Этот небольшой пример иллюстрирует недостаток шаблона visitor: виртуальная функция-член на самом деле не добавляется. Настоящие виртуальные функции имеют полньш доступ к каждому объекту, для которого они определены, в то время как из функции visitorParagraph можно получить доступ лишь к открытой части класса Paragraph. Управляющая функция Document: :DisplayStatisties создает объект класса DocStats и вызывает функцию Accept для каждого объекта класса DocElement, передавая его качестве параметра. Поскольку объект класса DocStats инспектирует разные конкретные объекты класса DocElement, он отлично собирает все данные, и переключение типов совсем не нужно. void Document::DisplayStatistics() { DocStats statistics; for (.каждый элемент документа") 3eAfeHm->Accept(statistics); statisties.DisplayО; Проанализируем возникающий контекст. Мы создали новую иерархию, корнем которой является класс DocEl ementvi si tor. Это иерархия операций (hierarchy of operations). Каждый класс, входящий в нее, на самом деле представляет собой операцию, например DocStats. Теперь добавлять новые операции совсем несложно. Для этого нужно лишь создать новый класс, производный от класса DocEl ementvi si tor. Ни один элемент иерархии класса DocElement для этого менять не требуется. Например, добавим одну операцию, скажем, IncrementFontSize, связанную с горячей клавишей, предназначенной для увеличения размера шрифта на один пункт, или соответствующей кнопкой. class IncrementFontSize : public DocElementvisitor { public: virtual void visitParagraph(Paragraph& par) { par.SetFontSize(par.GetFontSize() + 1); virtual void visitRasterBitmap(RasterBitmap&) { ничего не делаем Вот в все. Не нужно вносить никаких изменений ни в иерархию класса DocElement, ни в другие операции. Вы просто добавляете новый класс. Функция DocElement::Accept перебирает все объекты класса IncrementFontSize совершенно так же, как объекты класса DocStats. Полученная в результате структура классов представлена на рис. 10.1. Напомним, что по умолчанию новый класс легко добавить, а виртуальную функцию-член - нет. Мы преобразовали классы в функции с помощью перехода от иерархии класса DocElement к иерархии класса DocElementvisitor. Таким образом, классы, производные от класса DocElementvisitor, представляют собой воплощенные функции (objectified functions). Так работает шаблон проектирования Visitor. 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 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |