Главная страница
Навигация по странице:

  • КЛАСС COMMAND И ЕГО ПОДКЛАССЫ

  • Glyph

  • ПАТТЕРН COMMAND (КОМАНДА)

  • ДОСТУП К РАСПРЕДЕЛЕННОЙ ИНФОРМАЦИИ

  • КЛАСС ITERATOR И ЕГО ПОДКЛАССЫ

  • Iterator

  • ПАТТЕРН ITERATOR (ИТЕРАТОР)

  • ОБХОД И ДЕЙСТВИЯ, ВЫПОЛНЯЕМЫЕ ПРИ ОБХОДЕ

  • КЛАСС VISITOR И ЕГО ПОДКЛАССЫ

  • Э. Гамма, Р. Хелм


    Скачать 6.37 Mb.
    НазваниеЭ. Гамма, Р. Хелм
    АнкорFactorial
    Дата14.03.2022
    Размер6.37 Mb.
    Формат файлаpdf
    Имя файлаPatterny_Obektno-Orientirovannogo_Proektirovania_2020.pdf
    ТипДокументы
    #395452
    страница8 из 38
    1   ...   4   5   6   7   8   9   10   11   ...   38
    НАСТРОЙКА КЛАССА WINDOW С ПОМОЩЬЮ WINDOWIMP
    Важнейший вопрос, который мы еще не рассмотрели, — как настроить окно подходящим подклассом
    WindowImp
    ? Другими словами, когда инициализиру-

    2.6. Поддержка нескольких оконных систем
    85
    ется переменная
    _imp и как узнать, какая оконная система (а следовательно, и подкласс
    WindowImp
    ) используется? Чтобы окно могло сделать что-то не- тривиальное, ему необходим объект
    WindowImp
    Есть несколько возможностей, но мы остановимся на той, где используется паттерн абстрактная фабрика (113). Можно определить абстрактный фабрич- ный класс
    WindowSystemFactory
    , предоставляющий интерфейс для создания различных видов объектов в зависимости от оконной системы:
    class WindowSystemFactory {
    public:
    virtual WindowImp* CreateWindowImp() = 0;
    virtual ColorImp* CreateColorImp() = 0;
    virtual FontImp* CreateFontImp() = 0;
    // Операция "Create..." для всех ресурсов оконной системы
    };
    Далее можно определить конкретную фабрику для каждой оконной системы:
    class PMWindowSystemFactory : public WindowSystemFactory {
    virtual WindowImp* CreateWindowImp()
    { return new PMWindowImp; }
    // ...
    };
    class XWindowSystemFactory : public WindowSystemFactory {
    virtual WindowImp* CreateWindowImp()
    { return new XWindowImp; }
    // ...
    };
    Для инициализации переменной
    _imp указателем на объект
    WindowImp
    , соот- ветствующий данной оконной системе, конструктор базового класса
    Window может использовать интерфейс
    WindowSystemFactory
    :
    Window::Window () {
    _imp = windowSystemFactory->CreateWindowImp();
    }
    Переменная windowSystemFactory
    — это известный программе экзем- пляр подкласса
    WindowSystemFactory
    (аналог переменной guiFactory
    , определяющей вариант оформления). И инициализируется переменная windowSystemFactory точно так же.

    86
    Глава 2. Практический пример: проектирование редактора документов
    ПАТТЕРН BRIDGE (МОСТ)
    Класс
    WindowImp определяет интерфейс к общим средствам оконной системы, но на его дизайн накладываются иные ограничения, нежели на интерфейс класса
    Window
    . Прикладной программист не обращается к интерфейсу
    WindowImp непосредственно, он имеет дело только с объектами класса
    Window
    Поэтому интерфейс
    WindowImp необязательно должен соответствовать пред- ставлению программиста о мире, как то было в случае с иерархией и интер- фейсом класса
    Window
    . Интерфейс
    WindowImp может более точно отражать сущности, которые в действительности предоставляют оконные системы, со всеми их особенностями. Он может быть ориентирован на пересечение или объединение функциональности — в зависимости от того, что лучше подходит для целевой оконной системы.
    Важно понимать, что интерфейс класса
    Window призван обслуживать ин- тересы прикладного программиста, тогда как интерфейс класса
    WindowImp в большей степени ориентирован на оконные системы. Разделение функ- циональности окон между иерархиями
    Window и
    WindowImp позволяет не- зависимо реализовывать и специализировать их интерфейсы. Объекты из этих иерархий взаимодействуют, позволяя Lexi работать без изменений в нескольких оконных системах.
    Отношение иерархий
    Window и
    WindowImp являет собой пример паттерна мост (184). Его идея заключается в том, чтобы иерархии классов могли работать совместо даже в случае, если они эволюционировали по отдель- ности. Критерии разработки, которыми мы руководствовались, заставили нас создать две различные иерархии классов: одну, поддерживающую ло- гическую концепцию окон, и другую для воплощения разных реализаций окон. Паттерн мост позволяет сохранять и совершенствовать логические абстракции управления окнами, не прикасаясь к коду, зависящему от оконной системы, и наоборот.
    2.7. ОПЕРАЦИИ ПОЛЬЗОВАТЕЛЯ
    Часть функциональности Lexi доступна через WYSIWYG-представление документа. Вы вводите и удаляете текст, перемещаете точку вставки и вы- бираете фрагменты текста, просто указывая и щелкая мышью или нажимая клавиши. Другая часть функциональности доступна через выпадающие меню, кнопки и горячие клавиши. В частности, к этой категории относятся следующие операции:

    2.7. Операции пользователя
    87
    „
    „
    создание нового документа;
    „
    „
    открытие, сохранение и печать существующего документа;
    „
    „
    вырезание выбранной части документа и вставка ее в другое место;
    „
    „
    изменение шрифта и стиля выбранного текста;
    „
    „
    изменение форматирования текста (например, установка режима вы- равнивания);
    „
    „
    завершение работы приложения и др.
    Lexi предоставляет для этих операций различные пользовательские интер- фейсы. Но мы не хотим ассоциировать конкретную операцию с определен- ным пользовательским интерфейсом, поскольку для выполнения одной и той же операции желательно иметь несколько интерфейсов (например, листать страницы можно как помощью кнопки на экране, так и командой меню).
    Кроме того, отдельные элементы интерфейса могут измениться в будущем.
    Кроме того, эти операции реализуются в разных классах. Нам как разработ- чикам хотелось бы иметь доступ к функциональности классов, не создавая зависимостей между классами реализации и пользовательского интерфейса.
    В противном случае получится сильно связанный код, который будет трудно понять, расширять и сопровождать.
    Ситуация осложняется еще и тем, что Lexi должен поддерживать операции отмены и повтора
    1
    большинства, но не всех операций. Точнее, желательно уметь отменять операции модификации документа (скажем, удаление), которые из-за оплошности пользователя могут привести к уничтожению большого объема данных. Но не следует пытаться отменить такую опера- цию, как сохранение чертежа или завершение приложения. Мы также не хотели бы налагать произвольные ограничения на число уровней отмены и повтора.
    Разумеется, поддержка пользовательских операций распределена по всему приложению. Задача в том, чтобы найти простой и расширяемый механизм, удовлетворяющий всем вышеизложенным требованиям.
    ИНКАПСУЛЯЦИЯ ЗАПРОСА
    С точки зрения проектировщика выпадающее меню — это просто еще один вид вложения глифов. От других глифов, имеющих потомков, его отличает
    1
    Под повтором (redo) понимается выполнение только что отмененной операции.

    88
    Глава 2. Практический пример: проектирование редактора документов то, что большинство содержащихся в меню глифов каким-то образом реа- гирует на отпускание кнопки мыши.
    Предположим, что такие реагирующие глифы являются экземплярами подкласса
    MenuItem класса
    Glyph и что свою работу они выполняют в ответ на запрос клиента
    1
    . Для выполнения запроса может потребоваться вызвать одну операцию одного объекта или много операций разных объектов (или какой-нибудь промежуточный вариант).
    Нам не хватает механизма параметризации пунктов меню запросами, кото- рые они должны выполнять. Таким способом удалось бы избежать разраста- ния числа подклассов и обеспечить большую гибкость во время выполнения.
    MenuItem можно параметризовать вызываемой функцией, но это решение неполно по трем причинам:
    „
    „
    в нем не учитывается проблема отмены/повтора;
    „
    „
    с функцией трудно ассоциировать состояние. Например, функция, из- меняющая шрифт, должна знать, какой именно это шрифт;
    „
    „
    функции трудно расширять, а повторное использование их частей за- труднено.
    Отсюда следует, что пункты меню лучше параметризовать объектом, а не функцией. Тогда мы сможем прибегнуть к механизму наследования для расширения и повторного использования реализации запроса. Кроме того, у нас появляется место для сохранения состояния и реализации отмены/
    повтора. Это еще один пример инкапсуляции изменяющейся сущности, в данном случае — запроса. Каждый запрос мы инкапсулируем в объект- команду.
    КЛАСС COMMAND И ЕГО ПОДКЛАССЫ
    Сначала определим абстрактный класс
    Command
    , который будет предостав- лять интерфейс для выдачи запроса. Базовый интерфейс включает всего одну абстрактную операцию
    Execute
    . Подклассы
    Command по-разному реализуют эту операцию для выполнения разных запросов. Некоторые подклассы могут частично или полностью делегировать работу другим объектам, а остальные выполняют запрос сами (рис. 2.11). Однако для запрашиваю-
    1
    Концептуально клиентом является пользователь Lexi, но на самом деле это просто какой-то другой объект (например, диспетчер событий), который управляет обработкой ввода пользователя.

    2.7. Операции пользователя
    89
    щего объект
    Command
    — это всего лишь объект
    Command
    ; все такие объекты обрабатываются одинаково.
    Command
    Execute()
    PasteCommand
    Execute()
    buffer
    FontCommand
    Execute()
    newFont
    SaveCommand
    Execute()
    QuitCommand
    Execute()
    Вставить содержимое буфера в документ
    Вывести диалоговое окно,
    в котором пользователь сможет ввести имя документа, а затем сохранить документ с указанным именем
    Перерисовать выбранный фрагмент текста другим шрифтом if (документ изменен)
    { saveŒ>Execute() }
    Завершить приложение save
    Рис. 2.11. Часть иерархии класса Command
    Теперь в классе
    MenuItem может храниться объект, инкапсулирующий за- прос (рис. 2.12). Каждому объекту, представляющему пункт меню, переда- ется экземпляр того из подклассов
    Command
    , который соответствует этому пункту, точно так же, как мы задаем текст, отображаемый в пункте меню.
    Когда пользователь выбирает некоторый пункт меню, объект
    MenuItem про- сто вызывает операцию
    Execute для своего объекта
    Command
    , чтобы он вы- полнил запрос. Заметим, что кнопки и другие виджеты могут пользоваться объектами
    Command точно так же, как и пункты меню.
    Glyph
    MenuItem
    Clicked()
    Command
    Execute()
    command>Execute();
    command
    Рис. 2.12. Отношения между классами MenuItem и Command

    90
    Глава 2. Практический пример: проектирование редактора документов
    ОТМЕНА ОПЕРАЦИЙ
    Функциональность отмены/повтора играет важную роль в интерактивных приложениях. Чтобы иметь возможность отменять и повторять команды, нужно включить операцию
    Unexecute в интерфейс класса
    Command
    . Ее вы- полнение отменяет все последствия предыдущей операции
    Execute с ис- пользованием информации, сохраненной этой операцией. Так, при команде
    FontCommand операция
    Execute должна была бы сохранить диапазон текста, шрифт которого был изменен, а также первоначальный шрифт (или шриф- ты). Операция
    Unexecute класса
    FontCommand восстановит старый шрифт
    (или шрифты) для указанного диапазона текста.
    Иногда возможность выполнения отмены должна определяться во время выполнения. Скажем, запрос на изменение шрифта выделенного участка текста не производит никаких действий, если текст уже отображен требуе- мым шрифтом. Предположим, что пользователь выбрал некий текст и решил изменить его шрифт на случайно выбранный. Что произойдет в результате последующего запроса на отмену? Должно ли бессмысленное изменение приводить к столь же бессмысленной отмене? Наверное, нет. Если пользо- ватель повторит случайное изменение шрифта несколько раз, то не следует заставлять его выполнять точно такое же число отмен, чтобы вернуться к по- следнему осмысленному состоянию. Если суммарный эффект выполнения последовательности команд нулевой, то нет необходимости вообще делать что-либо при запросе на отмену.
    Для определения того, можно ли отменить действие команды, мы добавим к интерфейсу класса
    Command абстрактную операцию
    Reversible
    (обратимая), которая возвращает булево значение. Подклассы могут переопределить эту операцию и возвращать true или false в зависимости от критерия, вычис- ляемого во время выполнения.
    ИСТОРИЯ КОМАНД
    Последний шаг по поддержке отмены и повтора с произвольным числом уровней — определение истории команд, то есть списка ранее выполненных или отмененных команд. С концептуальной точки зрения история команд выглядит так:
    Прошлые команды
    Настоящее

    2.7. Операции пользователя
    91
    Каждый кружок представляет один объект
    Command
    . В данном случае поль- зователь выполнил четыре команды. Первой была выполнена крайняя левая команда, затем вторая слева и т. д. вплоть до последней команды (крайней правой). Линия с пометкой «настоящее» обозначает самую последнюю вы- полненную (или отмененную) команду.
    Чтобы отменить последнюю команду, мы просто вызываем операцию
    Unexecute для самой последней команды:
    Настоящее
    Unexecute()
    После отмены команды сдвигаем линию «настоящее» на одну команду влево.
    Если пользователь выполнит еще одну отмену, то произойдет откат еще на один шаг (см. рис. ниже).
    Настоящее
    Будущее
    Прошлое
    Видно, что за счет простого повторения процедуры мы получаем произ- вольное число уровней отмены, ограниченное лишь длиной списка истории команд.
    Чтобы повторить только что отмененную команду, проведем обратные действия. Команды справа от линии «настоящее» — те, что могут быть повторены в будущем. Для повтора последней отмененной команды мы вызываем операцию
    Execute для последней команды справа от линии
    «настоящее»:
    Настоящее
    Execute()

    92
    Глава 2. Практический пример: проектирование редактора документов
    Затем линия «настоящее» сдвигается, чтобы следующий повтор вызвал операцию
    Execute для следующей команды будущего.
    Настоящее
    Будущее
    Прошлое
    Разумеется, если следующая операция — это не повтор, а отмена, то команда слева от линии «настоящее» будет отменена. Таким образом, пользователь может перемещаться в обоих направлениях, чтобы исправить ошибки.
    ПАТТЕРН COMMAND (КОМАНДА)
    Команды Lexi — это пример применения паттерна команда (275), который описывает инкапсуляцию запроса. Этот паттерн предписывает единый интерфейс для выдачи запросов, с помощью которого можно настроить клиентов для обработки разных запросов. Интерфейс изолирует клиента от реализации запроса. Команда может полностью или частично делегировать реализацию запроса другим объектам либо выполнять данную операцию самостоятельно. Это идеальное решение для приложений типа Lexi, которые должны предоставлять централизованный доступ к функциональности, раз- бросанной по разным частям программы. Данный паттерн предлагает также механизмы отмены и повтора, построенные на основе базового интерфейса класса
    Command
    2.8. ПРОВЕРКА ПРАВОПИСАНИЯ
    И РАССТАНОВКА ПЕРЕНОСОВ
    Последняя задача проектирования связана с анализом текста, а конкретно с проверкой правописания и нахождением мест, где можно поставить пере- нос для улучшения форматирования.
    Ограничения здесь аналогичны тем, о которых уже говорилось при об- суждении форматирования в разделе 2.3. Как и в случае с разбиением на строки, существует много возможных реализаций поиска орфографических

    2.8. Проверка правописания и расстановка переносов
    93
    ошибок и вычисления точек переноса. Поэтому и здесь планировалась поддержка нескольких алгоритмов. Пользователь сможет выбрать тот алгоритм, который его больше устраивает по соотношению затрат памяти, скорости и качества. Добавление новых алгоритмов тоже должно реали- зовываться просто.
    Также необходимо избежать жесткой привязки этой информации к струк- туре документа. В данном случае такая цель даже более важна, чем при форматировании, поскольку проверка правописания и расстановка пере- носов — лишь два вида анализа текста, которые Lexi мог бы поддерживать.
    Со временем мы неизбежно захотим расширить аналитические возможности
    Lexi. Мы могли бы добавить поиск, подсчет слов, средства вычислений для суммирования значений в таблице, проверку грамматики и т. д. Но мы не хотим изменять класс
    Glyph и все его подклассы при каждом добавлении такого рода функциональности.
    У этой задачи есть два аспекта: (1) доступ к анализируемой информации, разбросанной по разным глифам в структуре документа, и (2) собственно проведение анализа. Рассмотрим их по отдельности.
    ДОСТУП К РАСПРЕДЕЛЕННОЙ ИНФОРМАЦИИ
    Для многих видов анализа необходимо обрабатывать текст на уровне от- дельных символов. Но анализируемый текст рассеян по иерархии структур, состоящих из объектов-глифов. Для анализа текста, представленного в та- ком виде, понадобится механизм доступа, располагающий информацией о структурах данных, в которых хранится текст. У одних глифов потомки могут храниться в связанных списках, у других — в массивах, а у третьих и вовсе используются какие-то экзотические структуры. Наш механизм до- ступа должен справляться со всем этим.
    К сожалению, для разных видов анализа методы доступа к информации могут различаться. Обычно текст сканируется от начала к концу. Но ино- гда требуется сделать прямо противоположное. Например, для обратного поиска нужно проходить по тексту в обратном, а не в прямом направлении.
    А при вычислении алгебраических выражений может потребоваться сим- метричный (in order) обход.
    Итак, наш механизм доступа должен уметь адаптироваться к разным струк- турам данных и поддерживать разные способы обхода (например, обход в прямом или обратном порядке или симметричный обход).

    94
    Глава 2. Практический пример: проектирование редактора документов
    ИНКАПСУЛЯЦИЯ ДОСТУПА И ПОРЯДКА ОБХОДА
    Пока что в нашем интерфейсе глифов для обращения к потомкам со сто- роны клиентов используется целочисленный индекс. Может, это и будет эффективно для тех классов глифов, которые хранят потомков в массиве, но совершенно неэффективно для глифов, использующих связанный список.
    Абстракция глифов должна скрыть структуру данных, в которой хранятся потомки. Тогда мы сможем изменить структуру данных, используемую классом глифа, не затрагивая другие классы.
    Поэтому только глиф может знать, какую структуру он использует. Отсюда следует, что интерфейс глифов не должен отдавать предпочтение какой-то одной структуре данных. Например, не следует оптимизировать его в пользу массивов, а не связанных списков, как это делалось до сих пор.
    Мы можем решить проблему и одновременно поддержать несколько разных способов обхода. Разумно включить множественные средства обращения и обхода прямо в классы глифов и предоставить способ выбирать между ними — например, с передачей константы из некоторого перечисления.
    Выполняя обход, классы передают этот параметр друг другу, чтобы гаран- тировать, что все они обходят структуру в одном и том же порядке. Так же должна передаваться любая информация, собранная во время обхода.
    Для поддержки описанного подхода в интерфейс класса
    Glyph можно было бы добавить следующие абстрактные операции:
    void First(Traversal kind)
    void Next()
    bool IsDone()
    Glyph* GetCurrent()
    void Insert(Glyph*)
    Операции
    First
    ,
    Next и
    IsDone управляют обходом.
    First инициализирует процедуру обхода. В параметре ей передается разновидность обхода в виде константы из перечисления
    Traversal
    , которая может принимать такие значения, как
    CHILDREN
    (обходить только прямых потомков глифа),
    PREORDER
    (обходить всю структуру в прямом порядке),
    POSTORDER
    (в обратном поряд- ке) или
    INORDER
    (в симметричном порядке).
    Next переходит к следующему глифу в порядке обхода, а
    IsDone сообщает, завершился ли обход.
    GetCurrent заменяет операцию
    Child
    — осуществляет доступ к текущему в данном об- ходе глифу. Старая операция
    Insert заменяется, теперь она вставляет глиф в текущую позицию. При анализе можно было бы использовать следующий код C++ для обхода структуры глифов с корнем g
    в прямом порядке:

    2.8. Проверка правописания и расстановка переносов
    95
    Glyph* g;
    for (g->First(PREORDER); !g->IsDone(); g->Next()) {
    Glyph* current = g->GetCurrent();
    // Выполнить анализ
    }
    Обратите внимание: целочисленный индекс исключен из интерфейса гли- фов. Не осталось ничего, что предполагало бы какой-то предпочтительный контейнер. Также клиенты были бы избавлены от необходимости самосто- ятельно реализовывать типичные виды доступа.
    Но этот подход еще не идеален. Во-первых, он не позволяет поддерживать новые виды обхода без расширения множества значений перечисления или добавления новых операций. Предположим, вам нужен вариант прямого обхода, при котором автоматически пропускаются нетекстовые глифы. Тогда пришлось бы изменить перечисление
    Traversal и включить в него значение вида
    TEXTUAL_PREORDER
    Тем не менее, менять уже имеющиеся объявления нежелательно. Помещение всего механизма обхода в иерархию класса
    Glyph затрудняет модификацию и расширение без изменения многих других классов. Также затруднено по- вторное использование этого механизма для обхода других видов структур объектов. Наконец, у данной структуры не может быть более одного неза- вершенного обхода.
    И снова наилучшее решение — инкапсуляция изменяющейся сущности в классе. В нашем случае это механизмы обращения к элементам и обхода.
    Вомзожно ввести класс объектов, называемых итераторами, единственное назначение которых — определить разные наборы таких механизмов. Можно также воспользоваться наследованием для унификации доступа к разным структурам данных и поддержки новых видов обхода. Тогда вам не придет- ся изменять интерфейсы глифов или трогать реализации существующих глифов.
    КЛАСС ITERATOR И ЕГО ПОДКЛАССЫ
    Мы применим абстрактный класс
    Iterator для определения общего интерфейса обращения к элементам и обхода. Конкретные подклассы
    (такие как
    ArrayIterator и
    ListIterator
    ) реализуют данный интерфейс для предоставления доступа к массивам и спискам, а такие подклассы, как
    PreorderIterator
    ,
    PostorderIterator и им подобные, реализуют разные

    96
    Глава 2. Практический пример: проектирование редактора документов виды обхода структур. Каждый подкласс класса
    Iterator содержит ссылку на структуру, которую он обходит. Экземпляры подкласса инициализи- руются этой ссылкой при создании. На рис. 2.13 показан класс
    Iterator и некоторые из его подклассов. Обратите внимание: в интерфейс класса
    Glyph добавлена абстрактная операция
    CreateIterator для поддержки итераторов.
    Iterator
    First()
    Next()
    IsDone()
    CurrentItem()
    PreorderIterator
    First()
    Next()
    IsDone()
    CurrentItem()
    ArrayIterator
    First()
    Next()
    IsDone()
    CurrentItem()
    ListIterator
    First()
    Next()
    IsDone()
    CurrentItem()
    NullIterator
    First()
    Next()
    IsDone()
    CurrentItem()
    currentItem
    Glyph
    ...
    CreateIterator()
    return new NullIterator return true
    Итераторы
    Корень
    Рис. 2.13. Класс Iterator и его подклассы
    Интерфейс итератора предоставляет операции
    First
    ,
    Next и
    IsDone для управления обходом. В классе
    ListIterator операция
    First реализуется указателем на первый элемент списка, а
    Next перемещает итератор к следу- ющему элементу. Операция
    IsDone возвращает признак, говорящий о том, перешел ли указатель за последний элемент списка. Операция
    CurrentItem разыменовывает итератор для возвращения глифа, на который он ссылается.
    Класс
    ArrayIterator делает то же самое с массивами глифов.
    Теперь мы можем обращаться к потомкам в структуре глифа, не зная ее представления:
    Glyph* g;
    Iterator* i = g->CreateIterator();

    2.8. Проверка правописания и расстановка переносов
    97
    for (i->First(); !i->IsDone(); i->Next()) {
    Glyph* child = i->CurrentItem();
    // Выполнить действие с текущим потомком
    }
    CreateIterator по умолчанию возвращает экземпляр
    NullIterator
    — вы- рожденный итератор для глифов, у которых нет потомков, то есть листовых глифов. Операция
    IsDone для
    NullIterator всегда возвращает true
    Подкласс глифа, имеющего потомков, замещает операцию
    CreateIterator так, что она возвращает экземпляр другого подкласса класса
    Iterator
    . Ка-
    кого именно — зависит от структуры, в которой содержатся потомки. Если подкласс
    Row класса
    Glyph размещает потомков в списке, то его операция
    CreateIterator будет выглядеть примерно так:
    Iterator* Row::CreateIterator () {
    return new ListIterator(_children);
    }
    Итераторы для обхода в прямом и симметричном порядке реализуют ал- горитм обхода в контексте конкретных глифов. В обоих случаях итератору передается корневой глиф той структуры, которую нужно обойти. Итераторы вызывают
    CreateIterator для каждого глифа в этой структуре и сохраняют возвращенные итераторы в стеке.
    Например, класс
    PreorderIterator получает итератор от корневого глифа, инициализирует его так, чтобы он указывал на свой первый элемент, а затем помещает в стек:
    void PreorderIterator::First () {
    Iterator* i = _root->CreateIterator();
    if (i) {
    i->First();
    _iterators.RemoveAll();
    _iterators.Push(i);
    }
    }
    CurrentItem будет просто вызывать операцию
    CurrentItem для итератора на вершине стека:
    Glyph* PreorderIterator::CurrentItem () const {
    Return _iterators.Size() > 0 ? _iterators.Top()->CurrentItem() : 0;
    }

    98
    Глава 2. Практический пример: проектирование редактора документов
    Операция
    Next получает итератор с вершины стека и приказывает его текущему элементу создать свой итератор, спускаясь тем самым по струк- туре глифов как можно ниже (как это делается для прямого порядка).
    Next устанавливает новый итератор так, чтобы он указывал на первый элемент в порядке обхода, и помещает его в стек. Затем
    Next проверяет последний встретившийся итератор; если его операция
    IsDone возвращает true
    , значит, обход текущего поддерева (или листа) закончен. В таком случае
    Next снимает итератор с вершины стека и повторяет всю последовательность действий, пока не найдет следующее неполностью обойденное дерево, если таковое существует. Если же необойденных деревьев больше нет, то обход завершен:
    void PreorderIterator::Next () {
    Iterator* i =
    _iterators.Top()->CurrentItem()->CreateIterator();
    i->First();
    _iterators.Push(i);
    while (
    _iterators.Size() > 0 && _iterators.Top()->IsDone()
    ) {
    delete _iterators.Pop();
    _iterators.Top()->Next();
    }
    }
    Обратите внимание: класс
    Iterator позволяет вводить новые виды обходов, не изменяя классы глифов, — достаточно породить новый подкласс и доба- вить новый обход так, как было сделано для
    PreorderIterator
    . Подклассы класса
    Glyph используют тот же самый интерфейс, чтобы предоставить клиентам доступ к своим потомкам без раскрытия внутренней структуры данных, в которой они хранятся. Поскольку итераторы сохраняют собствен- ную копию состояния обхода, то одновременно можно иметь несколько ак- тивных итераторов для одной и той же структуры. И, хотя в нашем примере мы занимались обходом структур глифов, ничто не мешает параметризовать класс типа
    PreorderIterator типом объекта структуры. В C++ для этого ис- пользовались бы шаблоны. Тогда описанный механизм итераторов можно было бы применить для обхода других структур.
    ПАТТЕРН ITERATOR (ИТЕРАТОР)
    Паттерн итератор (302) абстрагирует описанный метод поддержки обхода структур, состоящих из объектов, и доступа к их элементам. Он применим

    2.8. Проверка правописания и расстановка переносов
    99
    не только к составным структурам, но и к группам, абстрагирует алгоритм обхода и изолирует клиентов от деталей внутренней структуры объектов, которые они обходят. Паттерн итератор — еще один пример того, как инкап- суляция изменяющейся сущности помогает добиться гибкости и повторной используемости. Но все равно проблема итерации оказывается неожиданно глубокой, поэтому паттерн итератор гораздо сложней, чем было рассмотрено выше.
    ОБХОД И ДЕЙСТВИЯ, ВЫПОЛНЯЕМЫЕ ПРИ ОБХОДЕ
    Итак, теперь, когда у нас есть способ обойти структуру глифов, нужно за- няться проверкой правописания и расстановкой переносов. Для обоих видов анализа необходимо накапливать собранную во время обхода информацию.
    Прежде всего следует решить, на какую часть программы возложить ответ- ственность за выполнение анализа. Можно было бы поручить это классам
    Iterator
    , тем самым сделав анализ неотъемлемой частью обхода. Но решение стало бы более гибким и пригодным для повторного использования, если бы обход был отделен от действий, которые при этом выполняются. Дело в том, что для одного и того же вида обхода могут выполняться разные виды анализа. Поэтому один и тот же набор итераторов можно было бы исполь- зовать для разных аналитических операций. Например, прямой порядок обхода применяется в разных случаях, включая проверку правописания, расстановку переносов, поиск в прямом направлении и подсчет слов.
    Итак, анализ и обход следует разделить. На кого еще можно возложить ответственность за выполнение анализа? Мы знаем, что разновидностей анализа достаточно много. При каждом виде анализа в определенные мо- менты обхода будут выполняться разные действия. В зависимости от вида анализа некоторые глифы могут оказаться более важными, чем другие. При проверке правописания и расстановке переносов следует рассматривать только символьные глифы и пропускать графические — линии, растровые изображения и т. д. Если мы занимаемся разделением цветов, то желательно было бы ограничиться только видимыми глифами. Таким образом, разные виды анализа будут просматривать разные глифы.
    Поэтому данный вид анализа должен уметь различать глифы по их типу.
    Очевидное решение — встроить аналитическую функциональность в сами классы глифов. Тогда для каждого вида анализа мы можем добавить одну или несколько абстрактных операций в класс
    Glyph и реализовать их в под- классах в соответствии с той ролью, которую они играют при анализе.

    100
    Глава 2. Практический пример: проектирование редактора документов
    Однако у такого подхода есть и недостаток: каждый класс глифов придется изменять при добавлении нового вида анализа. В некоторых случаях про- блему удается сгладить: если в анализе участвует немного классов или если большинство из них выполняют анализ одним и тем же способом, то можно поместить подразумеваемую реализацию абстрактной операции прямо в класс
    Glyph
    . Такая операция по умолчанию будет обрабатывать наиболее распространенный случай. Тогда мы смогли бы ограничиться только изме- нениями класса
    Glyph и тех его подклассов, которые отклоняются от нормы.
    Несмотря на то что реализация по умолчанию сокращает объем изменений, принципиальная проблема остается: интерфейс класса
    Glyph необходимо расширять при добавлении каждого нового вида анализа. Со временем такие операции начнут скрывать смысл этого интерфейса. Будет трудно понять, что основная цель глифа — определить и структурировать объекты, имеющие внешнее представление и форму; интерфейс потеряется за посторонним шумом.
    ИНКАПСУЛЯЦИЯ АНАЛИЗА
    Судя по всему, стоит инкапсулировать анализ в отдельный объект, как мы уже много раз делали прежде. Можно было бы поместить механизм кон- кретного вида анализа в его собственный класс, а экземпляр этого класса использовать совместно с подходящим итератором. Тогда итератор «пере- носил» бы этот экземпляр от одного глифа к другому, а объект выполнял бы свой анализ для каждого элемента. По мере продвижения обхода анализатор накапливал бы определенную информацию (в данном случае символы).
    Итератор ''a_''
    Анализатор "a"
    "_"
    1 2
    3 4
    5

    2.8. Проверка правописания и расстановка переносов
    101
    Принципиальный вопрос при таком подходе — как объект-анализатор раз- личает виды глифов, не прибегая к проверке или приведениям типов? Мы не хотим включать в класс
    SpellingChecker псевдокод следующего вида:
    void SpellingChecker::Check (Glyph* glyph) {
    Character* c;
    Row* r;
    Image* i;
    if (c = dynamic_cast(glyph)) {
    // проанализировать символ
    } else if (r = dynamic_cast(glyph)) {
    // подготовиться к анализу потомков r
    } else if (i = dynamic_cast(glyph)) {
    // ничего не делать
    }
    }
    Такой код получается довольно уродливым. Он опирается на специфические возможности вроде безопасных по отношению к типам приведений. Его трудно расширять. Нужно не забыть изменить тело данной функции после любого изменения иерархии класса
    Glyph
    . В общем это как раз такой код, для избавления от которого создавались объектно-ориентированные языки.
    Как уйти от данного подхода методом «грубой силы»? Посмотрим, что произойдет, если мы добавим в класс
    Glyph такую абстрактную операцию:
    void CheckMe(SpellingChecker&)
    Определим операцию
    CheckMe в каждом подклассе класса
    Glyph следующим образом:
    void GlyphSubclass::CheckMe (SpellingChecker& checker) {
    checker.CheckGlyphSubclass(this);
    }
    где
    GlyphSubclass заменяется именем подкласса глифа. Заметим, что при вызове
    CheckMe конкретный подкласс класса
    Glyph известен, ведь мы же выполняем одну из его операций. В свою очередь, в интерфейсе класса
    SpellingChecker есть операция типа
    CheckGlyphSubclass для каждого под- класса класса
    Glyph
    1
    :
    1
    Можно было бы воспользоваться перегрузкой функций, чтобы присвоить этим функци- ям одинаковые имена, поскольку их можно различить по типам параметров. Здесь мы дали им разные имена, чтобы было видно, что это все-таки разные функции, особенно при их вызове.

    102
    Глава 2. Практический пример: проектирование редактора документов class SpellingChecker {
    public:
    SpellingChecker();
    virtual void CheckCharacter(Character*);
    virtual void CheckRow(Row*);
    virtual void CheckImage(Image*);
    // ...и так далее
    List& GetMisspellings();
    protected: virtual bool IsMisspelled(const char*); private: char _currentWord[MAX_WORD_SIZE];
    List _misspellings;
    };
    Операция проверки в классе
    SpellingChecker для глифов типа
    Character могла бы выглядеть так:
    void SpellingChecker::CheckCharacter (Character* c) { const char ch = c->GetCharCode(); if (isalpha(ch)) {
    // присоединить алфавитный символ к _currentWord
    } else {
    // обнаружен символ, не являющийся алфавитным if (IsMisspelled(_currentWord)) {
    // добавить _currentWord к _misspellings
    _misspellings.Append(strdup(_currentWord));
    }
    _currentWord[0] = '\0';
    // сбросить _currentWord для проверки следующего слова
    }
    }
    Обратите внимание: мы определили специальную операцию
    GetCharCode только для класса
    Character
    . Объект проверки правописания может работать со специфическими для подклассов операциями, не прибегая к проверке или приведению типов, а это позволяет нам трактовать некоторые объекты специальным образом.
    Объект класса
    CheckCharacter накапливает буквы в буфере
    _currentWord
    Когда встречается не буква, например символ подчеркивания, этот объект вызывает операцию
    IsMisspelled для проверки орфографии слова, находя-

    2.8. Проверка правописания и расстановка переносов
    103
    щегося в
    _currentWord
    1
    . Если слово написано неправильно, то
    CheckCharacter добавляет его в список слов с ошибками. Затем буфер
    _currentWord очища- ется для приема следующего слова. По завершении обхода можно добраться до списка слов с ошибками с помощью операции
    GetMisspellings
    Теперь логично обойти всю структуру глифов, вызывая
    CheckMe для каждого глифа и передавая ей объект проверки правописания в качестве аргумента.
    Тем самым текущий глиф для
    SpellingChecker идентифицируется и при- казывает модулю проверки выполнить следующий шаг в проверке:
    SpellingChecker spellingChecker;
    Composition* c;
    // ...
    Glyph* g;
    PreorderIterator i(c); for (i.First(); !i.IsDone(); i.Next()) { g = i.CurrentItem(); g->CheckMe(spellingChecker);
    }
    На следующей схеме показано, как взаимодействуют глифы типа
    Character и объект
    SpellingChecker aCharacter(''a'')
    anotherCharacter(''_'')
    CheckMe(aSpellingChecker)
    aSpellingChecker
    CheckCharacter(this)
    GetCharacter()
    CheckCharacter(this)
    GetCharacter()
    CheckMe(aSpellingChecker)
    Проверяет полное слово
    1
    Функция
    IsMisspelled реализует алгоритм проверки орфографии, подробности кото- рого здесь не приводятся, поскольку мы сделали его независимым от дизайна Lexi. Мы можем поддержать разные алгоритмы, порождая подклассы класса
    SpellingChecker
    , или применить для этой цели паттерн стратегия
    (как для форматирования в разделе 2.3).

    104
    Глава 2. Практический пример: проектирование редактора документов
    Этот подход работает при поиске орфографических ошибок, но как он может помочь в поддержке нескольких видов анализа? Похоже, что придется до- бавлять операцию вроде
    CheckMe(SpellingChecker&)
    в класс
    Glyph и его под- классы всякий раз, когда добавляется новый вид анализа. Так оно и есть, если мы настаиваем на независимом классе для каждого вида анализа. Но почему бы не придать всем видам анализа одинаковый интерфейс? Это позволит нам использовать их полиморфно. И тогда мы сможем заменить специфические для конкретного вида анализа операции вроде
    CheckMe(SpellingChecker&)
    одной инвариантной операцией, принимающей более общий параметр.
    КЛАСС VISITOR И ЕГО ПОДКЛАССЫ
    Мы будем использовать термин «посетитель» для обозначения класса объ- ектов, «посещающих» другие объекты во время обхода, дабы сделать то, что необходимо в данном контексте
    1
    . Тогда мы можем определить класс
    Visitor
    , описывающий абстрактный интерфейс для посещения глифов в структуре:
    class Visitor {
    public:
    virtual void VisitCharacter(Character*) { }
    virtual void VisitRow(Row*) { }
    virtual void VisitImage(Image*) { }
    // ...И так далее
    };
    Конкретные подклассы
    Visitor выполняют разные виды анализа. Напри- мер, можно определить подкласс
    SpellingCheckingVisitor для проверки правописания и подкласс
    HyphenationVisitor для расстановки переносов.
    При этом
    SpellingCheckingVisitor был бы реализован точно так же, как мы реализовали класс
    SpellingChecker выше, только имена операций отражали бы более общий интерфейс класса
    Visitor
    . Так, операция
    CheckCharacter называлась бы
    VisitCharacter
    Поскольку имя
    CheckMe не подходит для посетителей, которые ничего не проверяют, мы используем имя
    Accept
    . Аргумент этой операции тоже при- дется изменить на
    Visitor&
    , чтобы отразить тот факт, что может приниматься любой посетитель. Теперь для добавления нового вида анализа нужно лишь определить новый подкласс класса
    Visitor
    , а трогать классы глифов вовсе
    1
    «Посетить» — всего лишь чуть более общий термин, чем «проанализировать». Он про- сто предвосхищает ту терминологию, которая будет использоваться при обсуждении следующего паттерна.

    2.8. Проверка правописания и расстановка переносов
    105
    не обязательно. Таким образом добавление всего одной операции в класс
    Glyph и его подклассы позволяет поддерживать в будущем все возможные виды анализа.
    О том, как работает проверка правописания, говорилось выше. Такой же под- ход будет применен для накопления текста в подклассе
    HyphenationVisitor
    Но после того как операция
    VisitCharacter из подкласса
    HyphenationVisitor закончила распознавание целого слова, она ведет себя по-другому. Вместо проверки орфографии применяется алгоритм расстановки переносов, чтобы определить, в каких местах можно перенести слово на другую строку (если это вообще возможно). Затем для каждой из найденных точек в структуру вставляется разделяющий (discretionary) глиф. Разделяющие глифы явля- ются экземплярами подкласса
    Glyph
    — класса
    Discretionary
    Разделяющий глиф может выглядеть по-разному в зависимости от того, является он последним символом в строке или нет. Если это последний символ, глиф выглядит как дефис, в противном случае он не отображает- ся вообще. Разделяющий глиф проверяет своего родителя (объект
    Row
    ), чтобы узнать, является ли он последним потомком; он делает это всякий раз, когда от него требуют отобразить себя или вычислить свои размеры.
    Стратегия форматирования поступает с разделяющими глифами точно так же, как с пропусками, в результате чего они становятся «кандидатами» на завершающий символ строки. На схеме ниже показано, как может выглядеть встроенный разделитель.
    "a"
    "l"
    "l"
    "o"
    "y"
    Разделитель
    aluminum alloy
    или
    aluminum al
    loy
    ПАТТЕРН VISITOR (ПОСЕТИТЕЛЬ)
    Вышеописанная процедура — пример применения паттерна посети- тель (379). Его главными составляющими являются класс
    Visitor и его подклассы. Паттерн посетитель абстрагирует метод, позволяющий иметь произвольное число видов анализа структур глифов без изменения самих классов глифов. Еще одна полезная особенность посетителей состоит в том, что их можно применять не только к таким агрегатам, как наши структуры

    106
    Глава 2. Практический пример: проектирование редактора документов глифов, но и к любым структурам, состоящим из объектов. В эту категорию входят множества, списки и даже направленные ациклические графы. Бо- лее того, классы, которые обходит посетитель, необязательно должны быть связаны друг с другом через общий родительский класс. А это значит, что посетители могут охватывать разные иерархии классов.
    Важный вопрос, который надо задать себе перед применением паттерна посетитель, звучит так: «Какие иерархии классов наиболее часто будут из- меняться?» Этот паттерн особенно хорошо подходит для выполнения дей- ствий с объектами, входящими в стабильную структуру классов. Добавление нового вида посетителя не требует изменять структуру классов, что особенно важно, когда эта структура велика. Но каждый раз, когда в структуру добав- ляется новый подкласс, вам придется обновить все интерфейсы посетителя и добавить операцию
    Visit
    ... для этого подкласса. В нашем примере это оз- начает, что добавление подкласса
    Foo класса
    Glyph потребует изменить класс
    Visitor и все его подклассы, чтобы добавить операцию
    VisitFoo
    . Однако при наших ограничениях гораздо более вероятно добавление к Lexi нового вида анализа, а не нового вида глифов. Поэтому для наших целей паттерн посетитель вполне подходит.
    2.9. РЕЗЮМЕ
    При проектировании Lexi мы применили восемь различных паттернов:
    „
    „
    компоновщик (196) для представления физической структуры доку- мента;
    „
    „
    стратегия (362) для возможности использования различных алгорит- мов форматирования;
    „
    „
    декоратор (209) для оформления пользовательского интерфейса;
    „
    „
    абстрактная фабрика (113) для поддержки нескольких стандартов оформления;
    „
    „
    мост (184) для поддержки нескольких оконных систем;
    „
    „
    команда (275) для реализации отмены и повтора операций пользова- теля;
    „
    „
    итератор (302) для обхода структур объектов;
    „
    „
    посетитель (379) для поддержки неизвестного заранее числа видов ана- лиза без усложнения реализации структуры документа.

    2.9.
    Резюме
    107
    Ни одно из этих проектных решений не ограничено документо-ориентиро- ванными редакторами вроде Lexi. На самом деле в большинстве нетриви- альных приложений есть возможность воспользоваться многими из этих паттернов, быть может, для других целей. В приложении для финансового анализа паттерн компоновщик можно было бы применить для определения инвестиционных портфелей, разбитых на субпортфели и счета разных ви- дов. Компилятор мог бы использовать паттерн стратегия, чтобы поддержать реализацию разных схем распределения машинных регистров для целевых компьютеров с различной архитектурой. Приложения с графическим ин- терфейсом пользователя вполне могли бы применить паттерны декоратор и команда точно так же, как это сделали мы.
    Хотя мы и рассмотрели несколько крупных проблем проектирования Lexi, осталось гораздо больше таких, которых мы не касались. Но ведь и в кни- ге описаны не только рассмотренные восемь паттернов. Поэтому, изучая остальные паттерны, подумайте о том, как вы могли бы применить их к Lexi.
    А еще лучше — подумайте об их использовании в своих собственных про- ектах!

    1   ...   4   5   6   7   8   9   10   11   ...   38


    написать администратору сайта