12.2.3 Компоновщик (Composite)
Компоновщик (Composite)
шаблон проектирования, объединяет объ- екты в древовидную структуру для представления иерархии от частного к целому. Компоновщик позволяет клиентам обращаться к отдельным объек- там и к группам объектов одинаково.
Назначение:
необходимо объединять группы схожих объектов и управлять ими;
объекты могут быть как примитивными (элементарными), так и со- ставными (сложными). Составной объект может включать в себя коллекции других объектов, образуя сложные древовидные структуры. Пример: дирек- тория файловой системы состоит из элементов, каждый их которых также может быть директорией;
код клиента работает с примитивными и составными объектами еди- нообразно.
Плюсы:
в систему легко добавлять новые примитивные или составные объ- екты, так как паттерн Composite использует общий базовый класс
Component;
код клиента имеет простую структуру – примитивные и составные объекты обрабатываются одинаковым образом;
127
паттерн Composite позволяет легко обойти все узлы древовидной структуры.
Минусы: Неудобно осуществить запрет на добавление в составной объект
Composite объектов определенных типов. Так, например, в состав римской армии не могут входить боевые слоны.
Применение: Управление группами объектов может быть непростой задачей, осо- бенно, если эти объекты содержат собственные объекты.
Для военной стратегической игры «Пунические войны», описывающей
военное противостояние между Римом и Карфагеном, каждая боевая еди- ница (всадник, лучник, пехотинец) имеет свою собственную разрушающую силу. Эти единицы могут объединяться в группы для образования более сложных военных подразделений, например, римские легионы, которые, в свою очередь, объединяясь, образуют целую армию. Как рассчитать боевую мощь таких иерархических соединений?
Паттерн Composite предлагает следующее решение. Он вводит абстракт- ный базовый класс
Component с поведением, общим для всех примитивных и составных объектов. Для случая стратегической игры – это метод getStrength()
для подсчета разрушающей силы. Подклассы
Primitive и
Composite являются производными от класса
Component
. Составной объект
Composite хранит компоненты-потомки абстрактного типа
Component
, каж- дый из которых может быть также
Composite
#include
#include
#include
// Component class Unit
{ public: virtual int getStrength() = 0; virtual void addUnit(Unit* p) { assert( false);
} virtual Unit() {}
};
// Primitives class Archer: public Unit
{ public: virtual int getStrength() { return 1;
}
128
}; class Infantryman: public Unit
{ public: virtual int getStrength() { return 2;
}
}; class Horseman: public Unit
{ public: virtual int getStrength() { return 3;
}
};
// Composite class CompositeUnit: public Unit
{ public: int getStrength() { int total = 0; for(int i=0; igetStrength(); return total;
} void addUnit(Unit* p) { c.push_back( p);
}
CompositeUnit() { for(int i=0; i } private: std::vector c;
};
// Вспомогательная функция для создания легиона
CompositeUnit* createLegion()
{
// Римский легион содержит:
CompositeUnit* legion = new CompositeUnit;
// 3000 тяжелых пехотинцев for (int i=0; i<3000; ++i) legion->addUnit(new Infantryman);
// 1200 легких пехотинцев for (int i=0; i<1200; ++i) legion->addUnit(new Archer);
// 300 всадников for (int i=0; i<300; ++i) legion->addUnit(new Horseman); return legion;
} int main()
{
129
// Римская армия состоит из 4-х легионов
CompositeUnit* army = new CompositeUnit; for (int i=0; i<4; ++i) army->addUnit( createLegion()); cout << "Roman army damaging strength is "
<< army->getStrength() << endl;
// … delete army; return 0;
}
Следует обратить внимание на один важный момент. Абстрактный базо- вый класс
Unit объявляет интерфейс для добавления новых боевых единиц addUnit()
, несмотря на то, что объектам примитивных типов (
Archer
,
Infantryman
,
Horseman
) подобная операция не нужна. Сделано это в угоду прозрачности системы в ущерб ее безопасности. Клиент знает, что объект типа
Unit всегда будет иметь метод addUnit()
. Однако его вызов для при- митивных объектов считается ошибочным и небезопасным.
12.2.4 Декоратор (Decorator)
Декоратор (Decorator)
структурный шаблон проектирования, предна- значенный для динамического подключения дополнительного поведения к объекту. Шаблон Декоратор предоставляет гибкую альтернативу практике созданию подклассов с целью расширения функциональности.
Назначение:
Паттерн Decorator динамически добавляет новые обязанности объ- екту. Декораторы являются гибкой альтернативой порождению подклассов для расширения функциональности.
Рекурсивно декорирует основной объект.
Паттерн Decorator использует схему «обертываем подарок, кладем его в коробку, обертываем коробку».
Плюсы
нет необходимости создавать подклассы для расширения функцио- нальности объекта;
возможность динамически подключать новую функциональность до или после основной функциональности объекта
ConcreteComponent
Применение:
Клиент всегда заинтересован в функциональности
CoreFunctionality.doThis()
. Клиент может или не может быть заинтере- сован в методах
OptionalOne.doThis()
и
OptionalTwo.doThis()
. Каждый
130 из этих классов переадресует запрос базовому классу
Decorator
, а тот направляет его в декорируемый объект.
Паттерн Decorator динамически добавляет новые обязанности объекту.
Украшения для новогодней елки являются примерами декораторов. Огни, гирлянды, игрушки и т.д. вешают на елку для придания ей праздничного вида. Украшения не меняют саму елку, а только делают ее новогодней.
Хотя картины можно повесить на стену и без рамок, рамки часто добав- ляются для придания нового стиля. class I { public: virtual I(){} virtual void do_it() = 0;
}; class A: public I { public:
A() { cout << "A dtor" << '\n';
}
/*virtual*/ void do_it() { cout << 'A';
}
}; class D: public I { public:
D(I *inner) { m_wrappee = inner;
}
D() { delete m_wrappee;
}
/*virtual*/ void do_it() { m_wrappee->do_it();
} private:
I *m_wrappee;
}; class X: public D { public:
X(I *core): D(core){}
X() { cout << "X dtor" << " ";
}
/*virtual*/ void do_it() {
D::do_it(); cout << 'X';
}
}; class Y: public D {
131 public:
Y(I *core): D(core){}
Y() { cout << "Y dtor" << " ";
}
/*virtual*/ void do_it() {
D::do_it(); cout << 'Y';
}
}; class Z: public D { public:
Z(I *core): D(core){}
Z() { cout << "Z dtor" << " ";
}
/*virtual*/ void do_it() {
D::do_it(); cout << 'Z';
}
}; int main() {
I *anX = new X(new A);
I *anXY = new Y(new X(new A));
I *anXYZ = new Z(new Y(new X(new A))); anX->do_it(); cout << '\n'; anXY->do_it(); cout << '\n'; anXYZ->do_it(); cout << '\n'; delete anX; delete anXY; delete anXYZ;
}
Вывод программы:
AX
AXY
AXYZ
X dtor A dtor
Y dtor X dtor A dtor
Z dtor Y dtor X dtor A dtor
12.2.5 Прокси (Proxy)
Прокси (Proxy)
структурный шаблон проектирования, предназначен- ный для замещения другого объекта для контроля доступа к нему.
Назначение:
Паттерн Proxy является суррогатом или замеcтителем другого объекта и контролирует доступ к нему.
132
Предоставляя дополнительный уровень косвенности при доступе к объекту, может применяться для поддержки распределенного, управляе- мого или интеллектуального доступа.
Являясь «оберткой» реального компонента, защищает его от излиш- ней сложности.
Применение: Суррогат или
заместитель это объект, интерфейс которого идентичен ин- терфейсу реального объекта. При первом запросе клиента заместитель со- здает реальный объект, сохраняет его адрес и затем отправляет запрос этому реальному объекту. Все последующие запросы просто переадресуются ин- капсулированному реальному объекту.
Существует четыре ситуации, когда можно использовать паттерн Proxy:
Виртуальный proxy является заместителем объектов, создание кото- рых обходится дорого. Реальный объект создается только при первом за- просе/доступе клиента к объекту.
Удаленный proxy предоставляет локального представителя для объ- екта, который находится в другом адресном пространстве («заглушки» в
RPC и CORBA).
Защитный proxy контролирует доступ к основному объекту. «Сурро- гатный» объект предоставляет доступ к реальному объекту, только вызыва- ющий объект имеет соответствующие права.
Интеллектуальный proxy выполняет дополнительные действия при доступе к объекту.
Вот типичные области применения интеллектуальных proxy:
Подсчет числа ссылок на реальный объект. При отсутствии ссылок па- мять под объект автоматически освобождается (известен также как интел- лектуальный указатель или smart pointer).
Загрузка объекта в память при первом обращении к нему.
Установка запрета на изменение реального объекта при обращении к нему других объектов.
Паттерн Proxy для доступа к реальному объекту использует его суррогат или заместитель. Банковский чек является заместителем денежных средств на счете. Чек может быть использован вместо наличных денег для соверше- ния покупок и, в конечном счете, контролирует доступ к наличным деньгам на счете чекодателя.
Особенности паттерна Proxy:
133
Adapter предоставляет своему объекту другой интерфейс. Proxy предоставляет тот же интерфейс. Decorator предоставляет расширенный интерфейс.
Decorator и Proxy имеют разные цели, но схожие структуры. Оба вво- дят дополнительный уровень косвенности: их реализации хранят ссылку на объект, на который они отправляют запросы. class RealImage
{ int m_id; public:
RealImage(int i)
{ m_id = i; cout << " $$ ctor: " << m_id << '\n';
}
RealImage()
{ cout << " dtor: " << m_id << '\n';
} void draw()
{ cout << " drawing image " << m_id << '\n';
}
};
// 1. Класс-обертка с "дополнительным уровнем косвенности" class Image
{
// 2. Класс-обертка содержит указатель на реальный класс
RealImage *m_the_real_thing; int m_id; static int s_next; public:
Image()
{ m_id = s_next++;
// 3. Инициализируется нулевым значением m_the_real_thing = 0;
}
Image()
{ delete m_the_real_thing;
} void draw()
{
// 4. Реальный объект создается при поступлении
// запроса "на первом использовании" if (!m_the_real_thing) m_the_real_thing = new RealImage(m_id);
// 5. Запрос всегда делегируется реальному объекту m_the_real_thing->draw();
}
}; int Image::s_next = 1; int main()
134
{
Image images[5]; for (int i; true;)
{ cout << "Exit[0], Image[1-5]: "; cin >> i; if (i == 0) break; images[i - 1].draw();
}
}
12.3 Поведенческие паттерны проектирования Паттерны поведения рассматривают вопросы о связях между объектами и распределением обязанностей между ними. Для
этого могут использо- ваться механизмы, основанные как на наследовании, так и на композиции.
К поведенческим паттернам проектирования относятся следующие:
команда (command);
итератор (iterator);
наблюдатель (observer);
стратегия (strategy);
шаблонный метод (template method);
посетитель (visitor).
12.3.1 Команда (Command) Команда (Command)
шаблон проектирования, используемый при объ- ектно-ориентированном программировании, преобразующий запрос на вы- полнение действия в отдельный объект-команду.
Назначение:
Система управляется событиями. При появлении такого события (за- проса) необходимо выполнить определенную последовательность дей- ствий.
Необходимо параметризировать объекты выполняемым действием, ставить запросы в очередь или поддерживать операции отмены (undo) и по- втора (redo) действий.
Нужен объектно-ориентированный аналог функции обратного вызова в процедурном программировании.
Плюсы: 135
Придает системе гибкость, отделяя инициатора запроса от его получа- теля, позволяет осуществлять динамическую замену команд, использовать сложные составные команды, осуществлять отмену операций.
Применение: Паттерн Command преобразовывает запрос на выполнение действия в отдельный объект-команду. Такая инкапсуляция позволяет передавать эти действия другим функциям и объектам в качестве параметра, приказывая им выполнить запрошенную операцию. Команда – это объект, поэтому над ней допустимы любые операции, что и над объектом.
Интерфейс командного объекта определяется абстрактным базовым классом
Command и в самом простом случае имеет единственный метод execute()
. Производные классы определяют получателя запроса (указатель на объект-получатель) и необходимую для выполнения операцию (метод этого объекта). Метод execute()
подклассов
Command просто вызывает нужную операцию получателя.
В паттерне Command может быть до трех участников:
клиент,
создающий экземпляр командного объекта;
инициатор запроса, использующий командный объект;
получатель запроса.
Сначала клиент создает объект
ConcreteCommand
, конфигурируя его по- лучателем запроса. Этот объект также доступен инициатору. Инициатор ис- пользует его при отправке запроса, вызывая метод execute()
. Этот алго- ритм напоминает работу функции обратного вызова в процедурном про- граммировании – функция регистрируется, чтобы быть вызванной позднее.
Паттерн Command отделяет объект, инициирующий операцию, от объ- екта, который знает, как ее выполнить. Единственное, что должен знать ини- циатор, это как отправить команду. Это придает системе гибкость: позво- ляет осуществлять динамическую замену команд, использовать сложные со- ставные команды, осуществлять отмену операций.
#include
#include
#include class Game { public: void create( ) { cout << "Create game " << endl;
} void open( string file ) { cout << "Open game from " << file << endl;
} void save( string file ) {
136 cout << "Save game in " << file << endl;
} void make_move( string move ) { cout << "Make move " << move << endl;
}
}; string getPlayerInput( string prompt ) { string input; cout << prompt; cin >> input; return input;
}
// Базовый класс class Command { public: virtual Command() {} virtual void execute() = 0; protected:
Command( Game* p ): pgame( p) {}
Game * pgame;
}; class CreateGameCommand: public Command { public:
CreateGameCommand( Game * p ) : Command( p) {} void execute() { pgame->create( );
}
}; class OpenGameCommand: public Command { public:
OpenGameCommand( Game * p ) : Command( p) {} void execute() { string file_name; file_name = getPlayerInput( "Enter file name:"); pgame->open( file_name);
}
}; class SaveGameCommand: public Command { public:
SaveGameCommand( Game * p ) : Command( p) {} void execute( ) { string file_name; file_name = getPlayerInput( "Enter file name:"); pgame->save( file_name);
}
}; class MakeMoveCommand: public Command { public:
MakeMoveCommand( Game * p) : Command( p) {} void execute() {
// Сохраним игру для возможного последующего отката pgame->save("TEMP_FILE"); string move; move = getPlayerInput( "Enter your move:"); pgame->make_move(move);
137
}
}; class UndoCommand: public Command { public:
UndoCommand( Game * p ) : Command( p) {} void execute() {
// Восстановим игру из временного файла pgame->open( "TEMP_FILE");
}
}; int main()
{
Game game;
// Имитация действий игрока vector v;
// Создаем новую игру v.push_back( new CreateGameCommand( &game));
// Делаем несколько ходов v.push_back( new MakeMoveCommand( &game)); v.push_back( new MakeMoveCommand( &game));
// Последний ход отменяем v.push_back( new UndoCommand( &game));
// Сохраняем игру v.push_back( new SaveGameCommand( &game)); for (size_t i=0; iexecute(); for (size_t i=0; i}
12.3.2 Итератор (Iterator)
Итератор (Iterator) – Шаблон проектирования, предоставляющий меха- низм обхода элементов составных объектов (коллекций) не раскрывая их внутреннего представления.
Назначение:
Предоставляет способ последовательного доступа ко всем элементам составного объекта, не раскрывая его внутреннего представления.
Абстракция в стандартных библиотеках C++ и Java, позволяющая раз- делить классы коллекций и алгоритмов.
Придает обходу коллекции «объектно-ориентированный статус».
Полиморфный обход.
Применение:
138
Составной объект,
такой как список, должен предоставлять способ до- ступа к его элементам без раскрытия своей внутренней структуры. Более того, иногда нужно перебирать элементы списка различными способами, в зависимости от конкретной задачи. Но вы, вероятно, не хотите раздувать интерфейс списка операциями для различных обходов, даже если они необ- ходимы. Кроме того, иногда нужно иметь несколько активных обходов од- ного списка одновременно. Было бы хорошо иметь единый интерфейс для обхода разных типов составных объектов (т.е. полиморфная итерация).
Паттерн Iterator позволяет все это делать. Ключевая идея состоит в том, чтобы ответственность за доступ и обход переместить из составного объекта на объект Iterator, который будет определять стандартный протокол обхода.
Абстракция Iterator имеет основополагающее значение для технологии, называемой «обобщенное программирование». Эта технология четко разде- ляет такие понятия как «алгоритм» и «структура данных». Мотивирующие факторы: способствование компонентной разработке, повышение произво- дительности и снижение расходов на управление.
Рассмотрим пример. Если вы хотите одновременно поддерживать че- тыре вида структур данных (массив, бинарное дерево, связанный список и хэш-таблица) и три алгоритма (сортировка, поиск и слияние), то традицион- ный подход потребует 12 вариантов конфигураций (четыре раза по три), в то время как обобщенное программирование требует лишь 7 (четыре плюс три).
#include
using namespace std; class Stack { int items[10]; int sp; public: friend class StackIter;
Stack()
{ sp = -1;
} void push(int in)
{ items[++sp] = in;
} int pop()
{ return items[sp--];
} bool isEmpty()
{ return (sp == -1);
}
};
139 class StackIter { const Stack &stk; int index; public:
StackIter(const Stack &s): stk(s)
{ index = 0;
} void operator++()
{ index++;
} bool operator()()
{ return index != stk.sp + 1;
} int operator *()
{ return stk.items[index];
}
}; bool operator == (const Stack &l, const Stack &r)
{
StackIter itl(l), itr(r); for (; itl(); ++itl, ++itr) if (*itl != *itr) break; return !itl() && !itr();
} int main()
{
Stack s1; int i; for (i = 1; i < 5; i++) s1.push(i);
Stack s2(s1), s3(s1), s4(s1), s5(s1); s3.pop(); s5.pop(); s4.push(2); s5.push(9); cout << "1 == 2 is " << (s1 == s2) << endl; cout << "1 == 3 is " << (s1 == s3) << endl; cout << "1 == 4 is " << (s1 == s4) << endl; cout << "1 == 5 is " << (s1 == s5) << endl;
}