Намір

Вказує тип об'єктів для створення використовуючи прототипне створення примірників і створення нових об'єктів копіюванням цього прототипу.

Мотивація

Ви можете побудувати редактор музичних паритур, конфігуруючи загальний фреймворк для графічних редакторів і додаючи нових об'єктів, які представляють ноти, паузи і нотний стан. Фреймворк редактора може мати палітру інструментів для додавання цих музичних об'єктів до паритури. Палітра буде також включати інструменти для виділення, переміщення чи іншого маніпулювання музичними об'єктами. Користувачі вибиратимуть мишкою інструмент четвертної ноти і використовувати її для додавання четвертних нот до паритури. Або вони можуть використати інструмент пересування для пересування ноти вверх чи вниз на нотному стані, тобто змінюючи її звучання. Давайте припустимо, що фреймворк забезпечує абстрактний клас Graphic для графічних компонентів, на подобі нот і нотний стан. Більш того він постачає абстрактний клас Tool для визначення інструментів на подобі тих, які є у палітрі. Фреймворк також визначає потомок GraphicTool для інструментів, які створюють примірники графічних об'єктів і додають їх до документа. Але GraphicTool представляє проблему для інженера фреймворку. Класи для нот і нотного стану являється специфічними для нашої програми, але клас GraphicTool належить до фреймворку. GraphicTool не знає як створювати примірники для наших музичних класів для додавання до нотного стану. Ми можемо створити потомок GraphicTool для кожного типу музичного об'єкту, але цей метод буде продукувати багато потомків, які відрізняються тільки типом музичного об'єкту, примірник якого вони створюють. Ми знаємо, що об'єктна композиція є гнучкою альтернативою до створення потомків. Питання у тому, як фреймворк використовує його для параметризування примірників GraphicalTool класом Graphic, який вони повинні створювати? Рішення полягає у тому, щоб змусити GraphicTool створювати новий примірник класу Graphic копіюванням чи “клонуванням” примірника потомка Graphic. Ми називаємо цій примірник прототип. GraphicTool є параметризований прототипом, якого він повинен клонувати і додати до документу. Якщо усі потомки класу Graphic підтримують операцію Clone, тоді GraphicTool може клонувати будь-який підтип класу Graphic. Отож у нашому музичному редакторі, кожен інструмент для створення музичного об'єкту являється примірником класу GraphicTool, який ініціалізований за допомогою іншого прототипу. Кожен примірник GraphicTool продукуватиме об'єкт, клонуючи його прототип і додаючи клон до нотного стану. fig.3.fig10_Prototype_motivation_1 Ми можемо використовувати шаблон Прототип для зменшення числа класів навіть більше. Ми відокремили класи для цілої ноти і для половинної, але це мабуть необов'язково. Замість цього, вони можуть бути примірниками одного класу ініціалізованого за допомогою різних зображень і тривалостей. Інструмент для створення цілих нот перетворюється просто у GraphicTool, чий прототип являється примірник класу MusicalNote, який ініціалізований для того, щоб бути цілою нотою. Це може драматично зменшити число класів у системі. Цей спосіб також полегшує додавання нових типів нот до музичного редактора.

Застосовуваність

Використовуйте шаблон Прототип коли система повинна бути незалежною від того, як її продукти створюються, складаються і представляються; і
  • коли класи, примірники яких необхідно створити, вказуються під час виконання програми, наприклад, динамічним завантаженням, чи
  • для запобігання будування ієрархії класів фабрик, які паралельні класам продуктів; чи
  • коли примірники класів можуть мати один з лиш декількох різних комбінацій станів. Більш зручно встановлювати відповідне число прототипів і клонувати їх, ніж створювати примірники класів вручну, кожного разу з необхідним станом.

Структура

fig.3.fig11_Prototype_structure

Учасники

  • Prototype (Прототип — Graphic)
    • оголошує інтерфейс для свого клонування
  • ConcretePrototype (ПевнийПрототип — Staff, WholeNote, HalfNote)
    • реалізовує операція для свого клонування
  • Client (Клієнт — GraphicTool)
    • створює новий об'єкт подаючи запит до прототипу, щоб він себе клонував.

Співробітництво

  • Клієнт запитує прототип на його клонування.

Наслідки

Прототип має багато тих самих наслідків, що й Абстрактна Фабрика (000) і Будівник (000): він приховує класи конкретних продуктів від клієнта, у зв'язку з тим зменшує кількість імен, які необхідно знати клієнтам. Більше того, ці шаблони дозволяють клієнтам працювати з програмно-залежними класами без модифікації. Додаткові переваги шаблону Прототип перераховані нижче.
  1. Добавляння і видаляння продуктів під час виконання програми. Прототип дозволяє вам вставляти нові класи продуктів у систему, просто реєструючи прототипний примірник з клієнтом. Це трішки більш гнучко ніж інші створюючі шаблони через те, що клієнт може інсталювання і видалити прототипи під час виконання програми.
  2. Створення нових об'єктів варіаціями значень. Високо динамічні системи дозволяють вам визначити нову поведінку за допомогою об'єктної композиції — вказуючи значення для змінних об'єктів, наприклад, а не створюючи нові класи. Ви ефективно визначаєте нові типи об'єктів створюючи примірники існуючих класів і реєструючи примірники, як прототипи об'єктів-клієнтів. Клієнт може демонструвати нову поведінку, делегуючи відповідальність до прототипу. Цей тип дизайну дозволяє користувачам визначати нові “класи” без програмування. Фактично, клонування прототипу подібне до створення примірника класу. Шаблон Прототип може сильно зменшити кількість класів, які потребує система. У нашому музичному редакторі, один клас GraphicTool може створювати необмежені варіації музичних об'єктів.
  3. Створення нових об'єктів варіацією структур. Велика кількість програм будує об'єкти з частин і субчастин. Редактори для створення схем, наприклад, будують схеми з менших схем1. Для зручності, такі програми часто дозволяють вам створювати примірники складних, користувацьких структур, скажемо, для використання певних дочірніх схем знову і знову. Шаблон Прототип підтримує це також. Ми просто додаємо цю дочірню схему як прототип до палітри доступних елементів схеми. Настільки композитний об'єкт схеми реалізовує операцію Clone як глибоку копію, схеми з різними структурами можуть бути прототипами.
  4. Зменшення кількості необхідних потомків. Метод Фабрики (000) часто продукує ієрархію класів Creator, яка паралельна ієрархії класів продуктів. Шаблон Прототип дозволяє вам клонувати прототип замість того, щоб опитувати метод фабрики для створення нового об'єкту. Отже вам не потрібен ієрархія класу Creator взагалі. Ця перевага проявляється в основному в таких мовах як С++, які не трактують класи як головні об'єкти. Мови, які діють протилежно, на подобі Smalltalk i Objective C, отримують менше переваг, оскільки ви завжди можете використовувати об'єкт класу як створювач. Об'єкти класів вже діють як прототипи у цих мовах.
  5. Динамічне конфігурування програми за допомогою класів. Деякі середовища виконання дозволяють вам завантажувати класи у програму динамічно. Шаблон прототип являється ключом для використання даних можливостей у мовах на подобі С++. Програма, яка бажає створювати примірники динамічно завантаженого класу не буде здатна зв'язувати його конструктор статично. Замість того, середовище виконання створює примірники кожного класу автоматично, коли вони завантажені, і воно реєструє примірники за допомогою менеджера прототипів (перегляньте секцію Реалізація). Тоді програма може запитати менеджера прототипів за примірниками нових завантажених класів, класів які не оригінально не зв'язані з програмою. Програмний фреймворк ET++ [WGM88] має систему, яка працює під час виконання і використовує дану схему.
Головна відповідальність шаблону Прототип полягає у тому, що кожен потомок Прототипу повинен реалізовувати операцію Clone, що може видатись складним. Наприклад, додавання операції Clone являється складним коли відповідні класи вже існують. Реалізування операції Clone може бути складним коли їхні складові включають об'єкти, які не підтримують копіювання чи мають рекурсивні зв'язки.

Реалізація

Прототип надзвичайно корисні у таких статичних мовах на подобі С++, де класи не об'єкти, і невелика кількість інформації або її повна відсутність доступна під час виконання програми. Це менш важливо у мовах на подобі Smalltalk чи Objective C, які забезпечують повну інформацію про прототип (тобто, клас об'єкта) для створення примірників кожного класу. Цей шаблон вбудований у заснованих на прототипах мовах на подобі Self [US87], у яких усі створення об'єктів відбуваються за допомогою клонування прототипу. Розглянемо наступні проблеми під час реалізації прототипів:
  1. Використання менеджера прототипів. Коли кількість прототипів у системі не фіксоване (тобто, вони можуть бути створеними і знищеними динамічно), тримайте реєстр доступних прототипів. Клієнти не будуть самостійно виконувати управління прототипами, але все-одно зберігати і отримувати їх з реєстру. Клієнт буде запитувати реєстр за прототипом перед тим, як клонувати його. Ми називаємо цей реєстр менеджером прототипів. Менеджер прототипів являється асоціативним складом, який повертає прототипи, які відповідають заданим ключам. Він має операції для реєстрування прототипу за даним ключом і для його розуміння. Клієнти можуть змінювати чи навіть переглядати реєстр під час виконання програми. Це дозволяє клієнтам розширювати і робити переоблік системи без написання коду.
  2. Реалізування операції Clone. Найважчою частиною шаблону Прототип являється коректна реалізація операції Clone. Це надзвичайно складно коли структура об'єктів містить рекурсивні зв'язки. Більшість мов програмування забезпечують підтримку клонування об'єктів. Наприклад, Smalltalk забезпечує реалізацію копіювання, яка успадковується усіма потомками класу Object. С++ постачає конструктор копіювання. Але ці можливості не вирішують проблему “дрібного копіювання в противагу глибокого копіювання” [GR83]. Тобто, виконувати копіювання об'єкту разом з його примірниками внутрішніх змінних, чи виконувати копіювання, після якого оригінал просто розділяє внутрішні змінні з іншими об'єктами? Дрібне копіювання являється простим і часто достатнім і це те, що Smalltalk виконує за умовчанням. Стандартний конструктор копіювання у С++ виконує копіювання, у якому вказівники будуть розділятися між копією і оригіналом. Але клонування прототипів з складною структурою зазвичай вимагає глибокого копіювання через те, що клон і оригінал повинні бути незалежними. Отже ви повинні запевнити, що компоненти клону являються клонами компонентів прототипу. Клонування змушує вас зробити рішення чи взагалі будь-що буде розділятися між об'єктами. Якщо об'єкти у системі забезпечують операції Save і Load, тоді ви можете використовувати для забезпечення реалізації за умовчанням операції Clone, зберігаючи об'єкти і негайно завантажуючи їх назад у пам'ять. Операція Save зберігає об'єкт у буфер пам'яті, а Load створює дублікат реконструюючи об'єкт з буферу.
  3. Ініціалізація клонів. Поки деякі клієнти повністю задоволені типовим клоном, таким який він є, інші ж навпаки — забажають ініціалізувати деякі або усі його внутрішні стани до значень обраними ними. Ви зазвичай не можете передати ці значення у операцію Clone через те, що їхня кількість буде змінюватися між класами прототипів. Деякі прототипи можуть потребувати множинні ініціалізовуючі параметри; інші зовсім не потребуватимуть їх. Передавання параметрів до операції Clone робить важчим уніфікування інтерфейсу клонування. Це може бути випадком, у якому ваші класи прототипів вже визначили операції для встановлення (чи оновлення) ключових частин станів. Якщо так, клієнти можуть негайно використовувати ці операції одразу після клонування. Якщо ні, тоді ви можете запровадити операцію Initialize (перегляньте секцію Приклад Коду), якій передаються ініціалізовуючі параметри в якості аргументів і відповідно встановлюють внутрішні стани клона. Стережіться операцій Clone, які виконують глибоке копіювання — копії можуть бути видалиними (або явно, або всередині Initialize) перед тим, як ви їх повторно ініціалізуєте.

Приклад Коду

Ми визначимо потомок MazePrototypeFactory класу MazeFactory. Клас MazePrototypeFactory буде ініціалізовуватися за допомогою прототипів об'єктів, які він створюватиме, отож нам не потрібно створювати його потомки просто для того, щоб змінювати класи стін чи кімнати, які він створюватиме. MazePrototypeFactory збільшу інтерфейс класу MazeFacory конструктором, якому передаються прототипи як аргументи:
class MazePrototypeFactory : public MazeFactory 
{ 

  public:

      MazePrototypeFactory(Maze*, Wall*, Room*, Door*) ; 

      virtual Maze* MakeMaze () const ; 
      virtual Room* MakeRoom (int) const ; 
      virtual Wall* MakeWall () const ; 
      virtual Door* MakeDoor (Room*, Room*) const ; 

  private: 

      Maze* _prototypeMaze ; 
      Room* _prototypeRoom ; 
      Wall* _prototypeWall ; 
      Door* _prototypeDoor ; 

} ;
Новий конструктор просто ініціалізовує його прототипи:
MazePrototypeFactory :: MazePrototypeFactory ( Maze* m, 
                                              Wall* w, 
                                              Room* r, 
                                              Door* d ) 
{ 
     _prototypeMaze = m ; 
     _prototypeWall = w ; 
     _prototypeRoom = r ; 
     _prototypeDoor = d ; 
}
Метод класу для створення стін, кімнат і дверей являється подібним: кожен клонує прототипи і тоді ініціалізовує їх. Ось визначення методів MakeWall i MakeDoor:
Wall* MazePrototypeFactory :: MakeWall () const 
{ 
      return _prototypeWall->Clone() ; 
} 

Door* MazePrototypeFactory :: MakeDoor (Room* r1, Room *r2) const 
{ 
      Door* door = _prototypeDoor->Clone() ; 
      door->Initialize(r1, r2) ; 
      return door ; 
}
Ми можемо використати MazePrototypeFactory для створення прототипних або стандартних лабіринтів просто ініціалізуванням його за допомогою прототипів базових компонентів лабіринту:
MazeGame game;
MazePrototypeFactory simpleMazeFactory (new Maze, 
                                        new Wall, 
                                        new Room, 
                                        new Door ); 

Maze* maze = game.CreateMaze(simpleMazeFactory) ;
Для зміни типу лабіринту, ми ініціалізовуємо MazePrototypeFactory за допомогою множини прототипів. Наступний виклик створює лабіринт з примірниками класів BombedDoor і RoomWithABomb:
MazePrototypeFactory bombedMazeFactory (new Maze, 
                                        new BombedWall, 
                                        new RoomWithABomb, 
                                        new Door ) ;
Об'єкт, який можна використати як прототип, на подобі примірника Wall, повинен підтримувати операцію Clone. Він також повинен мати конструктор копіювання для клонування. Він може також потребувати окрему операцію для оновлення внутрішнього стану (повторного ініціалізування). Ми додамо операцію Initialize до класу Door для того, щоб дозволити клієнтам ініціалізовувати клони кімнат. Порівняйте наступне визначення класу Door:
class Door : public MapSite 
{ 

  public : 

      Door() ; 
      Door(const Door&) ; 

      virtual void Initialize(Room*, Room*) ; 
      virtual Door* Clone() const ; 
      virtual void Enter() ; 
      Room* OtherSideFrom(Room*) ; 

  private: 

      Room* _room1 ; 
      Room* _room2 ; 

} ; 

Door :: Door (const Door& other) 
{ 
      _room1 = other._room1 ; 
      _room2 = other._room2 ; 
}

void Door::Initialize (Room* r1, Room* r2) 
{ 
      _room1 = r1 ; 
      _room2 = r2 ; 
} 

Door* Door :: Clone () const 
{ 
      return new Door(*this) ; 
}
Дочірній клас BombedWall повинен перевизначити операцію Clone і реалізовувати відповідний конструктор копіювання.
class BombedWall : public Wall 
{ 

  public : 

      BombedWall () ; 
      BombedWall (const BombedWall&) ; 

      virtual Wall* Clone () const; 
      bool HasBomb () ; 

  private: 

      bool _bomb; 

} ; 

BombedWall::BombedWall (const BombedWall& other) : Wall(other) 
{ 
      _bomb = other._bomb; 
} 

Wall* BombedWall::Clone () const 
{ 
      return new BombedWall(*this); 
}
Хоча BombedWall::Clone повертає Wall*, його реалізація повертає вказівник на новий примірник дочірнього класу, тобто, BombedWall*. Ми визначимо метод Clone таким способом у базовому класі щоб впевнитись, що клієнти, які клонують прототип не повинні мати інформації про їхні конкретні потомки. Клієнти ніколи не повинні перетворювати тип поверненого значення від методу Clone до бажаного типу. У Smalltalk, ви можете повторно використати стандартний метод копіювання, який успадкований від Object для клонування будь-якого MapSite. Ви можете використати MazeFactory для продукування прототипів які вам необхідні; наприклад, ви можете створити кімнату передаючи ім'я “#room”. Метод MazeFactory має словник, який зв'язує імена і прототипи. Метод make виглядає наступним чином:
make: partName 
   ^ (partCatalog at: partName) copy
Даючи відповідні методи для ініціалізування MazeFactory прототипами, ви можете створити простий лабіринт за допомогою наступного коду:
CreateMaze 
   on: (MazeFactory new 
      with: Door new named: #door; 
      with: Wall new named: #wall; 
      with: Room new named: #room; 
      yourself)
Де визначення методу on: метод класу для CreateMaze буде виглядати
on: aFactory 
    | room1 room2 | 
    room1 := (aFactory make: #room) location: 1@1. 
    room2 := (aFactory make: #room) location: 2@1. 
    door := (aFactory make: #door) from: room1 to: room2. 

    room1 
        atSide: #north put: (aFactory make: #wall); 
        atSide: #east put: door; 
        atSide: #south put: (aFactory make: #wall); 
        atSide: #west put: (aFactory make: #wall). 

    room2 
        atSide: #north put: (aFactory make: #wall); 
        atSide: #east put: (aFactory make: #wall); 
        atSide: #south put: (aFactory make: #wall); 
        atSide: #west put: door. 
   ^ Maze new 
        addRoom: room1; 
        addRoom: room2; 
        yourself

Відомі використання

Можливо першим прикладом шаблону Прототип була система Sketchpad розроблена Ivan Sutherland [Sut63]. Першою широковідомою програмою з використанням шаблону у об'єктно-орієнтованій мові була ThingLab, де користувачі могли сформувати композитний об'єкт і тоді надати його до прототипу, інсталюючи його у бібліотеці багаторазових об'єктів. Goldberg і Robson згадували прототип як шаблон [GR83]. Але Coplien [Cop92] дає набагато більш повне описання. Він описує ідіоми, які відносяться до шаблону Прототип для C++ і дає багато прикладів і варіацій. Програма Etgdb являється відлагоджувачем заснованим на ET++, який забезпечує інтерфейс вкажи-і-натисни (point-and-click) до різних лінійно-орієнтованих відладників. Кожен відладник має відповідний потомок DebuggerAdaptor. Наприклад, GdbAdaptor налаштовує etgdb до командного синтаксису відладника GNU gdb, а SunDbxAdaptor налаштовує etgdb для відладника dbx компанії Sun. Etgdb не має жорстко закодований у нього набір класів DebuggerAdaptor. Замість того, він читає ім'я адаптеру, який він повинен використовувати з змінної середовища, шукає адаптер з вказаним ім'ям у глобальній таблиці і тоді клонує прототип. Нові відладники можуть бути доданими до etgdb, його зв'язуванням з DebuggerAdaptor, який працює для даного відладника. “Бібліотека технік взаємодії” (“interaction technique linbrary”) у Mode Composer зберігає прототипи об'єктів, які підтримують декілька технік взаємодій [Sha90]. Будь-яка техніка взаємодії створена за допомогою Mode Composer може бути використаною як прототип, вкладанням її у цю бібліотеку. Шаблон прототип дозволяє Mode Composer підтримувати необмежений набір технік. Приклад музичного редактора, обговорений раніше, заснований на фреймворку малювання Unidraw [VL90].

Споріднені шаблони

Прототип і Абстрактна Фабрика являються конкуруючими шаблонами у деяких випадках, коли ми це обговоримо у кінці цієї частини. Однак, вони можуть бути використаними разом. Абстрактна фабрика може зберігати набір прототипів з яких клонуються і повертаються об'єкти-продукти. Дизайн, який робить важким використання шаблонів Композитора і Декоратора часто можуть отримати вигоду від використання Прототипу. 1Такі програми відображають шаблони Композитор і Декоратор.