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

  • Graphic

  • Subject

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


    Скачать 6.37 Mb.
    НазваниеЭ. Гамма, Р. Хелм
    АнкорFactorial
    Дата14.03.2022
    Размер6.37 Mb.
    Формат файлаpdf
    Имя файлаPatterny_Obektno-Orientirovannogo_Proektirovania_2020.pdf
    ТипДокументы
    #395452
    страница21 из 38
    1   ...   17   18   19   20   21   22   23   24   ...   38

    240
    Глава 4. Структурные паттерны
    В подклассе
    Character хранится только код символа:
    class Character : public Glyph {
    public:
    Character(char);
    virtual void Draw(Window*, GlyphContext&);
    private:
    char _charcode;
    };
    Чтобы не выделять память для шрифта в каждом глифе, будем хранить этот атрибут во внешнем объекте класса
    GlyphContext
    . Данный объект служит хранилищем внешнего состояния. Он поддерживает соответствие между глифом и его шрифтом (а также любыми другими графическими атрибу- тами) в различных контекстах. Любой операции, у которой должна быть информация о шрифте глифа в данном контексте, в качестве параметра будет передаваться экземпляр
    GlyphContext
    . У него операция и может запросить нужные сведения. Контекст определяется положением глифа в структуре, поэтому операции обхода и манипулирования потомками должны обновлять
    GlyphContext при каждом использовании:
    class GlyphContext {
    public:
    GlyphContext();
    virtual

    GlyphContext();
    virtual void Next(int step = 1);
    virtual void Insert(int quantity = 1);
    virtual Font* GetFont();
    virtual void SetFont(Font*, int span = 1);
    private:
    int _index;
    BTree* _fonts;
    };
    Объекту
    GlyphContext должно быть известно о текущем положении в структуре глифов во время ее обхода. Операция
    GlyphContext::Next увеличивает переменную
    _index в процессе обхода структуры. Подклассы класса
    Glyph
    , имеющие потомков (например,
    Row и
    Column
    ), должны реа- лизовывать операцию
    Next так, чтобы она вызывала
    GlyphContext::Next в каждой точке обхода.
    Операция
    GlyphContext::GetFont использует переменную
    _index в качестве ключа для структуры
    BTree
    , в которой хранится информация соответствия

    Паттерн Flyweight (приспособленец)
    241
    между глифами и шрифтами. Каждый узел дерева помечен длиной строки, для которой он предоставляет информацию о шрифте. Листья дерева ука- зывают на шрифт, а внутренние узлы разбивают строку на подстроки — по одной для каждого потомка.
    Рассмотрим фрагмент текста, представляющий собой композицию глифов.
    Структура
    BTree
    , в которой хранится информация о шрифтах, может вы- глядеть так:
    500
    300
    199
    100
    6
    194
    8
    1
    3
    187
    1
    TimesItalic 12
    TimesBold 12
    Times 12
    Times 24
    Courier 24

    242
    Глава 4. Структурные паттерны
    Внутренние узлы определяют диапазоны индексов глифов. Дерево об- новляется в ответ на изменение шрифта, а также при каждом добавлении и удалении глифов из структуры. Например, если предположить, что теку- щей точке обхода соответствует индекс 102, то следующий код установит шрифт каждого символа в слове «expect» таким же, как у близлежащего текста (то есть times12
    — экземпляр класса
    Font для шрифта Times Roman размером 12 пунктов):
    GlyphContext gc;
    Font* times12 = new Font("Times-Roman-12");
    Font* timesItalic12 = new Font("Times-Italic-12");
    // ...
    gc.SetFont(times12, 6);
    Новая структура
    BTree выглядит так (изменения выделены черным цве- том):
    500
    300
    199
    8
    1
    3
    187
    1
    TimesBold 12
    Times 12
    Times 24
    Courier 24
    Предположим, перед «expect» добавляется слово «don't » (включая пробел после него), написанное шрифтом Times Italic размером 12 пунктов. Следу- ющий код проинформирует объект gc об этом (предполагается, что текущей позиции все еще соответствует индекс 102):
    gc.Insert(6); gc.SetFont(timesItalic12, 6);
    Структура
    BTree приходит к следующему виду:

    Паттерн Flyweight (приспособленец)
    243
    506
    306
    199
    100
    6
    200
    8
    1
    3
    187
    1
    TimesItalic 12
    TimesBold 12
    Times 12
    Times 24
    Courier 24
    При запросе шрифта текущего глифа объект
    GlyphContext спускается вниз по дереву, суммируя индексы, пока не будет найден шрифт для текущего индекса. Поскольку шрифт меняется нечасто, размер дерева невелик по сравнению с размером структуры глифов. Это позволяет уменьшить затраты на хранение без заметного увеличения времени поиска
    1
    Наконец, нужна еще фабрика
    FlyweightFactory
    , которая создает глифы и обеспечивает их корректное совместное использование. Класс
    GlyphFactory создает объекты
    Character и глифы других видов. Совместно используются только объекты
    Character
    . Составных глифов гораздо больше, и их суще- ственное состояние (то есть множество потомков) в любом случае является внутренним:
    const int NCHARCODES = 128;
    class GlyphFactory {
    public:
    GlyphFactory();
    virtual GlyphFactory();
    virtual Character* CreateCharacter(char);
    virtual Row* CreateRow();
    virtual Column* CreateColumn();
    // ...
    private:
    Character* _character[NCHARCODES];
    };
    1
    Время поиска в этой схеме пропорционально частоте смены шрифта. Наименьшая производительность достигается, когда шрифт меняется на каждом символе, но на практике это бывает редко.

    244
    Глава 4. Структурные паттерны
    Массив
    _character содержит указатели на глифы
    Character
    , индексиро- ванные кодом символа. Конструктор инициализирует этот массив нулями:
    GlyphFactory::GlyphFactory () {
    for (int i = 0; i < NCHARCODES; ++i) {
    _character[i] = 0;
    }
    }
    Операция
    CreateCharacter ищет символ в массиве и возвращает соответ- ствующий глиф, если он существует. В противном случае
    CreateCharacter создает глиф, помещает его в массив и затем возвращает:
    Character* GlyphFactory::CreateCharacter (char c) {
    if (!_character[c]) {
    _character[c] = new Character(c);
    }
    return _character[c];
    }
    Остальные операции просто создают новый объект при каждом обращении, так как несимвольные глифы не используются совместно:
    Row* GlyphFactory::CreateRow () {
    return new Row;
    }
    Column* GlyphFactory::CreateColumn () {
    return new Column;
    }
    Эти операции можно было бы опустить и позволить клиентам напрямую создавать экземпляры глифов, не используемых совместно. Но если позже вы решите сделать их тоже совместно используемыми, то придется изменять клиентский код, в котором они создаются.
    Известные применения
    Концепция объектов-приспособленцев впервые была описана и исполь- зована как прием проектирования в библиотеке InterViews 3.0 [CL90]. Ее разработчики построили мощный редактор документов Doc, чтобы доказать практическую полезность подобной идеи. В Doc объекты-глифы использу- ются для представления любого символа документа. Редактор строит по од- ному экземпляру глифа для каждого сочетания символа и стиля (в котором

    Паттерн Flyweight (приспособленец)
    245
    определены все графические атрибуты). Таким образом, внутреннее состо- яние символа состоит из его кода и информации о стиле (индекс в таблице стилей)
    1
    . Следовательно, внешней оказывается только позиция, поэтому Doc работает быстро. Документы представляются классом
    Document
    , который выполняет функции фабрики
    FlyweightFactory
    . Измерения показали, что реализованное в Doc совместное использование символов-приспособленцев весьма эффективно. В типичном случае для документа из 180 тысяч знаков достаточно создать только 480 объектов-символов.
    В каркасе ET++ [WGM88] приспособленцы используются для поддержки независимого оформления
    2
    . Его стандарт определяет расположение элемен- тов пользовательского интерфейса (полос прокрутки, кнопок, меню и пр., в совокупности именуемых виджетами) и их оформления (тени и т. д.).
    Виджет делегирует заботу о своем расположении и изображении отдельному объекту
    Layout
    . Изменение этого объекта позволит сменить оформление даже на стадии выполнения.
    Для каждого класса виджета имеется соответствующий класс
    Layout
    (на- пример,
    ScrollbarLayout
    ,
    MenubarLayout и т. д.). Очевидная проблема такого решения состоит в том, что использование отдельных классов приводит к уд- воению числа объектов пользовательского интерфейса, так как для каждого интерфейсного объекта создается дополнительный объект
    Layout
    . Чтобы избавиться от расходов, объекты
    Layout реализуются в виде приспособлен- цев. Они прекрасно подходят на эту роль, так как заняты преимущественно определением поведения и им легко передать тот небольшой объем внешней информации о состоянии, необходимый для размещения или прорисовки объекта.
    Объекты
    Layout создаются и управляются объектами класса
    Look
    . Класс
    Look

    это абстрактная фабрика (113), которая производит объекты
    Layout с помощью таких операций, как
    GetButtonLayout
    ,
    GetMenuBarLayout и т. д.
    Для каждого стандарта внешнего облика у класса
    Look есть соответствующий подкласс (
    MotifLook
    ,
    OpenLook и т. д.).
    Кстати говоря, объекты
    Layout
    — это, по существу, стратегии (см. описание паттерна стратегия (362)). Таким образом, мы имеем пример объекта-стра- тегии, реализованный в виде приспособленца.
    1
    В приведенном выше примере кода информация о стиле вынесена наружу, так что внутреннее состояние — это только код символа.
    2
    Другой подход к обеспечению независимого оформления представлен в описании паттерна абстрактная фабрика

    246
    Глава 4. Структурные паттерны
    Родственные паттерны
    Паттерн приспособленец часто используется в сочетании с компоновщиком
    (196) для реализации логической иерархической структуры в виде аци- клического направленного графа с совместно используемыми листовыми вершинами.
    Часто наилучшим способом реализации объектов состояния (352) и страте- гии (362) является паттерн приспособленец.
    ПАТТЕРН PROXY (ЗАМЕСТИТЕЛЬ)
    Название и классификация паттерна
    Заместитель — паттерн, структурирующий объекты.
    Назначение
    Является суррогатом другого объекта и контролирует доступ к нему.
    Другие названия
    Surrogate
    (суррогат).
    Мотивация
    Одна из причин для управления доступом к объекту — возможность отложить затраты на создание и инициализацию до момента, когда возникнет фактиче- ская необходимость в объекте. Рассмотрим редактор документов, в котором в документы могут встраиваться графические объекты. Затраты на создание некоторых таких объектов (например, больших растровых изображений) могут быть весьма значительными. Но документ должен открываться быстро, поэтому следует избегать создания всех «тяжелых» объектов на стадии откры- тия (и вообще это излишне, поскольку не все они будут видны одновременно).
    В связи с такими ограничениями кажется разумным создавать «тяжелые» объекты по требованию. Это означает «когда изображение становится видимым». Но что поместить в документ вместо изображения? И как без усложнения реализации редактора скрыть тот факт, что изображение соз- дается по требованию? Например, оптимизация не должна отражаться на коде, отвечающем за рисование и форматирование.
    Решение состоит в том, чтобы использовать другой объект — заместитель изображения, который временно подставляется вместо реального изобра-

    Паттерн Proxy (заместитель)
    247
    жения. Заместитель ведет себя точно так же, как само изображение, и при необходимости создает его экземпляр.
    На диске
    В памяти
    aTextDocument
    Изображение
    anImageProxy
    Имя файла
    anImage
    Данные
    Заместитель создает настоящее изображение, только если редактор документа вызовет операцию
    Draw
    . Все последующие запросы заместитель переадресует непосредственно изображению. Поэтому после создания изображения он должен сохранить ссылку на него.
    Предположим, что изображения хранятся в отдельных файлах. В таком случае мы можем использовать имя файла как ссылку на реальный объект.
    Заместитель хранит также размер изображения, то есть длину и ширину.
    «Зная» ее, заместитель может отвечать на запросы форматера о своем раз- мере, не создавая экземпляр изображения.
    На следующей диаграмме классов этот пример показан более подробно.
    Graphic
    DocumentEditor
    Draw()
    GetExtent()
    Store()
    Load()
    ImageProxy
    Draw()
    GetExtent()
    Store()
    Load()
    fileName extent
    Image
    Draw()
    GetExtent()
    Store()
    Load()
    imageImp extent image if (image == 0) {
    image = LoadImage(fileName);
    }
    image–>Draw()
    if (image == 0) {
    return extent;
    } else {
    return image–>GetExtent();
    }

    248
    Глава 4. Структурные паттерны
    Редактор документов получает доступ к встроенным изображениям только через интерфейс, определенный в абстрактном классе
    Graphic
    ImageProxy
    — это класс для представления изображений, создаваемых по требованию.
    В
    ImageProxy хранится имя файла, играющее роль ссылки на изображение, которое находится на диске. Имя файла передается конструктору класса
    ImageProxy
    В объекте
    ImageProxy находятся также ограничивающий прямоугольник изо- бражения и ссылка на экземпляр реального объекта
    Image
    . Ссылка остается недействительной, пока заместитель не создаст экземпляр реального изобра- жения. Операция
    Draw гарантирует, что изображение будет создано до того, как заместитель переадресует ему запрос. Операция
    GetExtent переадресует запрос изображению, только если его экземпляр уже создан; в противном случае
    ImageProxy возвращает те размеры, которые хранит сам.
    Применимость
    Паттерн заместитель применим во всех случаях, когда возникает необходи- мость сослаться на объект более гибким или нетривиальным способом, чем при использовании простого указателя. Несколько типичных ситуаций, в которых заместитель может оказаться полезным:
    „
    „
    удаленный заместитель предоставляет локального представителя для объекта, находящегося в другом адресном пространстве. В системе
    NEXTSTEP [Add94] для этой цели применяется класс
    NXProxy
    . За- местителя такого рода Джеймс Коплиен [Cop92] называет «послом»
    (Ambassador);
    „
    „
    виртуальный заместитель создает «тяжелые» объекты по требованию.
    Примером может служить класс
    ImageProxy
    , описанный в разделе «Мо- тивация»;
    „
    „
    защищающий заместитель контролирует доступ к исходному объекту.
    Такие заместители полезны, когда для разных объектов определены различные права доступа. Например, в операционной системе Choices
    [CIRM93] объекты
    KernelProxy ограничивают права доступа к объек- там операционной системы;
    „
    „
    «умная» ссылка — это замена обычного указателя. Она позволяет вы- полнить дополнительные действия при доступе к объекту. К типичным применениям такой ссылки можно отнести:
    • подсчет числа ссылок на реальный объект, с тем чтобы занимаемую им память можно было освободить автоматически, когда не останется

    Паттерн Proxy (заместитель)
    249
    ни одной ссылки (такие ссылки называют еще «умными» указателями
    [Ede92]);
    • загрузку объекта из долгосрочного хранилища в память при первом обращении к нему;
    • проверку и установку блокировки на реальный объект при обращении к нему, чтобы никакой другой объект не смог в это время изменить его.
    Структура

    realSubject–>Request();

    Subject
    Клиент
    Request()

    RealSubject
    Request()

    Proxy
    Request()

    realSubject
    Вот как может выглядеть схема объектов для структуры с заместителем во время выполнения.
    aClient
    Субъект
    aProxy
    Реальный субъект
    aRealSubject
    Участники
    „
    „
    Proxy (ImageProxy) — заместитель:
    • хранит ссылку, которая позволяет заместителю обратиться к реально- му субъекту. Объект класса
    Proxy может обращаться к объекту класса
    Subject
    , если интерфейсы классов
    RealSubject и
    Subject одинаковы;
    • предоставляет интерфейс, идентичный интерфейсу
    Subject
    , так что заместитель всегда может быть подставлен вместо реального субъекта;
    • контролирует доступ к реальному субъекту и может отвечать за его создание и удаление;

    250
    Глава 4. Структурные паттерны
    • прочие обязанности зависят от вида заместителя:
    — удаленный заместитель отвечает за кодирование запроса и его аргументов и отправление закодированного запроса реальному субъекту в другом адресном пространстве;
    — виртуальный заместитель может кэшировать дополнительную информацию о реальном субъекте, чтобы отложить его создание.
    Например, класс
    ImageProxy из раздела «Мотивация» кэширует размеры реального изображения;
    — защищающий заместитель проверяет, имеет ли вызывающий объ- ект необходимые для выполнения запроса права;
    „
    „
    Subject (
    Graphic
    ) — субъект:
    • определяет общий для
    RealSubject и
    Proxy интерфейс, так что класс
    Proxy можно использовать везде, где ожидается
    RealSubject
    ;
    „
    „
    RealSubject (
    Image
    ) — реальный субъект:
    • определяет реальный объект, представленный заместителем.
    Отношения
    Proxy при необходимости переадресует запросы объекту
    RealSubject
    . Детали зависят от вида заместителя.
    Результаты
    С помощью паттерна заместитель при доступе к объекту вводится дополни- тельный уровень косвенности. У этого подхода есть много вариантов в за- висимости от вида заместителя:
    „
    „
    удаленный заместитель может скрыть тот факт, что объект находится в другом адресном пространстве;
    „
    „
    виртуальный заместитель может выполнять оптимизацию, например создание объекта по требованию;
    „
    „
    защищающий заместитель и «умная» ссылка позволяют решать допол- нительные задачи при доступе к объекту.
    Есть еще одна оптимизация, которую паттерн заместитель иногда скрыва- ет от клиента. Она называется копированием при записи (copy-on-write) и имеет много общего с созданием объекта по требованию. Копирование большого и сложного объекта — очень затратная операция. Если копия не модифицировалась, то нет смысла эту цену платить. Если отложить процесс

    Паттерн Proxy (заместитель)
    251
    копирования, применив паттерн заместитель, то можно быть уверенным, что эта операция произойдет только тогда, когда он действительно был изменен.
    Чтобы во время записи можно было копировать, необходимо подсчитывать ссылки на субъект. Копирование заместителя просто увеличивает счетчик ссылок. И только тогда, когда клиент запрашивает операцию, изменяющую субъект, заместитель действительно выполняет копирование. Одновремен- но заместитель должен уменьшить счетчик ссылок. Когда счетчик ссылок становится равным нулю, субъект уничтожается.
    Копирование при записи может существенно уменьшить плату за копиро- вание «тяжелых» субъектов.
    Реализация
    При реализации паттерна заместитель можно использовать следующие воз- можности языка:
    „
    „
    перегрузку оператора обращения к членам класса в C++. Язык C++ под- держивает перегрузку оператора обращения к членам класса
    ->
    . Это позволяет производить дополнительные действия при любом разыме- новании указателя на объект. Для реализации некоторых видов заме- стителей это оказывается полезно, поскольку заместитель ведет себя аналогично указателю.
    В следующем примере показано, как воспользоваться данным приемом для реализации виртуального заместителя
    ImagePtr
    :
    class Image;
    extern Image* LoadAnImageFile(const char*);
    // внешняя функция class ImagePtr {
    public:
    ImagePtr(const char* imageFile);
    virtual ImagePtr();
    virtual Image* operator->();
    virtual Image& operator*();
    private:
    Image* LoadImage();
    private:
    Image* _image;
    const char* _imageFile;
    };
    ImagePtr::ImagePtr (const char* theImageFile) {

    252
    Глава 4. Структурные паттерны
    _imageFile = theImageFile;
    _image = 0;
    }
    Image* ImagePtr::LoadImage () { if (_image == 0) {
    _image = LoadAnImageFile(_imageFile);
    } return _image;
    }
    Перегруженные операторы
    ->
    и
    *
    используют операцию
    LoadImage для возврата клиенту изображения, хранящегося в переменной
    _image
    (при необходимости загрузив его):
    Image* ImagePtr::operator-> () {
    return LoadImage();
    }
    Image& ImagePtr::operator* () {
    return *LoadImage();
    }
    Такой подход позволяет вызывать операции объекта
    Image через объек- ты
    ImagePtr
    , не заботясь о том, чтобы включить их в интерфейс данного класса:
    ImagePtr image = ImagePtr("anImageFileName"); image->Draw(Point(50, 100));
    // (image.operator->())->Draw(Point(50, 100))
    Обратите внимание, что заместитель изображения ведет себя подобно указателю, но не объявлен как указатель на
    Image
    . Это означает, что использовать его в точности как настоящий указатель на
    Image нельзя.
    Поэтому при таком подходе клиентам следует трактовать объекты
    Image и
    ImagePtr по-разному.
    Перегрузка оператора доступа не является лучшим решением для всех видов заместителей. Некоторым из них должно быть точно известно,
    какая операция вызывается, а в таких случаях перегрузка оператора до- ступа не работает.
    Возьмем пример виртуального заместителя, обсуждавшийся в разделе
    «Мотивация». Изображение нужно загружать в точно определенное время — при вызове операции
    Draw
    , а не при каждом обращении к нему.

    Паттерн Proxy (заместитель)
    253
    Перегрузка оператора доступа не позволяет различить подобные случаи.
    В такой ситуации придется вручную реализовать каждую операцию за- местителя, переадресующую запрос субъекту.
    Обычно все эти операции очень похожи друг на друга, как видно из при- мера кода в одноименном разделе. Они проверяют, что запрос корректен, что объект-адресат существует и т. д., а потом уже перенаправляют ему запрос. Писать этот код снова и снова надоедает, поэтому нередко он автоматически генерируется препроцессором;
    „
    „
    метод doesNotUnderstand в Smalltalk. В языке Smalltalk есть возможность, позволяющая автоматически поддерживать переадресацию запросов.
    При отправке клиентом сообщения, для которого у получателя нет со- ответствующего метода, Smalltalk вызывает метод doesNotUnderstand: aMessage
    . Заместитель может переопределить doesNotUnderstand так, что сообщение будет переадресовано субъекту.
    Чтобы гарантировать, что запрос будет перенаправлен субъекту, а не просто тихо поглощен заместителем, класс
    Proxy можно определить так, что он не станет понимать никаких сообщений. Smalltalk позволяет это сделать, надо лишь, чтобы у
    Proxy не было суперкласса
    1
    Главный недостаток метода doesNotUnderstand:
    в том, что в большин- стве Smalltalk-систем имеется несколько специальных сообщений, об- рабатываемых непосредственно виртуальной машиной, а в этом случае стандартный механизм поиска методов обходится. Правда, единственной такой операцией, написанной в классе
    Object
    (а следовательно, способной повлиять на заместителей), является тождество
    ==
    Если вы собираетесь применять doesNotUnderstand:
    для реализации заместителя, вышеописанная проблема должна быть как-то решена на уровне проектирования. Нельзя же ожидать, что совпадение заместите- лей равнозначно совпадению реальных субъектов. К сожалению, метод doesNotUnderstand:
    изначально создавался для обработки ошибок, а не для построения заместителей, поэтому его быстродействие оставляет желать лучшего;
    „
    „
    заместителю не всегда должен быть известен тип реального объекта.
    Если класс
    Proxy может работать с субъектом только через его абстракт- ный интерфейс, то не нужно создавать Proxy для каждого класса реаль-
    1
    Этот прием используется при реализации распределенных объектов в системе
    NEXTSTEP [Add94] (точнее, в классе NXProxy). Только там переопределяется метод forward — эквивалент описанного только что приема в Smalltalk.

    254
    Глава 4. Структурные паттерны ного субъекта
    RealSubject
    ; заместитель может обращаться к любому из них единообразно. Но если заместитель должен создавать экземпляры реальных субъектов (как обстоит дело в случае виртуальных заместите- лей), то знание конкретного класса обязательно.
    К проблемам реализации можно отнести и решение вопроса о том, как об- ращаться к субъекту, экземпляр которого еще не создан. Некоторые заме- стители должны обращаться к своим субъектам независимо от того, где они находятся — на диске или в памяти. Это означает, что нужно использовать какую-то форму идентификаторов объектов, не зависящих от адресного про- странства. В разделе «Мотивация» для этой цели использовалось имя файла.
    Пример кода
    В следующем коде реализованы два вида заместителей: виртуальный, описанный в разделе «Мотивация», и реализованный с помощью метода doesNotUnderstand:
    1
    „
    „
    виртуальный заместитель. В классе
    Graphic определен интерфейс для графических объектов:
    class Graphic {
    public:
    virtual Graphic();
    virtual void Draw(const Point& at) = 0;
    virtual void HandleMouse(Event& event) = 0;
    virtual const Point& GetExtent() = 0;
    virtual void Load(istream& from) = 0;
    virtual void Save(ostream& to) = 0;
    protected:
    Graphic();
    };
    Класс
    Image реализует интерфейс
    Graphic для отображения графических файлов. В нем замещается операция
    HandleMouse
    , при помощи которой пользователь может интерактивно изменять размер изображения:
    class Image : public Graphic {
    public:
    Image(const char* file); // Загружает изображение из файла virtual Image();
    1
    Еще один вид заместителя дает паттерн итератор

    Паттерн Proxy (заместитель)
    255
    virtual void Draw(const Point& at);
    virtual void HandleMouse(Event& event);
    virtual const Point& GetExtent();
    virtual void Load(istream& from);
    virtual void Save(ostream& to);
    private:
    // ...
    };
    Класс
    ImageProxy имеет тот же интерфейс, что и
    Image
    :
    class ImageProxy : public Graphic {
    public:
    ImageProxy(const char* imageFile);
    virtual ImageProxy();
    virtual void Draw(const Point& at);
    virtual void HandleMouse(Event& event);
    virtual const Point& GetExtent();
    virtual void Load(istream& from);
    virtual void Save(ostream& to);
    protected:
    Image* GetImage();
    private:
    Image* _image;
    Point _extent;
    char* _fileName;
    };
    Конструктор сохраняет локальную копию имени файла, в котором хра- нится изображение, и инициализирует переменные
    _extent и
    _image
    :
    ImageProxy::ImageProxy (const char* fileName) {
    _fileName = strdup(fileName);
    _extent = Point::Zero; // размеры пока не известны
    _image = 0;
    }
    Image* ImageProxy::GetImage() {
    if (_image == 0) {
    _image = new Image(_fileName);
    }
    return _image;
    }

    256
    Глава 4. Структурные паттерны
    Реализация операции
    GetExtent возвращает кэшированный размер, если это возможно. В противном случае изображение загружается из файла.
    Операция
    Draw загружает изображение, а
    HandleMouse перенаправляет событие реальному изображению:
    const Point& ImageProxy::GetExtent () {
    if (_extent == Point::Zero) {
    _extent = GetImage()->GetExtent();
    }
    return _extent;
    }
    void ImageProxy::Draw (const Point& at) {
    GetImage()->Draw(at);
    }
    void ImageProxy::HandleMouse (Event& event) {
    GetImage()->HandleMouse(event);
    }
    Операция
    Save записывает кэшированный размер изображения и имя файла в поток, а
    Load считывает эту информацию и инициализирует со- ответствующие переменные:
    void ImageProxy::Save (ostream& to) {
    to << _extent << _fileName;
    }
    void ImageProxy::Load (istream& from) {
    from >> _extent >> _fileName;
    }
    Наконец, предположим, что есть класс
    TextDocument для представления документа, который может содержать объекты класса
    Graphic
    :
    class TextDocument {
    public:
    TextDocument();
    void Insert(Graphic*);
    // ...
    };
    Объект
    ImageProxy можно вставить в документ следующим образом:
    TextDocument* text = new TextDocument;
    // ...
    text->Insert(new ImageProxy("anImageFileName"));

    Паттерн Proxy (заместитель)
    257
    „
    „
    заместители, использующие метод doesNotUnderstand. В языке Smalltalk можно создавать обобщенных заместителей, определяя классы, не име- ющие суперкласса
    1
    , а в них — метод doesNotUnderstand:
    для обработки сообщений.
    В показанном ниже фрагменте предполагается, что у заместителя есть метод realSubject
    , возвращающий связанный с ним реальный субъект.
    При использовании
    ImageProxy этот метод должен был бы проверить, создан ли объект
    Image
    , при необходимости создать его и затем вернуть.
    Для обработки перехваченного сообщения, которое было адресовано реальному субъекту, используется метод perform:withArguments:
    doesNotUnderstand: aMessage
    ^ self realSubject perform: aMessage selector withArguments: aMessage arguments
    Аргументом doesNotUnderstand:
    является экземпляр класса
    Message
    , представляющий сообщение, не понятое заместителем. Таким образом, при ответе на любое сообщение заместитель сначала проверяет, что ре- альный субъект существует, а потом уже переадресует ему сообщение.
    Одно из преимуществ метода doesNotUnderstand:
    — возможность вы- полнения произвольной обработки. Например, можно было бы создать защищающего заместителя, определив набор legalMessages
    -сообщений, которые следует принимать, и передав заместителю следующий метод:
    doesNotUnderstand: aMessage
    ^ (legalMessages includes: aMessage selector) ifTrue: [self realSubject perform: aMessage selector withArguments: aMessage arguments] ifFalse: [self error: 'Illegal operator']
    Прежде чем переадресовать сообщение реальному субъекту, указанный ме- тод проверяет, что оно действительно. Если это не так, doesNotUnderstand:
    посылает сообщение error:
    самому себе, что приведет к зацикливанию, если в заместителе не определен метод error:
    . Следовательно, опреде- ление error:
    должно быть скопировано из класса
    Object вместе со всеми методами, которые в нем используются.
    1
    Практически для любого класса Object является суперклассом самого верхнего уровня.
    Поэтому выражение «нет суперкласса» означает то же самое, что «определение класса, для которого Object не является суперклассом».

    258
    Глава 4. Структурные паттерны
    Известные применения
    Пример виртуального заместителя из раздела «Мотивация» заимствован из классов строительного блока текста, определенных в каркасе ET++.
    В системе NEXTSTEP [Add94] заместители (экземпляры класса NXProxy) используются как локальные представители объектов, которые могут быть распределенными. Сервер создает заместителей для удаленных объектов, когда клиент их запрашивает. Заместитель кодирует полученное сообщение вместе со всеми аргументами, после чего отправляет его удаленному субъ- екту. Аналогично субъект кодирует возвращенные результаты и посылает их обратно объекту
    NXProxy
    В работе McCullough [McC87] обсуждается применение заместителей в Smalltalk для обращения к удаленным объектам. Джефри Пэско (Geoffrey
    Pascoe) [Pas86] описывает, как обеспечить побочные эффекты при вызове методов и реализовать контроль доступа с помощью «инкапсуляторов».
    Родственные паттерны
    Адаптер (171): предоставляет другой интерфейс к адаптируемому объекту. На- против, заместитель в точности повторяет интерфейс своего субъекта. Однако, если заместитель используется для ограничения доступа, он может отказаться выполнять операцию, которую субъект выполнил бы, поэтому на самом деле интерфейс заместителя может быть и подмножеством интерфейса субъекта.
    Декоратор (209): хотя его реализация и похожа на реализацию заместителя, но назначение совершенно иное. Декоратор добавляет объекту новые обя- занности, а заместитель контролирует доступ к объекту.
    Степень схожести реализации заместителей и декораторов может быть раз- личной. Защищающий заместитель мог бы быть реализован в точности как декоратор. С другой стороны, удаленный заместитель не содержит прямых ссылок на реальный субъект, а лишь косвенную ссылку, что-то вроде «иден- тификатор хоста и локальный адрес на этом хосте». Вначале виртуальный заместитель имеет только косвенную ссылку (скажем, имя файла), но в ко- нечном итоге получает и использует прямую ссылку.
    ОБСУЖДЕНИЕ СТРУКТУРНЫХ ПАТТЕРНОВ
    Возможно, вы обратили внимание на некоторое сходство между структур- ными паттернами, особенно в том, что касается их участников и взаимо-

    Обсуждение структурных паттернов
    259
    действий. Скорее всего, это объясняется тем, что все структурные паттерны основаны на небольшом множестве языковых механизмов структурирования кода и объектов (одиночном и множественном наследовании для паттернов уровня класса и композиции для паттернов уровня объектов). Тем не менее, сходство может быть обманчиво, так как с помощью разных паттернов мож- но решать совершенно разные задачи. В этом разделе сопоставлены группы структурных паттернов, и вы сможете яснее представить их сравнительные достоинства и недостатки.
    АДАПТЕР И МОСТ
    У паттернов адаптер (171) и мост (184) есть несколько общих атрибутов.
    Тот и другой повышают гибкость, вводя дополнительный уровень косвен- ности при обращении к другому объекту. Оба перенаправляют запросы другому объекту, используя иной интерфейс.
    Принципиальное различие между адаптером и мостом в их назначении. Цель первого — устранение несовместимости между двумя существующими ин- терфейсами. При разработке адаптера не учитывается, как эти интерфейсы реализованы и то, как они могут независимо развиваться в будущем. Он должен лишь обеспечить совместную работу двух независимо разработан- ных классов, так чтобы ни один из них не пришлось переделывать. С другой стороны, мост связывает абстракцию с ее, возможно, многочисленными реализациями. Данный паттерн предоставляет клиентам стабильный ин- терфейс, позволяя в то же время изменять классы, которые его реализуют.
    Мост также подстраивается под новые реализации, появляющиеся в про- цессе развития системы.
    В связи с этими различиями адаптер и мост часто используются в разные моменты жизненного цикла системы. Когда выясняется, что два несовме- стимых класса должны работать вместе, часто приходится пользоваться адаптером — обычно для того, чтобы избежать дублирования кода. Заранее такую ситуацию предвидеть нельзя. Наоборот, пользователь моста с самого начала понимает, что у абстракции может быть несколько реализаций и раз- витие того и другого будет идти независимо. Адаптер обеспечивает работу
    после того, как нечто спроектировано; мост — до того. Это доказывает, что адаптер и мост предназначены для решения именно своих задач.
    Фасад (221) можно рассматривать как адаптер к набору других объектов. Но такая интерпретация упускает один нюанс: фасад определяет новый интер- фейс, тогда как адаптер повторно использует уже имеющийся. Вспомните,

    260
    Глава 4. Структурные паттерны что адаптер заставляет работать вместе два существующих интерфейса, а не определяет новый.
    КОМПОНОВЩИК, ДЕКОРАТОР И ЗАМЕСТИТЕЛЬ
    У компоновщика (196) и декоратора (209) похожие структурные схемы, что указывает на то, что оба паттерна основаны на рекурсивной композиции и предназначены для организации заранее неопределенного числа объектов.
    При обнаружении данного сходства может возникнуть искушение посчи- тать объект-декоратор вырожденным случаем компоновщика, но при этом будет искажен сам смысл паттерна декоратор. Сходство и заканчивается на рекурсивной композиции, и снова из-за различия задач, решаемых с по- мощью паттернов.
    Назначение декоратора — добавить новые обязанности объекта без порож- дения подклассов. Этот паттерн позволяет избежать комбинаторного роста числа подклассов, если проектировщик пытается статически определить все возможные комбинации. У компоновщика другие задачи. Он должен так структурировать классы, чтобы c разными взаимосвязанными объектами можно было бы работать единообразно, а несколько объектов обрабатывать как один. Акцент здесь делается не на оформлении, а на представлении.
    Эти цели различны, но они дополняют друг друга, поэтому компоновщик и декоратор часто используются совместно. Оба паттерна позволяют спро- ектировать систему так, что приложения можно будет создавать, просто соединяя объекты между собой, без определения новых классов. Появится некий абстрактный класс, одни подклассы которого — компоновщики, дру- гие — декораторы, а третьи — реализации фундаментальных строительных блоков системы. В таком случае у компоновщиков и декораторов будет об- щий интерфейс. С точки зрения паттерна декоратор компоновщик является конкретным компонентом. С точки зрения компоновщика декоратор — это листовый узел. Разумеется, их не обязательно использовать вместе, и, как было показано выше, эти паттерны имеют разные цели.
    Заместитель (246) — еще один паттерн, структура которого напоминает де- коратор. Оба паттерна описывают формирование уровня косвенного доступа к объекту, а в реализации объектов-декораторов и заместителей хранится ссылка на другой объект, которому переадресуются запросы. Но и здесь цели различаются.
    Как и декоратор, паттерн заместитель предоставляет клиенту интерфейс, со- впадающий с интерфейсом замещаемого объекта. Но в отличие от декоратора

    Обсуждение структурных паттернов
    261
    заместителю не нужно динамически добавлять и отбирать свойства, он не предназначен для рекурсивной композиции. Заместитель должен предоста- вить стандартную замену субъекту, когда прямой доступ к нему неудобен или нежелателен, например потому, что он находится на удаленной машине, хранится на диске или доступен лишь ограниченному кругу клиентов.
    В паттерне заместитель субъект определяет ключевую функциональность, а заместитель разрешает (или запрещает) доступ к ней. В декораторе компо- нент предоставляет лишь часть функциональности, а остальное привносят один или несколько декораторов. Декоратор предназначен для ситуаций, в которых полную функциональность объекта нельзя определить на этапе компиляции или это по крайней мере неудобно. Такая неопределенность делает рекурсивную композицию неотъемлемой частью декоратора. В случае с заместителем дело обстоит не так, ибо ему важно лишь одно отношение — между собой и своим субъектом, а данное отношение можно выразить статически.
    Указанные различия существенны, поскольку в них абстрагированы ре- шения конкретных проблем, снова и снова возникающих при объектно- ориентированном проектировании. Но это не означает, что паттерны не могут комбинироваться. Можно представить себе заместителя-декоратора, который добавляет новую функциональность заместителю, или декора- тора-заместителя, который оформляет удаленный объект. Такие гибриды теоретически могут быть полезны (у нас, правда, не нашлось реального примера), а вот паттерны, из которых они составлены, полезны наверняка.

    1   ...   17   18   19   20   21   22   23   24   ...   38


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