Намір
Розділяє абстракцію від її реалізації, отож вони можуть варіюватись незалежно один від одного.
Також відомий як
Handle/Body
Мотивація
Коли абстракція може мати один або декілька можливих реалізацій, типовим способом їх пристосування являється успадковування. Абстрактний клас визначає інтерфейс до абстракції і конкретні потомки реалізовують її різними способами. Але цей підхід не завжди достатньо гнучкий. Успадковування постійно зв'язує реалізацію з абстракцією, що ускладнює її модифікування, розширення і незалежне багаторазове використання абстракції і реалізації.
Розглянемо реалізацію переносимої абстракції Window у інструментарію інтерфейсу користувача. Цей підхід повинен дозволити нам написати програми, які працюють на двох віконних системах X Window System і Presentation Manager компанії IBM, наприклад.
Використовуючи успадкування, ми можемо визначити абстрактний клас Window і потомки Xwindow і PMWindow, які реалізовують віконний інтерфейс для різних платформ. Але цей підхід має два недоліки:
- Він незручний для розширення абстракції Window для різних типів вікон чи нових платформ. Уявіть потомок iconWindow класу Window, який спеціалізовується на абстракції Window для іконок. Щоб підтримувати IconWindow на обох платформах, ми повинні реалізувати два нових класи, XiconWindow і PMIconWindow. Гірше того, ми повинні визначити два класи для кожного типу вікон. Підтримка третьої платформи вимагає ще одного потомка Window для кожного типу вікон.
- Він робить клієнтський код платформо-залежним. Кожного разу, коли клієнт створює вікно, він створює конкретний клас, який має певну реалізацію. Наприклад, створення об'єкту Xwindow зв'язує абстракцію Window до реалізації X Window. Це, у свою чергу, робить важчим портування клієнтського коду на інші платформи.
Клієнти повинні бути здатними створювати вікно без звернення до певної реалізації. Тільки реалізація вікна повинна залежати від платформи на якій виконується програма. Тому клієнтський код повинен створювати вікна без згадування певних платформ.
Шаблон Міст звертається до цих проблем, вкладанням віконної абстракції і її реалізації в окремі класові ієрархії. Існує одна класова ієрархія для інтерфейсу вікна (Window, IconWindow, TransientWindow) і окрема ієрархія для платформенно-залежної віконної реалізації, з класом WindowImp в її корені. Потомок XwindowImp, наприклад, забезпечує реалізацію, засновану на на віконній системі X Window System.
Усі операції на потомках Window реалізовуються у термінах абстрактних операцій від інтерфейсу WindowImp. Це розділяє віконну абстракцію від декількох платформенно-залежних реалізацій. Ми вказуємо на відносини між Window і WindowImp як міст, через те що він створює міст між абстракцією і її реалізацією, дозволяючи їм варіюватись незалежно.
Застосовуваність
Використовуйте шаблон Міст коли
- ви бажаєте запобігти постійний зв'язок між абстракцією і її реалізацією. Це може бути випадок, наприклад, коли реалізація повинна бути обрана чи перемкнута під час виконання програми.
- І абстракції і їхні реалізації повинні бути розширюваними за допомогою створення потомків. У цьому випадку, шаблон Міст дозволяє вам комбінувати різні абстракції і реалізації і розширювати їх незалежно.
-
Зміни у реалізації абстракції не повинні впливати на клієнтів; тобто, їхній код не повинен бути перекомпільованим.
-
(С++) ви бажаєте повністю сховати реалізацію абстракції від клієнтів. У С++ представлення класу являється видимим у інтерфейсі класу.
- Ви маєте розповсюдження класів, як показано раніше у першій діаграмі секції Мотивації. Така класова ієрархія показує потребу у розділенні об'єкта на дві частини. Рамбаух (Rumbaugh) використовує термін “вмонтовані узагальнення” (“nested generalizations”) [RBP+91] для відношення до таких класових ієрархій.
- Ви бажаєте розділити реалізацію між багатьма об'єктами (можливо використовуючи обрахунок посилань), і цей факт повинен бути прихованим від клієнтів. Простий приклад являється клас String Коплайна (Coplien) [Cop92], у якому багато об'єктів можуть розділяти одне і те ж представлення рядка (StringRep).
Структура
Учасники
- Abstraction (Абстракція — Window)
- визначає інтерфейс абстракції
- підтримує посилання на об'єкт типу Implementor
- RefinedAbstraction (УдосконаленаАбстракція - IconWindow)
- Розширює інтерфейс визначений Abstraction
- Implementor (Реалізатор — WindowImp)
- визначає інтерфейс для класів реалізації. Цей інтерфейс не повинен точно відповідати до інтерфейсу класу Abstraction; фактично два інтерфейси можуть доволі відрізнятися. Зазвичай інтерфейс типу Implementor забезпечує тільки примітивні операції і Abstraction визначає високорівневі операції, які базуються на цих примітивах.
- ConcreteImplementor (ПевнийРеалізатор — XwindowImp, PMWindowImp)
- реалізує інтерфейс типу Implementor і визначає його конкретну реалізацію.
Співпрацювання
Абстракція передає запити клієнта до його об'єкту типу Implementor.
Наслідки
Шаблон Міст має наступні наслідки використання:
- Розділення інтерфейсу і реалізації. Реалізація постійно не зв'язана з інтерфейсом. Реалізація абстракції може бути сконфігурованою під час виконання програми. Навіть можливо змінити для об'єкта його реалізацію під час виконання програми.
Розділення Abstraction i Implementor також виключає залежності реалізації під час компілювання програми. Зміна класу реалізації не вимагає перекомпілювання класу Abstraction і його клієнтів. Ця властивість суттєва коли ви повинні бути впевнені в бінарній сумісності між різними версіями бібліотек класів.
Більше того, це розділення заохочує розшарування, що може призвести до краще структурованої системи. Високорівнева частина системи повинна тільки знати про типи Abstraction і Implementor.
- Покращена розширюваність. Ви можете незалежно розширювати ієрархії Abstraction i Implementor.
- Приховування реалізаційних деталей від клієнтів. Ви можете захистити клієнтів від реалізаційних деталей, на подобі спільного використання об'єктів реалізаторів і супроводжуючий механізм обрахунку посилань (якщо він існує).
Реалізація
Розгляньте наступні реалізаційні питання, які необхідно пам'ятати під час застосування шаблону Міст:
- Тільки один тип Implementor. У ситуаціях в яких існує тільки один реалізатор, створення абстрактного класу Implementor не є обов'язковим. Це є дегенеративний випадок шаблону Міст; існує взаємовідносини один-до-одного між Abstraction i Implementor. Проте, це розділення все одно корисне коли зміна у реалізації класу не повинна нести ефект на його існуючих клієнтів, тобто вони не повинні бути перекомпільованими, а просто перекомпонованими.
Каролан (Carolan) [Car89] використовує термін “Чеширський Кіт” (“Cheshire Cat”) для опису цієї ситуації. У С++, інтерфейс класу типу Implementor може бути визначений у приватному заголовковому файлі, який не постачається клієнтам. Це дозволяє вам повністю приховати реалізацію класу від його клієнтів.
- Створення правильного об'єкта Implementor. Як, коли і де ви вирішуєте створити примірник певного класу Implementor коли їх більше ніж один?
Якщо тип Abstraction знає про усі конкретні класи ConcreteImplementor, тоді він може створити примірник одного з них у його конструкторі; він може вибрати один з них, базуючись на параметрах переданих до його конструктора. Якщо, наприклад, колекція класів підтримує велику кількість реалізацій, вибір може базуватися на розмірі колекції. Зв'язаний список реалізацій може використовуватися для малих колекції хеш-таблиця для великих.
Інший підхід полягає у початковому виборі реалізації по замовчуванню і пізнішої її зміни згідно з використанням. Наприклад, якщо колекція виросла більша за певну межу, тоді вона перемикає свою реалізацію до тієї, яка більш відповідна для великої кількості елементів.
Також можливо делегувати рішення цілком до іншого об'єкту. У прикладі Window/WindowImp, ми можемо впровадити об'єкт (перегляньте Абстрактна Фабрика) єдиний обов'язок якого полягає в інкапсулюванні платформи. Фабрика знає який тип об'єкту WindowImp створити для платформи, яку вона використовує; Window просто опитує її за певним типом WindowImp, і вона повертає необхідний. Перевага цього підходу полягає у тому, що тип Abstraction не зв'язаний прямо з будь-якими класами Implementor.
- Спільне використання реалізації. Коплайн (Coplien) ілюструє як ідіома Handle/Body у С++ може бути використана для розділення реалізацій між декількома об'єктами [Сop92]. Тип Body зберігає кількість посилань, які клас Handle додає і віднімає. Код для зв'язування управління з розподіленими тілами має наступну загальну форму:
Handle& Handle :: operator = (const Handle& other)
{
other._body->Ref() ;
_body->Unref() ;
if (_body->RefCount() == 0)
{
delete _body ;
}
_body = other._body ;
return *this ;
}
- Використання множинного успадкування. Ви можете використати множинне успадкування у С++ для комбінування інтерфейсів разом з їхньою реалізацією [Mar91].
Наприклад, клас може публічно успадкувати від типу Abstraction і приватно від типу ConcreteImplementor. Але через те, що цей підхід базується на статичному успадкуванні, він постійно зв'язує реалізацію з його інтерфейсом. Тому ви не можете реалізувати справжній Міст з множинним успадкуванням — в крайній мірі не у С++.
Приклад Коду
Наступний код С++ реалізовує приклад Window/WindowImp з секції Motivation. Клас Window визначає віконну абстракцію для програм клієнтів:
class Window
{
public:
Window(View* contents);
// запити обробляються вікном
virtual void DrawContents();
virtual void Open();
virtual void Close();
virtual void Iconify();
virtual void Deiconify();
// запити передаються до реалізації
virtual void SetOrigin(const Point& at);
virtual void SetExtent(const Point& extent);
virtual void Raise();
virtual void Lower();
virtual void DrawLine(const Point&, const Point&);
virtual void DrawRect(const Point&, const Point&);
virtual void DrawPolygon(const Point[], int n);
virtual void DrawText(const char*, const Point&);
protected:
WindowImp* GetWindowImp();
View* GetView();
private:
WindowImp* _imp;
View* _contents; // the window's contents
} ;
Window підтримує посилання до WindowImp, абстрактний клас, який оголошує інтерфейс до основної віконної системи.
class WindowImp
{
public:
virtual void ImpTop() = 0;
virtual void ImpBottom() = 0;
virtual void ImpSetExtent(const Point&) = 0;
virtual void ImpSetOrigin(const Point&) = 0;
virtual void DeviceRect(Coord, Coord, Coord, Coord) = 0;
virtual void DeviceText(const char*, Coord, Coord) = 0;
virtual void DeviceBitmap(const char*, Coord, Coord) = 0;
// багато інших функцій для малювання у вікні...
protected:
WindowImp();
} ;
Потомки типу Window визначають інші типи вікон, які може використовувати програма, на подобі програмних вікон, ікон, тимчасові вікна для діалогових вікон, плаваючі палітри інструментів і так далі.
Наприклад, тип ApplicationWindow буде реалізувати DrawContents для відмалювання примірника View, яку він зберігає:
class ApplicationWindow : public Window
{
public:
// ...
virtual void DrawContents();
} ;
void ApplicationWindow :: DrawContents ()
{
GetView()->DrawOn(this);
}
Тип IconWindow зберігає ім'я бітової маски для іконки, яку вона зберігає...
class IconWindow : public Window
{
public:
// ...
virtual void DrawContents();
private:
const char* _bitmapName;
} ;
... і він реалізовує DrawContents для відмальовування бітової маски на вікні:
void IconWindow :: DrawContents()
{
WindowImp* imp = GetWindowImp() ;
if (imp != 0)
{
imp->DeviceBitmap(_bitmapName, 0.0, 0.0) ;
}
}
Можливі багато інших варіацій типу Window. Тип TransientWindow може потребувати спілкуватися з вікном, яке створило його під час діалогу; отже він утримує посилання до цього вікна. PalletteWindow завжди плаває понад іншими вікнами. Тип IconDocWindow утримує примірники IconWindow і їх акуратно аранжує.
Віконні операції визначені у термінах інтерфейсу WindowImp. Наприклад, DrawRect витягує чотири координати з його двох параметрів типу Point перед викликом операції WindowImp, яка відмальовує прямокутник у вікні:
void Window :: DrawRect (const Point& p1, const Point& p2)
{
WindowImp* imp = GetWindowImp();
imp->DeviceRect(p1.X(), p1.Y(), p2.X(), p2.Y());
}
Конкретні потомки типу WindowImp підтримують різні віконні системи. Потомок XwindowImp підтримує X Window System:
class XWindowImp : public WindowImp
{
public:
XwindowImp();
virtual void DeviceRect(Coord, Coord, Coord, Coord);
// публічний інтерфейс...
private:
// багато віконно-залежних станів
// системи X Window, включаючи:
Display* _dpy;
Drawable _winid; // ідентифікатор вікна
GC _gc; // віконний графічний контекст
} ;
Для Presentation Manager (PM), ми визначимо клас PMWindowImp:
class PMWindowImp : public WindowImp
{
public:
PMWindowImp();
virtual void DeviceRect(Coord, Coord, Coord, Coord);
// публічний інтерфейс...
private:
// велика кількість системно-залежних
// станів вікон PM, включаючи
HPS _hps;
} ;
Ці потомки реалізують операції WindowImp у термінах примітивів віконних систем. Наприклад, DeviceRect реалізовується для системи X Window наступним чином:
void XwindowImp :: DeviceRect ( Coord x0,
Coord y0,
Coord x1,
Coord y1 )
{
int x = round(min(x0, x1)) ;
int y = round(min(y0, y1)) ;
int w = round(abs(x0 — x1)) ;
int h = round(abs(y0 — y1)) ;
XDrawRectangle(_dpy, _winid, _gc, x, y, w, h) ;
}
Реалізація для системи PM може виглядати наступним чином:
void PMWindowImp :: DeviceRect ( Coord x0,
Coord y0,
Coord x1,
Coord y1 )
{
Coord left = min(x0, x1) ;
Coord right = max(x0, x1) ;
Coord bottom = min(y0, y1) ;
Coord top = max(y0, y1) ;
PPOINTL point[4] ;
point[0].x = left ;
point[0].y = top ;
point[1].x = right ;
point[1].y = top ;
point[2].x = right ; point[2].y = bottom ;
point[3].x = left ; point[3].y = bottom ;
if (
(GpiBeginPath(_hps, 1L) == false) ||
(GpiSetCurrentPosition(_hps, &point[3]) == false) ||
(GpiPolyLine(_hps, 4L, point) == GPI_ERROR) ||
(GpiEndPath(_hps) == false)
)
{
// повідомити помилку
}
else
{
GpiStrokePath(_hps, 1L, 0L);
}
}
Як вікно отримує примірник необхідного потомка WindowImp? Ми припустимо, що Window має цю відповідальність у цьому випадку. Його операція GetWindowImp отримує необхідний примірник від абстрактної фабрики (перегляньте
Абстрактна Фабрика), що ефективно інкапсулює усю специфіку віконної системи.
WindowImp* Window::GetWindowImp ()
{
if (_imp == 0)
{
_imp = WindowSystemFactory :: Instance() -> MakeWindowImp() ;
}
return _imp;
}
Метод WindowSystemFactory::Instance повертає абстрактну фабрику яка виготовляє усі залежні від віконної системи об'єкти. Для простоти, ми зробили її
Синглтоном і дозволили класу Window прямий доступ до фабрики.
Відомі використання
Приклад Window, описаний вище, походить від ET++ [WGM88]. У ET++, WindowImp називається “WindowPort” і має потомки на подобі XwindowPort i SunWindowPort. Віконний об'єкт створює відповідний об'єкт Implementor запитуючи його з абстрактної фабрики названої “WindowSystem”. WindowSystem забезпечує інтерфейс для створення платформенно-залежних об'єктів на подобі шрифтів, курсорів, бітових карт і так далі.
Дизайн ET++ Window/WindowPort розширює шаблон Bridge у тому, що WindowPort також утримує посилання назад до Window. Клас реалізатор WindowPort використовує це посилання для того щоб повідомити вікно про події специфічні для WindowPort: прийняття подій введення, зміна розміру вікна, тощо.
Коплайн (Coplien) [Cop92] i Страуструп (Stroustrup) [Str91] згадували класи Handle і дали декілька прикладів. Їхні приклади підкреслювали питання управління пам'яттю на подобі розділення представлення рядків символів і підтримка змінно-розмірних об'єктів. Наш фокус лежить більше на підтримку незалежного розширення і абстракції, і реалізації.
Libg++ [Lea88] визначає класи, які реалізовують загальні структури даних, на подобі Set, LinkedSet, HashSet, LinkedList i HashTable. Set являється абстрактним класом, який визначає множину абстракцій, поки LinkedList і HashTable являються конкретними реалізаторами для зв'язаного списку і хеш-таблиці відповідно. LinkedSet i HashSet являються реалізаторами Set, які створюють міст між Set і його конкретними частинами LinkedList i HashTable. Це являється прикладом дегенеративного (спрощеного) мосту, через те що немає абстрактного класу Implementor.
NeXT AppKit [Add94] використовує шаблон Міст у реалізації і відображення графічних зображень. Зображення може бути представленим декількома способами. Оптимальне відображення зображення залежить від властивостей дисплея, особливо його колірні властивості і його роздільна здатність. Без допомоги з AppKit, розробники повинні б визначати, яку реалізацію використовувати під різними обставинами у кожній програмі.
Щоб звільнити розробників від цих дій, AppKit постачає міст NXImage/NXImageRep. NXImage визначає інтерфейс для обробки зображень. Реалізація зображень визначена в окремій ієрархії класів NXImageRep, яка має потомки на подобі NXEPSImageRep, NXCachedImageRep i NXBitMapImageRep. NXImage підтримує посилання до одного чи більше об'єктів NXImageRep. Якщо існує більше ніж одна реалізація зображень, тоді NXImage вибирає найбільш відповідну для поточного пристрою дисплея. NXImage навіть здатна перетворювати одну реалізацію до іншою в разі потреби. Цікавим аспектом цього варіанту Моста полягає у тому, що NXImage може зберігати одночасно більш ніж одну реалізацію типу NXImageRep.
Споріднені Шаблони
Абстрактна Фабрика може створювати і конфігурувати певні мости.
Шаблон
Адаптер створений для зв'язування разом двох несумісних класів. Зазвичай він застосовується до систем після того, як вони були створеними. Міст, на противагу йому, використовується під час створення систем для того, щоб дозволити абстракції і її реалізації варіюватись незалежно.