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

  • Component

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


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

    aPicture
    aPicture
    aText
    aLine
    aRectangle
    aLine
    aRectangle
    Применимость
    Основные условия для применения паттерна компоновщик:
    „
    „
    требуется представить иерархию объектов вида «часть — целое»;
    „
    „
    клиенты должны по единым правилам работать с составными и индиви- дуальными объектами.
    Структура
    Component
    Operation()
    Add(Component)
    Remove(Component)
    GetChild(int)
    Leaf
    Composite
    Operation()
    Operation()
    Add(Component)
    Remove(Component)
    GetChild(int)
    Для всех потомков g g.Operation();
    Client
    Потомки

    Паттерн Composite (компоновщик)
    199
    Структура типичного составного объекта могла бы выглядеть так:
    aComposite
    aComposite
    aLeaf
    aLeaf
    aLeaf
    aLeaf
    aLeaf
    aLeaf
    Участники
    „
    „
    Component (
    Graphic
    ) — компонент:
    объявляет интерфейс для компонуемых объектов;
    • реализует поведение по умолчанию для интерфейсов, общих для всех классов;
    • объявляет интерфейс для обращения к потомкам и управления ими;
    • (не обязательно) определяет интерфейс для обращения к родителю ком- понента в рекурсивной структуре и при необходимости реализует его;
    „
    „
    Leaf (
    Rectangle
    ,
    Line
    ,
    Text и т. п.) — лист:
    • представляет листовые узлы композиции и не имеет потомков;
    • определяет поведение примитивных объектов в композиции;
    „
    „
    Composite (
    Picture
    ) — составной объект:
    • определяет поведение компонентов, у которых есть потомки;
    • хранит компоненты-потомки;
    • реализует относящиеся к управлению потомками операции в интер- фейсе класса
    Component
    ;
    „
    „
    Client — клиент:
    • манипулирует объектами композиции через интерфейс
    Component
    Отношения
    Клиенты используют интерфейс класса
    Component для взаимодействия с объектами в составной структуре. Если получателем запроса является

    200
    Глава 4. Структурные паттерны листовый объект
    Leaf
    , то он и обрабатывает запрос. Когда же получателем является составной объект
    Composite
    , то обычно он перенаправляет запрос своим потомкам — возможно, с выполнением некоторых дополнительных операций до или после перенаправления.
    Результаты
    Паттерн компоновщик:
    „
    „
    определяет иерархии классов, состоящие из примитивных и составных
    объектов. Из примитивных объектов можно составлять более сложные, которые, в свою очередь, участвуют в более сложных композициях и так далее. Любой клиент, ожидающий получить примитивный объект, мо- жет работать и с составным;
    „
    „
    упрощает архитектуру клиента. Клиенты могут единообразно рабо- тать с индивидуальными и объектами и с составными структурами.
    Обычно клиенту неизвестно, взаимодействует ли он с листовым или составным объектом. Это упрощает код клиента, поскольку нет необхо- димости писать функции, ветвящиеся в зависимости от того, с объектом какого класса они работают;
    „
    „
    облегчает добавление новых видов компонентов. Новые подклассы клас- сов
    Composite или
    Leaf будут автоматически работать с уже существую- щими структурами и клиентским кодом. Изменять клиент при добавле- нии новых компонентов не нужно;
    „
    „
    способствует созданию общего дизайна. Впрочем, такая простота до- бавления новых компонентов имеет и свои отрицательные стороны: становится трудно установить ограничения на то, какие объекты могут входить в состав композиции. Иногда бывает нужно, чтобы составной объект мог включать только определенные виды компонентов. Паттерн компоновщик не позволяет воспользоваться для реализации таких огра- ничений статической системой типов. Вместо этого приходится прово- дить проверки во время выполнения.
    Реализация
    При реализации паттерна компоновщик приходится учитывать целый ряд аспектов:
    „
    „
    явные ссылки на родителей. Хранение в компоненте ссылки на своего родителя может упростить обход структуры и управление ею. Наличие такой ссылки облегчает передвижение вверх по структуре и удаление

    Паттерн Composite (компоновщик)
    201
    компонента. Кроме того, ссылки на родителей помогают реализовать паттерн цепочка обязанностей (263).
    Обычно ссылку на родителя определяют в классе
    Component
    . Классы
    Leaf и
    Composite могут наследовать саму ссылку и операции с ней.
    При наличии ссылки на родителя важно поддерживать следующий инвариант: для всех потомков в составном объекте родителем является составной объект, для которого они в свою очередь являются потомками.
    Простейший способ гарантировать соблюдение этого условия — из- менять родителя компонента только тогда, когда он добавляется или удаляется из составного объекта. Если это удается один раз реализовать в операциях
    Add и
    Remove
    , то реализация будет унаследована всеми подклассами, а следовательно, инвариант будет поддерживаться авто- матически;
    „
    „
    совместное использование компонентов. Часто бывает полезно органи- зовать совместное использование компонентов — например, для умень- шения объема занимаемой памяти. Но если у компонента может быть более одного родителя, то совместное использование становится про- блемой.
    Возможное решение — позволить компонентам хранить ссылки на не- скольких родителей. Однако в таком случае при распространении запроса по структуре могут возникнуть неоднозначности. Паттерн приспособле- нец (231) показывает, как следует изменить дизайн, чтобы вовсе отка- заться от хранения родителей. Работает он в тех случаях, когда потомки могут избежать отправки сообщений своим родителям, вынеся за свои границы часть внутреннего состояния;
    „
    „
    максимизация интерфейса класса Component. Одна из целей паттерна компоновщик — избавить клиентов от необходимости знать, работают ли они с листовым или составным объектом. Для достижения этой цели класс
    Component должен сделать как можно больше операций общими для классов
    Composite и
    Leaf
    . Обычно класс
    Component предоставляет для этих операций реализации по умолчанию, а подклассы
    Composite и
    Leaf замещают их.
    Однако иногда эта цель вступает в конфликт с принципом проектиро- вания иерархии классов, согласно которому класс должен определять только логичные для всех его подклассов операции. Класс
    Component поддерживает много операций, не имеющих смысла для класса
    Leaf
    . Как же тогда предоставить для них реализацию по умолчанию?

    202
    Глава 4. Структурные паттерны
    Иногда некоторая изобретательность позволяет перенести в класс
    Component операцию, которая, на первый взгляд, имеет смысл только для составных объектов. Например, интерфейс для обращений к по- томкам является фундаментальной частью класса
    Composite
    , но вовсе не обязательно класса
    Leaf
    . Однако если рассматривать
    Leaf как
    Component
    , у которого никогда не бывает потомков, то в классе
    Component можно опре- делить операцию обращения к потомкам как никогда не возвращающую потомков. Тогда подклассы
    Leaf могут использовать эту реализацию по умолчанию, а в подклассах
    Composite она будет переопределена, чтобы возвращать потомков.
    Операции управления потомками создают немало проблем; они будут рассмотрены в следующем разделе;
    „
    „
    объявление операций для управления потомками. Хотя в классе
    Composite реализованы операции Add и Remove для добавления и удаления по- томков, но для паттерна компоновщик важно, в каких классах эти опе- рации объявлены. Надо ли объявлять их в классе
    Component и тем самым делать доступными в
    Leaf
    , или их следует объявить и определить только в классе
    Composite и его подклассах?
    Ответ на этот вопрос подразумевает компромисс между безопасностью и прозрачностью:
    • если определить интерфейс для управления потомками в корне ие- рархии классов, мы добиваемся прозрачности, так как все компонен- ты удается трактовать единообразно. Однако за это расплачиваться приходится безопасностью, поскольку клиент может попытаться выполнить бессмысленное действие вроде добавления или удаления объекта из листового узла;
    • если управление потомками определяется в классе
    Composite
    , то безопасность будет обеспечена — ведь любая попытка добавить или удалить объекты из листьев в статически типизированном языке вроде C++ будет перехвачена на этапе компиляции. Но прозрачность при этом теряется, так как листовые и составные объекты обладают разными интерфейсами.
    В паттерне компоновщик мы придаем особое значение прозрачности, а не безопасности. Если для вас важнее безопасность, будьте готовы к тому, что в некоторых случаях вы можете потерять информацию о типе, и ком- понент придется преобразовывать к типу составного объекта. Как это сделать, не прибегая к небезопасным приведениям типов?

    Паттерн Composite (компоновщик)
    203
    Можно, например, объявить в классе
    Component операцию
    Composite*
    GetComposite()
    . Класс
    Component реализует ее по умолчанию, возвращая null
    -указатель. А в классе
    Composite эта операция переопределена, чтобы она возвращала текущий объект в виде указателя this
    :
    class Composite;
    class Component {
    public:
    // ...
    virtual Composite* GetComposite() { return 0; }
    };
    class Composite : public Component {
    public:
    void Add(Component*);
    // ...
    virtual Composite* GetComposite() { return this; }
    };
    class Leaf : public Component {
    // ...
    };
    Благодаря операции
    GetComposite можно спросить у компонента, являет- ся ли он составным. К возвращаемому этой операцией составному объекту допустимо безопасно применять операции
    Add и
    Remove
    :
    Composite* aComposite = new Composite;
    Leaf* aLeaf = new Leaf;
    Component* aComponent;
    Composite* test;
    aComponent = aComposite;
    if (test = aComponent->GetComposite()) {
    test->Add(new Leaf);
    }
    aComponent = aLeaf;
    if (test = aComponent->GetComposite()) {
    test->Add(new Leaf); // лист не добавляется
    }
    Аналогичные проверки на принадлежность классу
    Composite в C++ вы- полняются с помощью оператора dynamic_cast

    204
    Глава 4. Структурные паттерны
    Разумеется, недостаток такого подхода заключается в том, что мы не обращаемся со всеми компонентами единообразно. Снова приходится проверять тип, перед тем как предпринять то или иное действие.
    Единственный способ обеспечить прозрачность — это включить в класс
    Component реализации операций
    Add и
    Remove по умолчанию. Но тогда появится новая проблема: нельзя реализовать
    Component::Add так, чтобы не появилась возможность ошибки. Можно, конечно, сделать данную операцию пустой, но тогда нарушается важное проектное ограничение: попытка добавить что-то в листовый объект, скорее всего, свидетельствует об ошибке. Допустимо было бы заставить ее удалять свой аргумент, но такое поведение может оказаться неожиданным для клиента.
    Обычно лучшим решением является такая реализация
    Add и
    Remove по умолчанию, при которой они завершаются с ошибкой (возможно, воз- буждая исключение), если компоненту не разрешено иметь потомков
    (для
    Add
    ) или аргумент не является чьим-либо потомком (для
    Remove
    ).
    Другая возможность — слегка изменить семантику операции «удаление».
    Если компонент хранит ссылку на родителя, то можно было бы считать, что
    Component::Remove удаляет самого себя. Тем не менее, для операции
    Add по-прежнему нет разумной интерпретации;
    „
    „
    должен ли Component реализовывать список компонентов? Возможно, вам захочется определить множество потомков в виде переменной эк- земпляра класса
    Component
    , в котором объявлены операции обращения к потомкам и управления ими. Но размещение указателя на потомков в базовом классе создает лишние затраты памяти во всех листовых уз- лах, хотя у листа потомков быть не может. Такое решение может ис- пользоваться только в том случае, если в структуре относительно мало потомков;
    „
    „
    упорядочение потомков. Во многих случаях важен порядок следования потомков составного объекта. В рассмотренном выше примере клас- са
    Graphic под порядком может пониматься Z-порядок расположения потомков. В составных объектах, описывающих деревья синтаксиче- ского разбора, составные операторы могут быть экземплярами класса
    Composite
    , порядок следования потомков которых отражает семантику программы.
    Если порядок следования потомков важен, необходимо учитывать его при проектировании интерфейсов доступа и управления потомками. В этом может помочь паттерн итератор (302);

    Паттерн Composite (компоновщик)
    205
    „
    „
    кэширование для повышения производительности. Если приходится часто выполнять обход или поиск в композициях, то класс
    Composite может кэшировать либо непосредственно полученные результаты, либо только информацию, достаточную для ускорения обхода или поиска.
    Например, класс
    Picture из примера, приведенного в разделе «Моти- вация», мог бы кэшировать охватывающие прямоугольники своих по- томков. При рисовании или выделении эта информация позволила бы пропускать тех потомков, которые не видимы в текущем окне.
    При любом изменении компонента кэшированная информация всех его родителей должна становиться недействительной. Наиболее эффективен такой подход в случае, когда компонентам известно об их родителях.
    Поэтому, если вы решите воспользоваться кэшированием, необходимо определить интерфейс, позволяющий уведомить составные объекты о не- действительности содержимого их кэшей;
    „
    „
    кто должен удалять компоненты? В языках, где нет уборщика мусора, лучше всего поручить классу
    Composite удалять своих потомков в мо- мент уничтожения. Исключением из этого правила является случай, когда листовые объекты постоянны и, следовательно, могут использо- ваться совместно;
    „
    „
    выбор структуры данных для хранения компонентов. Составные объ- екты могут хранить своих потомков в самых разных структурах дан- ных, включая связанные списки, деревья, массивы и хеш-таблицы.
    Выбор структуры данных определяется, как всегда, эффективностью.
    Собственно говоря, вовсе не обязательно пользоваться какой-либо из универсальных структур. Иногда в составных объектах каждый пото- мок представляется отдельной переменной. Правда, для этого каждый подкласс
    Composite должен реализовывать свой собственный интер- фейс управления памятью. См. пример в описании паттерна интерпре- татор.
    Пример кода
    Такие изделия, как компьютеры и стереосистемы, часто имеют иерархи- ческую структуру. Например, в раме монтируются дисковые накопители и плоские электронные платы, к шине подсоединяются различные карты, а корпус содержит раму, шины и т. д. Подобные структуры моделируются с помощью паттерна компоновщик.
    Класс
    Equipment определяет интерфейс для всех видов аппаратуры в иерар- хии вида «часть — целое»:

    206
    Глава 4. Структурные паттерны class Equipment {
    public:
    virtual

    Equipment();
    const char* Name() { return _name; }
    virtual Watt Power();
    virtual Currency NetPrice();
    virtual Currency DiscountPrice();
    virtual void Add(Equipment*);
    virtual void Remove(Equipment*);
    virtual Iterator* CreateIterator();
    protected:
    Equipment(const char*);
    private:
    const char* _name;
    };
    В классе
    Equipment объявлены операции, которые возвращают атрибуты аппаратного блока, например энергопотребление и стоимость. Подклас- сы реализуют эти операции для конкретных видов оборудования. Класс
    Equipment объявляет также операцию
    CreateIterator
    , возвращающую итератор
    Iterator
    (см. приложение В) для обращения к отдельным частям.
    Реализация этой операции по умолчанию возвращает итератор
    NullIterator
    , умеющий обходить только пустое множество.
    Среди подклассов
    Equipment могут быть листовые классы, представляющие дисковые накопители, микросхемы и переключатели:
    class FloppyDisk : public Equipment { public:
    FloppyDisk(const char*); virtual FloppyDisk(); virtual Watt Power(); virtual Currency NetPrice(); virtual Currency DiscountPrice();
    };
    CompositeEquipment
    — это базовый класс для оборудования, содержащего другое оборудование. Одновременно это подкласс класса
    Equipment
    :
    class CompositeEquipment : public Equipment { public: virtual CompositeEquipment(); virtual Watt Power(); virtual Currency NetPrice();

    Паттерн Composite (компоновщик)
    207
    virtual Currency DiscountPrice(); virtual void Add(Equipment*); virtual void Remove(Equipment*); virtual Iterator* CreateIterator(); protected:
    CompositeEquipment(const char*); private:
    List _equipment;
    };
    CompositeEquipment определяет операции для доступа и управления вну- тренними аппаратными блоками. Операции
    Add и
    Remove добавляют и уда- ляют оборудование из списка, хранящегося в переменной
    _equipment
    Операция
    CreateIterator возвращает итератор (точнее, экземпляр класса
    ListIterator
    ), который будет обходить этот список.
    Реализация по умолчанию операции
    NetPrice могла бы использовать
    CreateIterator для суммирования цен на отдельные блоки
    1
    :
    Currency CompositeEquipment::NetPrice () {
    Iterator* i = CreateIterator();
    Currency total = 0;
    for (i->First(); !i->IsDone(); i->Next()) {
    total += i->CurrentItem()->NetPrice();
    }
    delete i;
    return total;
    }
    Теперь мы можем представить аппаратный блок компьютера в виде под- класса к
    CompositeEquipment под названием
    Chassis
    Chassis наследует порожденные операции класса
    CompositeEquipment class Chassis : public CompositeEquipment {
    public:
    Chassis(const char*);
    virtual Chassis();
    virtual Watt Power();
    virtual Currency NetPrice();
    virtual Currency DiscountPrice();
    };
    1
    Очень легко забыть об удалении итератора после завершения работы с ним. В описании паттерна итератор рассказано, как защититься от таких ошибок.

    1   ...   13   14   15   16   17   18   19   20   ...   38


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