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

  • TextConverter

  • TextWidgetConverter ConvertCharacter(char)ConvertFontChange(Font)ConvertParagraph()GetTextWidget()ASCIIText TeXText TextWidget

  • Director Construct() Builder

  • Director (RTFReader) — распорядитель:• конструирует объект, пользуясь интерфейсом Builder; Паттерн Builder (строитель) 127 „„Product

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


    Скачать 6.37 Mb.
    НазваниеЭ. Гамма, Р. Хелм
    АнкорFactorial
    Дата14.03.2022
    Размер6.37 Mb.
    Формат файлаpdf
    Имя файлаPatterny_Obektno-Orientirovannogo_Proektirovania_2020.pdf
    ТипДокументы
    #395452
    страница10 из 38
    1   ...   6   7   8   9   10   11   12   13   ...   38
    118
    Глава 3. Порождающие паттерны она инициализируется экземпляром-прототипом каждого продукта в се- мействе и создает новый продукт путем клонирования этого прототипа.
    Подход на основе прототипов устраняет необходимость создавать новый класс конкретной фабрики для каждого нового семейства продуктов.
    Вот как можно реализовать фабрику на основе прототипов в языке
    Smalltalk. Конкретная фабрика хранит клонируемые прототипы в словаре под названием partCatalog
    . Метод make:
    извлекает прототип и клонирует его:
    make: partName
    ^ (partCatalog at: partName) copy
    У конкретной фабрики есть метод для добавления деталей в каталог:
    addPart: partTemplate named: partName partCatalog at: partName put: partTemplate
    Прототипы добавляются к фабрике путем пометки их символом #:
    aFactory addPart: aPrototype named: #ACMEWidget
    Разновидность метода на базе прототипов возможна в языках, в кото- рых классы являются полноценными объектами (например, Smalltalk и Objective C). В таких языках класс можно представлять себе как вы- рожденный случай фабрики, умеющей создавать только один вид про- дуктов. Можно хранить классы внутри конкретной фабрики, которая создает разные конкретные продукты в переменных. Это очень похоже на прототипы. Такие классы создают новые экземпляры от имени конкретной фабрики. Новая фабрика инициализируется экземпляром конкретной фабрики с классами продуктов, а не путем порождения подкласса. Подобный подход задействует некоторые специфические свойства языка, тогда как «чистый» подход, основанный на прототипах, от языка не зависит.
    Как и для только что рассмотренной фабрики на базе прототипов в Smalltalk, в версии на основе классов будет единственная переменная экземпляра partCatalog
    , представляющая собой словарь, ключом кото- рого является название детали. Но вместо хранения клонируемых про- тотипов partCatalog хранит классы продуктов. Метод make:
    выглядит теперь следующим образом:
    make: partName
    ^ (partCatalog at: partName) new

    Паттерн Abstract Factory (абстрактная фабрика)
    119
    „
    „
    определение расширяемых фабрик. Класс
    AbstractFactory обычно опре- деляет разные операции для всех видов изготавливаемых продуктов.
    Виды продуктов кодируются в сигнатуре операции. Для добавления но- вого вида продуктов нужно изменить интерфейс класса
    AbstractFactory и всех зависящих от него классов.
    Более гибкий, но менее безопасный способ — добавить параметр к опе- рациям, создающим объекты. Данный параметр определяет вид созда- ваемого объекта. Это может быть идентификатор класса, целое число, строка или что-то еще, однозначно описывающее вид продукта. При таком подходе классу
    AbstractFactory нужна только одна операция
    Make с параметром, задающим тип создаваемого объекта. Данный прием применялся в обсуждавшихся выше абстрактных фабриках на основе прототипов и классов.
    Такой вариант проще использовать в динамически типизированных языках вроде Smalltalk, нежели в статически типизированных, каким является C++. Воспользоваться им в C++ можно только в том случае, если у всех объектов имеется общий абстрактный базовый класс или если объекты-продукты могут быть безопасно приведены к корректному типу клиентом, который их запросил. В разделе «Реализация» из опи- сания паттерна фабричный метод (135) показано, как реализовать такие параметризованные операции в C++.
    Но даже если приведение типов не нужно, остается принципиальная про- блема: все продукты возвращаются клиенту одним и тем же абстрактным интерфейсом с уже определенным типом возвращаемого значения. Клиент не может ни различить классы продуктов, ни сделать какие-нибудь пред- положения о них. Если клиенту нужно выполнить операцию, зависящую от подкласса, то она будет недоступна через абстрактный интерфейс. Хотя клиент мог бы выполнить понижающее приведение типа (например, с по- мощью оператора dynamic_cast в C++), это небезопасно и необязательно заканчивается успешно. Здесь мы имеем классический пример компромисса между высокой степенью гибкости и расширяемостью интерфейса.
    Пример кода
    Паттерн абстрактная фабрика мы применим к построению обсуждавшихся в начале этой главы лабиринтов.
    Класс
    MazeFactory может создавать компоненты лабиринтов. Он строит комнаты, стены и двери между комнатами. Например, им можно восполь- зоваться из программы, которая читает план лабиринта из файла, а затем

    120
    Глава 3. Порождающие паттерны создает его, или из приложения, строящего случайный лабиринт. Программы построения лабиринта принимают
    MazeFactory в качестве аргумента, так что программист может сам указать классы комнат, стен и дверей:
    class MazeFactory {
    public:
    MazeFactory();
    virtual Maze* MakeMaze() const
    { return new Maze; }
    virtual Wall* MakeWall() const
    { return new Wall; }
    virtual Room* MakeRoom(int n) const
    { return new Room(n); }
    virtual Door* MakeDoor(Room* r1, Room* r2) const
    { return new Door(r1, r2); }
    };
    Напомним, что функция
    CreateMaze строит небольшой лабиринт, состоящий всего из двух комнат, соединенных одной дверью. В ней жестко «зашиты» имена классов, поэтому воспользоваться функцией для создания лабиринтов с другими компонентами проблематично.
    Следующая версия
    CreateMaze избавлена от подобного недостатка, посколь- ку она получает
    MazeFactory в параметре:
    Maze* MazeGame::CreateMaze (MazeFactory& factory) {
    Maze* aMaze = factory.MakeMaze();
    Room* r1 = factory.MakeRoom(1);
    Room* r2 = factory.MakeRoom(2);
    Door* aDoor = factory.MakeDoor(r1, r2); aMaze->AddRoom(r1); aMaze->AddRoom(r2); r1->SetSide(North, factory.MakeWall()); r1->SetSide(East, aDoor); r1->SetSide(South, factory.MakeWall()); r1->SetSide(West, factory.MakeWall()); r2->SetSide(North, factory.MakeWall()); r2->SetSide(East, factory.MakeWall()); r2->SetSide(South, factory.MakeWall()); r2->SetSide(West, aDoor); return aMaze;
    }

    Паттерн Abstract Factory (абстрактная фабрика)
    121
    Чтобы создать фабрику
    EnchantedMazeFactory для производства волшебных лабиринтов, следует породить подкласс от
    MazeFactory
    . В этом подклассе замещены различные функции класса, так что он возвращает другие под- классы классов
    Room
    ,
    Wall и т. д.:
    class EnchantedMazeFactory : public MazeFactory {
    public:
    EnchantedMazeFactory();
    virtual Room* MakeRoom(int n) const
    { return new EnchantedRoom(n, CastSpell()); }
    virtual Door* MakeDoor(Room* r1, Room* r2) const
    { return new DoorNeedingSpell(r1, r2); }
    protected:
    Spell* CastSpell() const;
    };
    А теперь предположим, что мы хотим построить для некоторой игры лаби- ринт, в комнате которого может быть заложена бомба. Если бомба взрывает- ся, то она разрушает стены (а то и что-нибудь еще). Тогда можно породить от класса
    Room подкласс, отслеживающий, есть ли в комнате бомба и взорвалась ли она. Также понадобится подкласс класса
    Wall для хранения информации о том, были ли повреждены стены. Назовем эти классы соответственно
    RoomWithABomb и
    BombedWall
    И наконец, мы определим класс
    BombedMazeFactory
    , являющийся подклассом
    BombedMazeFactory
    , который создает стены класса
    BombedWall и комнаты класса
    RoomWithABomb
    . В
    BombedMazeFactory надо переопределить всего две функции:
    Wall* BombedMazeFactory::MakeWall () const {
    return new BombedWall;
    }
    Room* BombedMazeFactory::MakeRoom(int n) const {
    return new RoomWithABomb(n);
    }
    Чтобы построить простой лабиринт, в котором могут быть спрятаны бомбы, просто вызовем функцию
    CreateMaze
    , передав ей в параметре
    BombedMazeFactory
    :
    MazeGame game;
    BombedMazeFactory factory;
    game.CreateMaze(factory);

    122
    Глава 3. Порождающие паттерны
    С таким же успехом
    CreateMaze можно передать в параметре
    Enchan ted-
    MazeFactory для построения волшебного лабиринта.
    Отметим, что
    MazeFactory
    — всего лишь набор фабричных методов. Это самый распространенный способ реализации паттерна абстрактная фабрика.
    Еще заметим, что MazeFactory — не абстрактный класс, то есть он работает и как
    AbstractFactory
    , и как
    ConcreteFactory
    . Это еще одна типичная реа- лизация для простых применений паттерна абстрактная фабрика. Поскольку
    MazeFactory
    — конкретный класс, состоящий только из фабричных методов, легко получить новую фабрику
    MazeFactory
    , породив подкласс и заместив в нем необходимые операции.
    В функции
    CreateMaze используется операция
    SetSide для описания сторон комнат. Если она создает комнаты с помощью фабрики
    BombedMazeFactory
    , то лабиринт будет составлен из объектов класса
    RoomWithABomb
    , стороны кото- рых описываются объектами класса
    BombedWall
    . Если классу
    RoomWithABomb потребуется обратиться к членам
    BombedWall
    , не имеющим аналога в его предках, то ссылку на объекты-стены придется преобразовать от типа
    Wall*
    к типу
    BombedWall*
    . Такое понижающее приведение безопасно при условии, что аргумент действительно принадлежит классу
    BombedWall*
    , а это заведомо так, если стены создаются исключительно фабрикой
    BombedMazeFactory
    В динамически типизированных языках вроде Smalltalk приведение, разуме- ется, не нужно, но будет выдано сообщение об ошибке во время выполнения, если объект класса
    Wall встретится вместо ожидаемого объекта подкласса класса
    Wall
    . Использование абстрактной фабрики для создания стен предот- вращает подобные ошибки, гарантируя, что могут быть созданы лишь стены определенных типов.
    Рассмотрим версию
    MazeFactory на языке Smalltalk, в которой есть един- ственная операция make
    , получающая вид изготавливаемого объекта в пара- метре. Конкретная фабрика при этом будет хранить классы изготавливаемых объектов.
    Для начала напишем на Smalltalk эквивалент
    CreateMaze
    :
    createMaze: aFactory
    | room1 room2 aDoor |
    room1 := (aFactory make: #room) number: 1.
    room2 := (aFactory make: #room) number: 2.
    aDoor := (aFactory make: #door) from: room1 to: room2.
    room1 atSide: #north put: (aFactory make: #wall).
    room1 atSide: #east put: aDoor.
    room1 atSide: #south put: (aFactory make: #wall).

    Паттерн Abstract Factory (абстрактная фабрика)
    123
    room1 atSide: #west put: (aFactory make: #wall).
    room2 atSide: #north put: (aFactory make: #wall).
    room2 atSide: #east put: (aFactory make: #wall).
    room2 atSide: #south put: (aFactory make: #wall).
    room2 atSide: #west put: aDoor.
    ^ Maze new addRoom: room1; addRoom: room2; yourself
    В разделе «Реализация» мы уже говорили о том, что классу
    MazeFactory нужна всего одна переменная экземпляра partCatalog
    , предоставляющая словарь, в котором ключом служит класс компонента. Напомним еще раз реализацию метода make
    :
    make: partName
    ^ (partCatalog at: partName) new
    Теперь мы можем создать фабрику
    MazeFactory и воспользоваться ей для реализации createMaze
    . Для создания фабрики будет использоваться метод createMazeFactory класса
    MazeGame
    :
    createMazeFactory
    ^ (MazeFactory new addPart: Wall named: #wall; addPart: Room named: #room; addPart: Door named: #door; yourself)
    BombedMazeFactory и
    EnchantedMazeFactory создаются путем ассоциирования других классов с ключами. Например,
    EnchantedMazeFactory можно создать следующим образом:
    createMazeFactory
    ^ (MazeFactory new addPart: Wall named: #wall;
    addPart: EnchantedRoom named: #room;
    addPart: DoorNeedingSpell named: #door;
    yourself)
    Известные применения
    В библиотеке InterViews [Lin92] для обозначения классов абстрактных фабрик используется суффикс «Kit». Для изготовления объектов пользо- вательского интерфейса с заданным внешним обликом в ней определены абстрактные фабрики
    WidgetKit и
    DialogKit
    . В InterViews также включен класс
    LayoutKit
    , который генерирует разные объекты композиции в зави- симости от того, какая требуется стратегия размещения. Например, разме-

    124
    Глава 3. Порождающие паттерны щение, которое концептуально можно было бы назвать «горизонтальным», может потребовать разных объектов в зависимости от ориентации документа
    (книжной или альбомной).
    В библиотеке ET++ [WGM88] паттерн абстрактная фабрика применяется для достижения переносимости между разными оконными системами (напри- мер, X Window и SunView). Абстрактный базовый класс
    WindowSystem опре- деляет интерфейс для создания объектов, которые представляют ресурсы оконной системы (
    MakeWindow
    ,
    MakeFont
    ,
    MakeColor и т. п.). Его конкретные подклассы реализуют эти интерфейсы для той или иной оконной системы.
    Во время выполнения ET++ создает экземпляр конкретного подкласса
    WindowSystem
    , который уже и порождает объекты, соответствующие ресурсам данной оконной системы.
    Родственные паттерны
    Классы
    AbstractFactory часто реализуются фабричными методами (см. паттерн фабричный метод (135)), но могут быть реализованы и с помощью паттерна прототип (146).
    Конкретная фабрика часто описывается паттерном одиночка (157).
    ПАТТЕРН BUILDER (СТРОИТЕЛЬ)
    Название и классификация паттерна
    Строитель — паттерн, порождающий объекты.
    Назначение
    Отделяет конструирование сложного объекта от его представления, так что в результате одного и того же процесса конструирования могут получаться разные представления.
    Мотивация
    Программа чтения документов в формате RTF (Rich Text Format) должна также «уметь» преобразовывать его во многие другие форматы — напри- мер, в простой ASCII текст или в представление, которое можно отобразить в виджете для ввода текста. Тем не менее, число возможных преобразований заранее неизвестно, поэтому новые преобразования должны легко добав- ляться без изменения программы.

    Паттерн Builder (строитель)
    125
    Проблема решается настройкой класса
    RTFReader с помощью объекта
    TextConverter
    , который мог бы преобразовывать RTF в другой текстовый формат. При разборе документа в формате RTF класс
    RTFReader вызывает
    TextConverter для выполнения преобразования. Всякий раз, как
    RTFReader распознает лексему RTF (простой текст или управляющее слово), для ее преобразования объекту
    TextConverter посылается запрос. Объекты
    TextConverter отвечают как за преобразование данных, так и за представ- ление лексемы в конкретном формате.
    Подклассы
    TextConverter специализируются на различных преобразова- ниях и форматах. Например,
    ASCIIConverter игнорирует запросы на пре- образование чего бы то ни было, кроме простого текста. С другой стороны,
    TeXConverter будет реализовывать операции для всех запросов на получение представления в формате редактора TeX, собирая по ходу необходимую информацию о стилях. А
    TextWidgetConverter будет строить сложный объ- ект пользовательского интерфейса, который позволит пользователю про- сматривать и редактировать текст.
    while (t = получить следующую лексему){
    switch t.Type {
    CHAR:
    builderƒ>ConvertCharacter(t.Char)
    FONT:
    builderƒ>ConvertFontChange(t.Font)
    PARA:
    builderƒ>ConvertParagraph()
    }
    }
    TextConverter
    ConvertCharacter(char)
    ConvertFontChange(Font)
    ConvertParagraph()
    Строители
    RTFReader
    ParseRTF()
    builder
    ASCIIConverter
    ConvertCharacter(char)
    GetASCIIText()
    TeXConverter
    ConvertCharacter(char)
    ConvertFontChange(Font)
    ConvertParagraph()
    GetTeXText()
    TextWidgetConverter
    ConvertCharacter(char)
    ConvertFontChange(Font)
    ConvertParagraph()
    GetTextWidget()
    ASCIIText
    TeXText
    TextWidget
    Класс каждого конвертора принимает механизм создания и сборки сложного объекта и скрывает его за абстрактным интерфейсом. Конвертор отделен от загрузчика, который отвечает за синтаксический разбор RTF-документа.
    В паттерне строитель абстрагированы все эти отношения. В нем любой класс конвертора называется строителем, а загрузчик — распорядителем. В при- менении к рассмотренному примеру строитель отделяет алгоритм интерпре-

    126
    Глава 3. Порождающие паттерны тации формата текста (то есть парсер RTF-документов) от того, как создается и представляется документ в преобразованном формате. Это позволяет повторно использовать алгоритм разбора, реализованный в
    RTFReader
    , для создания разных текстовых представлений RTF-документов; достаточно передать в
    RTFReader различные подклассы класса
    TextConverter
    Применимость
    Основные условия для применения паттерна строитель:
    „
    „
    алгоритм создания сложного объекта не должен зависеть от того, из ка- ких частей состоит объект и как они стыкуются между собой;
    „
    „
    процесс конструирования должен обеспечивать различные представле- ния конструируемого объекта.
    Структура
    Director_Construct()_Builder'>Director
    Construct()
    Builder
    builder
    BuildPart()
    ConcreteBuilder
    BuildPart()
    GetResult()
    Для всех объектов в структуре {
    builder>BuildPart()
    }
    Product
    Участники
    „
    „
    Builder (
    TextConverter
    ) — строитель:
    • задает абстрактный интерфейс для создания частей объекта Product;
    „
    „
    СoncreteBuilder (
    ASCIIConverter
    ,
    TeXConverter
    ,
    TextWidgetConverter
    ) — конкретный строитель:
    • конструирует и собирает вместе части продукта посредством реали- зации интерфейса Builder;
    • определяет создаваемое представление и следит за ним;
    • предоставляет интерфейс для доступа к продукту (например,
    GetASCIIText
    ,
    GetTextWidget
    );
    „
    „
    Director (
    RTFReader
    ) — распорядитель:
    • конструирует объект, пользуясь интерфейсом Builder;

    Паттерн Builder (строитель)
    127
    „
    „
    Product (
    ASCIIText
    ,
    TeXText
    ,
    TextWidget
    ) — продукт:
    • представляет сложный конструируемый объект.
    ConcreteBuilder стро- ит внутреннее представление продукта и определяет процесс его сборки;
    • включает классы, которые определяют составные части, в том числе интерфейсы для сборки конечного результата из частей.
    Отношения
    „
    „
    клиент создает объект-распорядитель
    Director и настраивает его нуж- ным объектом-строителем
    Builder
    ;
    „
    „
    распорядитель уведомляет строителя о том, что нужно построить очеред- ную часть продукта;
    „
    „
    строитель обрабатывает запросы распорядителя и добавляет новые части к продукту;
    „
    „
    клиент забирает продукт у строителя.
    Следующая схема взаимодействий иллюстрирует взаимоотношения строи- теля и распорядителя с клиентом.
    aClient aDirector aConcreteBuilder new ConcreteBuilder new Director(aConcreteBuilder)
    Construct()
    GetResult()
    BuildPartA()
    BuildPartB()
    BuildPartC()
    Результаты
    Паттерн строитель:
    „
    „
    позволяет изменять внутреннее представление продукта. Объект
    Builder предоставляет распорядителю абстрактный интерфейс для конструирования продукта, за которым он может скрыть представле-

    1   ...   6   7   8   9   10   11   12   13   ...   38


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