Предыдущая Оглавление Следующая
Предположим, что интерфейс Вашей системы управляет графическими объектами через некий общий интерфейс, для чего все объекты порождаются от базового класса CGraphic. В один момент Вы обнаруживаете, что Вам совершенно необходим некий сложный графический класс CComplex , разрабатывать который с нуля нет ни малейшего желания.
По счастью, похожий класс CAlien есть в библиотеке, которую недавно делали в соседнем отделе. Вы бежите туда с дискеткой, записываете код, смотрите и... и ничего хорошего. Он не наследует от CGraphic . Его логика вообще другая.
Печально, но не смертельно. Нужно CComplex породить в открытую от CGraphic и в закрытую от CAlien, а функции интерфейса CGraphic переопределить так, чтобы они просто вызывали соответствующие функции CAlien. Это будет адаптер класса. Или просто вложить экземпляр (или указатель, или ссылку на) CAlien внутрь класса CComplex , и так же вызывать его функции. Это будет адаптер объекта.
Пример кода:
// Это - чужак class CAlien { public: void doAlien (); }; // Это то, к чему нужно привести интерфейс class CGraphic { public: virtual void doNatural ()=0; }; // Класс наследует интерфейс от CGraphics // а реализацию берет из CAlien. class CComplex : public CGraphic { private: CAlien m_alien; public: void doNatural () { m_alien.doAlien(); }; };
Вот структура паттерна adapter. На диаграмме классов я употребляю более
абстрактные имена классов, наподобие клиент или адаптер.
Немного отвлечемся от нашей игры. Пусть Ваша программа управляет файлами в различных файловой системе, для чего Вы от класса CFile порождаете потомки CFATFile и CNTFSFile. А еще файлы могут быть либо текстовыми, либо двоичными - да еще и в каждой файловой системой. Если просто порождать потомки, то получится уже четыре класса. Кроме того, код будет дублировать друг друга, а подклассы окажутся привязанными к реализации - поскольку никто не гарантирует одинакового поведения для CFATTextFile и CNTFSTextFile .
Выход в разделении файловых систем и вида файла в разные иерархии классов. То есть, пусть от CFile порождены CFATFile и CNTFSFile, но не более. А классы текстовых и двоичных файлов CTextFile и CBinFile порождены от CAbstractFile, который содержит в себе указатель на CFile. Теперь операции текстовых и двоичных файлов вызывают виртуальные функции CFile - а исполняются те, что нужно. Пример кода таков (не проверял)
// Это классы отвечающие за физическую реализацию class CFile { public: virtual void doIt1 () = 0; virtual void doIt2 () = 0; }; // В конкретных классах виртуальные функции // исполняют несколько полезных вещей непосредственно // с файловой системой. class CFATFile : public CFile { public: void doIt1 () { format("c:"); }; void doIt2 () { loadVirus("OneHalf"); }; }; class CNTFSFile : public CFile { public: void doIt1 () { mustdie("windows"); }; void doIt2 () { catchHim("Bill"); }; }; // А это классы, отвечающие за файлы на более высоком уровне class CAbstractFile : { public: virtual void processFile () = 0; private: // Аргегация низкоуровневого файла CFile* m_file; }; class CTextFile : public CAbstractFile { public: // В зависимости от класса m_file исполнятся // разные функции void processFile () { m_file->doIt1(); }; }; class CBinFile : public CAbstractFile { public: void processFile () { m_file->doIt2(); }; };
Таким образом паттерн Мост (Bridge) разделяет абстракцию и ее реализацию. Вот
структура паттерна.
Вернемся к нашей гаме. Пусть Вам нужно стоить здание. Здание строится из блоков: целых комнат или залов, панелей, перекрытий, перегородок, стен, оконных блоков, фундаментов и кирпичей. Что интересно, соединив два здания, Вы получаете осмысленый результат: другое здание. Это значит, что и само здание является блоком. В то же время, панели можно собрать из кирпича, фундаменты тоже, и так далее. Всякий блок состоит из других - а в основе пирамиды лежат голые кирпичи, деревяшки и стекла. И кстати, есть действия, которые можно применить сразу ко всему собранному блоку - его можно взорвать, перекрасить или продать - целиком, и это произойдет со всеми частями.
Стало быть, должен существовать способ, которым мы могли бы управлять однообразно любыми строительными блоками. Этот способ конечно прост: определяем абстрактный класс строительного блока так, чтобы он мог содержать набор других строительных блоков и управять ими одновременно.
Вот набросок кода. Операция doIt крупного объекта вызывает doIt для каждой из своих частей. Компоновщик удобно создавать при помощи строителя.
class CPart { void add (CPart* ) = 0; void remove (CPart* ) = 0; // Псеводокод virtual void doIt ( ) = 0; }; class CCompositePart : public CPart { void add (CPart* _part) {m_list.add(_part); } void remove (CPart* _part) {m_list.remove(_part); } // Псеводокод virtual void doIt ( ) { // выполнить doIt() для всех объектов в коллекции m_list }; private: CList<CPart*> m_list; }; class CBuilding : public CCompositePart { // ... void doIt(); }; class CBrick : public CPart { // ... void doIt(); };
А вот структура.
Снова к нашей игре.
Пусть юнитам (CUnit) в нашей нужно бывает окружать себя простой рамкой. Хорошим решением будет создать новый подкласс - который будет окружать юнит рамкой (CBorderedUnit). А иногда нужна прокрутка, например если строение слишком большое, чтобы его отражать полностью. Тогда создаем подкласс - который реализует прокрутку (CScrollUnit). Ну, конечно, если понадобится прокрутка и простая рамка, то породим класс (CBorderedScrollUnit) - множественным наследованием. Или один от другого. Только это не решит проблемы, поскольку вот еще видел я в Visual... такую рамочку вокруг компонента, которая такая из восьми черных квадратиков, она еще позволяет изменять размер и положение (ах, Малевич, не поняли его в России, а какое влияние на мир оказал). Эта рамка нужна больше жизни. И теперь мы получаем еще пачку классов CDesing, CDesignBordered, CDesignBordered - и так далее.
В общем, решение неудачное.
А вот решение получше: Пусть всякие рамки компонетам добавляет другой компонент-контейнер, который все запросы к себе исправно перенаправляет лежащему в нем объекту, а от себя добавляет только минимальное дополнительное поведение. При этом контейнер видит базовый класс компонента, и если мы положим в контейнер потомок базового класса или другой похожий контейнер, то он не заметит подмены. Это как в дельфях Вы выкладываете на форму панель, на панель скроллер, на скроллер картинку. О такой феньке я рассказывал в Шаге 7 "Стилей и идиом", там это называлось интерфейсным указателем, а здесь - декоратором; кроме того, требуется порождать компонент и его декоратор от одного базового класса, чтобы уже ИХ контейнер так же не замечал подмены.
// Общий предок с функцией отрисовки. class CUnit { public: virtual void draw (); }; // Какой-то конкретный юнит со своей отрисовкой. class CSomeUnit : public CUnit { public: void draw (); }; // базовый декоратор, перенаправляет вызовы class CDecorator : public CUnit { public: CUnit* m_unit; // Все декораторы перенаправляют вызовы как минимум void draw () {m_unit->draw(); }; }; //Конкретный декоратор, добавляет собственное поведение class CBorderDecorator : public CDecorator { // вот оно, собственное поведение void drawBorder (); public: void draw () { // сначала вызывается общее поведение CDecorator::draw(); // а теперь частное. drawBorder (); }; };