Э. Гамма, Р. Хелм
Скачать 6.37 Mb.
|
366 Глава 5. Паттерны поведения жестко «зашивается» в класс Context . Реализации алгоритма и контекста смешиваются, что затрудняет понимание, сопровождение и расширение контекста. Кроме того, заменить алгоритм динамически уже не удастся. В результате вы получаете множество родственных классов, отличающих- ся только алгоритмом или поведением. Инкапсуляция алгоритма в от- дельный класс Strategy позволяет изменять его независимо от контекста; стратегии позволяют избавиться от условных конструкций. С паттер- ном стратегия удается отказаться от условных операторов при выборе нужного поведения. Когда различные поведения помещаются в один класс, трудно выбрать нужное без применения условных операторов. Инкапсуляция же каждого поведения в отдельный класс Strategy ре- шает эту проблему. Так, без использования стратегий код для разбиения текста на строки мог бы выглядеть следующим образом: void Composition::Repair () { switch (_breakingStrategy) { case SimpleStrategy: ComposeWithSimpleCompositor(); break; case TeXStrategy: ComposeWithTeXCompositor(); break; // ... } // При необходимости объединить результаты // с существующей композицией } Паттерн стратегия позволяет обойтись без конструкции выбора за счет делегирования задачи разбиения на строки объекту Strategy : void Composition::Repair () { _compositor->Compose(); // При необходимости объединить результаты // с существующей композицией } Если код содержит много условных операторов, то часто это признак того, что нужно применить паттерн стратегия; выбор реализации. Стратегии могут предлагать различные реализации одного и того же поведения. Клиент вправе выбирать подходящую стра- тегию в зависимости от своих требований к быстродействию и памяти; Паттерн Strategy (стратегия) 367 клиенты должны знать о различных стратегиях. Потенциальный не- достаток этого паттерна в том, что для выбора подходящей стратегии клиент должен понимать, чем отличаются разные стратегии. Поэтому наверняка придется раскрыть клиенту некоторые особенности реализа- ции. Отсюда следует, что паттерн стратегия стоит применять лишь тогда, когда различия в поведении важны для клиента; затраты на передачу информации между стратегией и контекстом. Интерфейс Strategy совместно используется всеми подклассами ConcreteStrategy — какой бы сложной или тривиальной ни была их ре- ализация. Поэтому вполне вероятно, что некоторые стратегии не будут пользоваться всей передаваемой им информацией, особенно простые. Это означает, что в отдельных случаях контекст создаст и проинициа- лизирует параметры, которые никому не нужны. Если возникнет про- блема, то между классами Strategy и Context придется установить бо- лее тесную связь; увеличение числа объектов. Применение стратегий увеличивает число объектов в приложении. Иногда эти издержки можно сократить, если реализовать стратегии в виде объектов без состояния, которые могут со- вместно использоваться несколькими контекстами. Остаточное состо- яние хранится в самом контексте и передается при каждом обращении к объекту-стратегии. Совместно используемые стратегии не должны со- хранять состояние между вызовами. В описании паттерна приспособле- нец (231) этот подход обсуждается более подробно. Реализация Рассмотрим следующие вопросы реализации: определение интерфейсов классов Strategy и Context. Интерфейсы классов Strategy и Context должны предоставить объекту класса ConcreteStrategy эффективный доступ к любым данным контекста, и наоборот. Например, Context может передавать данные в параметрах операций класса Strategy . Тем самым разрывается тесная связь между контекстом и стратегией. С другой стороны, при этом контекст может передавать данные, которые стратегии не нужны. Другой способ — передача самого контекста в аргументе. В таком случае стратегия может явно запрашивать у него данные; также стратегия может хранить ссылку на свой контекст, так что передавать вообще ничего не придется. И в том, и в другом случаях стратегия может запрашивать толь- 368 Глава 5. Паттерны поведения ко ту информацию, которая реально необходима. Но тогда в контексте должен быть определен более развитый интерфейс к своим данным, что несколько усиливает связанность классов Strategy и Context Выбор подхода зависит от конкретного алгоритма и требований, которые он предъявляет к данным; стратегии как параметры шаблона. В C++ для настройки класса стра- тегией можно использовать шаблоны. Этот способ хорош, только если: (1) стратегия определяется на этапе компиляции, и (2) ее не нужно менять во время выполнения. Тогда настраиваемый класс (например, Context ) определяется в виде шаблона, для которого класс Strategy яв- ляется параметром: template class Context { void Operation() { theStrategy.DoAlgorithm(); } // ... private: AStrategy theStrategy; }; Затем этот класс настраивается классом Strategy в момент создания экземпляра: class MyStrategy { public: void DoAlgorithm(); }; Context При использовании шаблонов отпадает необходимость в абстрактном классе для определения интерфейса Strategy . Кроме того, передача стратегии в параметре шаблона позволяет статически связать стратегию с контекстом, вследствие чего повышается эффективность программы; объекты-стратегии можно не задавать. Класс Context можно упро- стить, если для него нормально не иметь никакой стратегии. Прежде чем обращаться к объекту Strategy , объект Context проверяет наличие стра- тегии. Если да, то работа продолжается как обычно, в противном случае контекст реализует некое поведение по умолчанию. Преимущество та- кого подхода в том, что клиентам вообще не нужно иметь дело со стра- тегиями, если их устраивает поведение по умолчанию. Паттерн Strategy (стратегия) 369 Пример кода Мы приведем высокоуровневый код для примера из раздела «Мотивация», в основе которого лежат классы Composition и Compositor из библиотеки InterViews [LCI+92]. В классе Composition есть коллекция экземпляров класса Component , пред- ставляющих текстовые и графические элементы документа. Компоновщик, то есть некоторый подкласс класса Compositor , составляет из объектов-ком- понентов строки, реализуя ту или иную стратегию разбиения на строки. С каждым объектом ассоциирован его естественный размер, а также свойства растягиваемости и сжимаемости. Растягиваемость определяет, насколько возможно увеличивать объект по сравнению с его естественным размером, а сжимаемость — насколько возможно этот размер уменьшать. Композиция передает эти значения компоновщику, который использует их, чтобы найти оптимальное место для разбиения строки. class Composition { public: Composition(Compositor*); void Repair(); private: Compositor* _compositor; Component* _components; // Список компонентов int _componentCount; // Количество компонентов int _lineWidth; // Ширина строки в композиции int* _lineBreaks; // Позиции точек разбиения строки // (измеренные в компонентах) int _lineCount; // Количество строк }; Когда возникает необходимость изменить расположение элементов, компо- зиция запрашивает у компоновщика позиции точек разбиения строк. При этом она передает компоновщику три массива, в которых содержатся есте- ственные размеры, величины растягиваемости и сжимаемости компонентов. Кроме того, передается число компонентов, ширина строки и массив, в кото- рый компоновщик должен поместить позиции точек разрыва. Компоновщик возвращает число рассчитанных им точек разбиения. Интерфейс класса Compositor позволяет композиции передать компоновщи- ку всю необходимую ему информацию. Пример передачи данных стратегии: class Compositor { public: virtual int Compose( Coord natural[], Coord stretch[], Coord shrink[], 370 Глава 5. Паттерны поведения int componentCount, int lineWidth, int breaks[] ) = 0; protected: Compositor(); }; Заметим, что Compositor — это абстрактный класс. В его конкретных под- классах определяются различные стратегии разбиения на строки. Композиция вызывает своего компоновщика из операции Repair , которая прежде всего инициализирует массивы, содержащие естественные разме- ры, растягиваемость и сжимаемость каждого компонента (подробности мы опускаем). Затем Repair вызывает компоновщика для получения позиций точек разбиения и, наконец, отображает документ (этот код также опущен): void Composition::Repair () { Coord* natural; Coord* stretchability; Coord* shrinkability; int componentCount; int* breaks; // Подготовить массивы с желательными размерами компонентов // ... // Определить, где должны находиться точки разбиения: int breakCount; breakCount = _compositor->Compose( natural, stretchability, shrinkability, componentCount, _lineWidth, breaks ); // Разместить компоненты с учетом точек разбиения // ... } Обратимся к подклассам класса Compositor . Класс SimpleCompositor для определения позиций точек разрыва анализирует компоненты по одному: class SimpleCompositor : public Compositor { public: SimpleCompositor(); virtual int Compose( Coord natural[], Coord stretch[], Coord shrink[], int componentCount, int lineWidth, int breaks[] ); // ... }; Паттерн Strategy (стратегия) 371 Класс TeXCompositor использует более глобальную стратегию. Он рассма- тривает абзац целиком, принимая во внимание размеры и растягиваемость компонентов. Данный класс также пытается минимизировать ширину про- пусков между компонентами: class TeXCompositor : public Compositor { public: TeXCompositor(); virtual int Compose( Coord natural[], Coord stretch[], Coord shrink[], int componentCount, int lineWidth, int breaks[] ); // ... }; Класс ArrayCompositor разбивает компоненты на строки, оставляя между ними равные промежутки: class ArrayCompositor : public Compositor { public: ArrayCompositor(int interval); virtual int Compose( Coord natural[], Coord stretch[], Coord shrink[], int componentCount, int lineWidth, int breaks[] ); // ... }; Не все из этих классов используют в полном объеме информацию, передан- ную Compose SimpleCompositor игнорирует растягиваемость компонентов, принимая во внимание только их естественную ширину. TeXCompositor ис- пользует всю переданную информацию, а ArrayCompositor игнорирует ее. При создании экземпляра класса Composition ему передается компоновщик, которым собираетесь пользоваться: Composition* quick = new Composition(new SimpleCompositor); Composition* slick = new Composition(new TeXCompositor); Composition* iconic = new Composition(new ArrayCompositor(100)); Интерфейс класса Compositor тщательно спроектирован для поддержки всех алгоритмов размещения, которые могут быть реализованы в подклассах. Вряд ли вам захочется изменять данный интерфейс при появлении каждого нового подкласса, поскольку это означало бы переписывание уже существу- ющих подклассов. В общем случае именно интерфейсы классов Strategy 372 Глава 5. Паттерны поведения и Context определяют, насколько хорошо паттерн стратегия соответствует своему назначению. Известные применения Библиотеки ET++ [WGM88] и InterViews используют стратегии для инкап- суляции алгоритмов разбиения на строки — так, как мы только что видели. В системе RTL для оптимизации кода компиляторов [JML92] с помо- щью стратегий определяются различные схемы распределения регистров ( RegisterAllocator ) и политики управления потоком команд ( RISCscheduler , CISCscheduler ). Это позволяет гибко настраивать оптимизатор для разных целевых машинных архитектур. Каркас ET++ SwapsManager предназначен для построения программ, рас- считывающих цены для различных финансовых инструментов [EG92]. Ключевыми абстракциями для него являются Instrument (инструмент) и YieldCurve (кривая дохода). Различные инструменты реализованы как подклассы класса Instrument YieldCurve рассчитывает коэффициенты дис- контирования, на основе которых вычисляется текущее значение будущего движения ликвидности. Оба класса делегируют часть своего поведения объектам-стратегиям класса Strategy . В каркасе присутствует семейство конкретных стратегий для генерирования движения ликвидности, оценки оборотов и вычисления коэффициентов дисконтирования. Можно создавать новые механизмы расчетов, конфигурируя классы Instrument и YieldCurve другими объектами конкретных стратегий. Этот подход поддерживает как использование существующих реализаций стратегий в различных сочета- ниях, так и определение новых. В библиотеке компонентов Гради Буча [BV90] стратегии используются как аргументы шаблонов. В классах коллекций поддерживаются три раз- новидности стратегий распределения памяти: управляемая (распределение из пула), контролируемая (распределение и освобождение защищаются блокировками) и неуправляемая (стандартное распределение памяти). Стра- тегия передается классу коллекции в аргументе шаблона в момент создания экземпляра. Например, для коллекции UnboundedCollection , в которой ис- пользуется неуправляемая стратегия, экземпляр создается конструкцией Un boundedCollection RApp — система для проектирования топологии интегральных схем [GA89, AG90]. Задача RApp — проложить контакты между различными подсисте- мами на схеме. Алгоритмы трассировки в RApp определены как подклассы абстрактного класса Router , который является стратегией. Паттерн Template Method (шаблонный метод) 373 В библиотеке ObjectWindows фирмы Borland [Bor94] стратегии использу- ются в диалоговых окнах для проверки правильности введенных пользова- телем данных. Например, можно контролировать, что число принадлежит заданному диапазону, а в данном поле должны быть только цифры. Не ис- ключено, что проверка корректности введенной строки потребует поиска данных по справочной таблице. Для инкапсуляции стратегий проверки в ObjectWindows используются объ- екты класса Validator — частный случай паттерна стратегия. Поля для ввода данных делегируют стратегию контроля необязательному объекту Validator Клиент при необходимости присоединяет таких проверяющих к полю (при- мер необязательной стратегии). В момент закрытия диалогового окна поля «просят» своих контролеров проверить правильность данных. В библиотеке имеются классы контролеров для наиболее распространенных случаев, на- пример RangeValidator для проверки принадлежности числа диапазону. Но клиент может легко определить и собственные стратегии проверки, порождая подклассы от класса Validator Родственные паттерны Приспособленец (231): объекты-стратегии в большинстве случаев подходят для применения паттерна приспособленец. ПАТТЕРН TEMPLATE METHOD (ШАБЛОННЫЙ МЕТОД) Название и классификация паттерна Шаблонный метод — паттерн поведения классов. Назначение Шаблонный метод определяет основу алгоритма и позволяет подклассам пере- определить некоторые шаги алгоритма, не изменяя его структуру в целом. Мотивация Рассмотрим каркас приложения, в котором имеются классы Application и Document . Класс Application отвечает за открытие существующих докумен- тов, хранящихся во внешнем формате (например, в файле). Объект класса Document представляет информацию документа после его прочтения из файла. Приложения, построенные на базе этого каркаса, могут порождать подклассы от классов Application и Document , отвечающие конкретным потребностям. 374 Глава 5. Паттерны поведения Например, графический редактор определит подклассы DrawApplication и DrawDocument , а электронная таблица — подклассы SpreadsheetApplication и SpreadsheetDocument AddDocument() OpenDocument() DoCreateDocument() CanOpenDocument() AboutToOpenDocument() Application DoCreateDocument() CanOpenDocument() AboutToOpenDocument() MyApplication Document MyDocument DoRead() Save() Open() Close() DoRead() docs return new MyDocument В абстрактном классе Application определен алгоритм открытия и чтения документа в операции OpenDocument : void Application::OpenDocument (const char* name) { if (!CanOpenDocument(name)) { // Обработать документ невозможно return; } Document* doc = DoCreateDocument(); if (doc) { _docs->AddDocument(doc); AboutToOpenDocument(doc); doc->Open(); doc->DoRead(); } } Операция OpenDocument определяет все шаги открытия документа. Она проверяет, возможно ли открыть документ, создает объект класса Document , добавляет его к набору документов и читает документ из файла. Операцию вида OpenDocument мы будем называть шаблонным методом, описывающим алгоритм в категориях абстрактных операций, которые за- мещены в подклассах для получения нужного поведения. Подклассы класса Паттерн Template Method (шаблонный метод) 375 Application проверяют возможность открытия ( CanOpenDocument ) и созда- ния документа ( DoCreateDocument ). Подклассы класса Document считывают документ ( DoRead ). Шаблонный метод определяет также операцию, которая позволяет подклассам Application получить информацию о том, что доку- мент вот-вот будет открыт ( AboutToOpenDocument ). Определяя некоторые шаги алгоритма с помощью абстрактных операций, шаблонный метод фиксирует их последовательность, но позволяет реали- зовать их в подклассах классов Application и Document Применимость Основные условия для применения паттерна шаблонный метод: однократное использование инвариантных частей алгоритма, при этом ре- ализация изменяющегося поведения остается на усмотрение подклассов; необходимость вычленить и локализовать в одном классе поведение, общее для всех подклассов, чтобы избежать дублирования кода. Это хороший пример техники «вынесения за скобки с целью обобщения», описанной в работе Уильяма Опдайка (William Opdyke) и Ральфа Джонсона (Ralph Johnson) [OJ93]. Сначала выявляются различия в су- ществующем коде, которые затем выносятся в отдельные операции. В конечном итоге различающиеся фрагменты кода заменяются шаблон- ным методом, из которого вызываются новые операции; управление расширениями подклассов. Шаблонный метод можно опре- делить так, что он будет вызывать операции-зацепки (hooks) — см. раз- дел «Результаты» — в определенных точках, разрешив тем самым рас- ширение только в этих точках. Структура TemplateMethod() PrimitiveOperation1() PrimitiveOperation2() |