Намір

Визначає інтерфейс для створення об'єкту, але дозволяє потомкам вирішувати примірники яких класів створювати. Метод Фабрики дозволяє класам уступати створення примірників для потомків.

Також відомий як

Віртуальний Конструктор

Мотивація

Фреймвори використовують абстрактні класи для визначення і підтримки відносин між об'єктами. Фреймворк так само часто відповідальний за створення цих об'єктів. Розглянемо фреймворк для програм, який може представити багато документів для користувача. Дві ключові абстракції у ньому являються класи Application i Document. Обидва класи є абстрактними і клієнти повинні створити їхні потомки щоб одержати їхні програмно-залежні реалізації. Для створення програм для малювання, наприклад, ми визначаємо класи DrawingApplication i DrawingDocument. Клас Application відповідальний за управління Документами і створить їх за необхідністю — коли користувач вибирає Відкрити чи Новий з меню, наприклад. Через те що створення примірника певного потомка Document являється програмно-залежним, клас Application не може завбачити який саме створювати примірник потомка Document — клас Application знає тільки коли новий документ повинен бути створеним, але не який тип документу. Це створює дилему: фреймворк повинен створити примірники класів, але він знає тільки про абстрактні класи, примірники яких він не може створити. Шаблон Метод Фабрики пропонує вирішення. Він інкапсулює знання того якого саме потомка Документу необхідно створити примірник і переміщає ці знання за межі фреймворка. fig.3.fig8.0_factory_method_motivation Потомки Application перевизначають абстрактну операцію CreateDocument класу Application для повернення відповідного потомка Document. Як тільки для потомка Application створено примірник, він може створити примірник програмно-залежного потомка Document без знань про його клас. Ми називаємо CreateDocument методом фабрики через те, що він відповідальний за “виготовлення” об'єкта.

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

Використовуйте шаблон Метод Фабрики коли
  • клас не може передбачити клас об'єкту який він повинен створити.
  • клас бажає, щоб його потомки вказували, які об'єкти створювати.
  • Класи делегують відповідальність до одного з декількох допоміжних потомків і ви бажаєте локалізувати інформацію до якого допоміжного потомка ведеться делегування.

Структура

fig.3.fig8_factory_method_structure

Учасники

  • Product (Document)
    • визначає інтерфейс об'єктів, які створює шаблон метод фабрики.
  • ConcreteProduct (MyDocument)
    • реалізовує інтерфейс Product.
  • Creator (Application)
    • оголошує метод фабрики, який повертає об'єкт типу Product. Creator також може визначити реалізацію за замовчуванням метода фабрики, який за замовчуванням повертає об'єкт ConcreteProduct.
    • Може викликати метод фабрики для створення об'єкта Product.
  • ConcreteCreator (MyApplication)
    • перевизначає метод фабрики для повернення примірника ConcreteProduct.

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

  • Creator покладається на його потомки для визначення методу фабрики, отож він повертає примірник відповідного ConcreteProduct.

Наслідки

Методи фабрики усувають потребу у зв'язуванні програмно-залежних класів у вашому коді. Код має справу тільки з інтерфейсом Product; отже він може працювати з будь-якими класами ConcreteProduct, визначеними користувачем. Потенціальний недолік методів фабрики полягає у тому, що клієнти можливо будуть змушеними створити потомки класу Creator, просто щоб створити певні об'єкти ConcreteProduct. Cтворення потомків являється хорошим випадком коли клієнт у будь-якому випадку повинен створити потомок класу Creator, але у іншому випадку клієнт тепер повинен мати справу з іншою точкою розвитку. Ось інші два додаткові наслідки шаблону Метод Фабрики:
  1. Забезпечує обробники для потомків. Створення об'єктів всередині класу за допомогою методу фабрики завжди більш гнучко ніж створення об'єкта напряму. Метод Фабрики дає потомкам обробник для забезпечення розширеної версії об'єкта. У прикладі Document, клас Document може визначити метод фабрики з іменем CreateFileDialog, який створює типовий об'єкт файлового діалогового вікна для відкривання існуючого документу. Потомок Document може визначити програмно-залежне файлове діалогове вікно перевизначаючи цей метод фабрики. У цьому випадку метод фабрики не являється абстрактним, але забезпечує хорошу реалізацію за замовчуванням.
  2. З'єднує паралельні класові ієрархії. У прикладах які ми розглядали до цього, тільки Creator'и викликали метод фабрики. Але це не повинно обов'язково бути так; клієнти можуть виявити методи фабрики корисними для них, особливо у випадку паралельних ієрархій класів. Ми отримуємо паралельні класові ієрархії, коли клас делегує деякі з його відповідальностей до окремих класів. Розглянемо графічні зображення, якими можуть маніпулювати інтерактивно; тобто, вони можуть бути розтягнутими, переміщеними чи повернутими використовуючи мишку. Реалізування таких взаємодій не завжди легко. Вона часто вимагає зберігання і оновлення інформації, яка записує стан маніпуляцій в заданий час. Цей стан потрібен тільки під час маніпулювання; тому її не потрібно тримати у об'єкті зображення. Більше того, різні зображення поводять себе по-різнму коли користувач маніпулює ними. Наприклад, розтягування зображення лінії може мати ефект пересування кінцевої точки, коли розтягування зображення тексту може змінювати його відступи.
З цими обмеженнями, краще використовувати окремий об'єкт Manipulator, який реалізовує взаємодію і відслідковує будь-який необхідний стан маніпулювання. Різні зображення будуть використовувати різні потомки Manipulator'а для обробки певних взаємодій. Результуюча ієрархія класів Manipulaor паралельна (хоча б частково) ієрархії класів Figure: fig.3.fig9_factory_method_paralel_classes Клас Figure забезпечує метод фабрики CreateManipulator, який дозволяє клієнтам створювати відповідний Manipulator зображення. Потомки Figure перевизначають цей метод для повернення примірника потомка Manipulator, який їм підходить. Альтернативно, клас Figure може реалізовувати CreateManipulator для повернення примірника Manipulator за замовчуванням і потомки Figure можуть просто успадкувати цю типову поведінку. Класи Figure, які так чинять, не потребують відповідного потомка Manipulaor — отже ієрархії тільки частково паралельні. Зверніть увагу як метод фабрики визначає зв'язок між двома класами ієрархій. Він локалізує інформацію про те, який класи належать один одному.

Реалізація

Розглянемо наступні питання при застосовуванні шаблону Мeтод Фабрики:
  1. Дві різні варіації. Двома варіаціями шаблону Метод Фабрики являються: (1) випадок, коли клас Creator являється абстрактним і не забезпечує реалізацію для методу фабрики, який він оголошує; і (2) випадок коли клас Creator являється конкретним класом і забезпечує реалізацію за замовчуванням для методу фабрики. Також можливо мати абстрактний клас, який визначає реалізацію за замовчуванням, але такий випадок менш поширений. Перший випадок вимагає наявність потомків для визначення реалізації, через те, що її немає у абстрактного класу. Він крутиться навколо дилеми створення примірників непередбачуваних класів. У другому випадку, конкретний клас Creator використовує метод фабрики в основному для гнучкості. Він слідує правилу яке твердить: “Створити об'єкти у окремій операції, отож потомки можуть перевизначати спосіб, яким вони (об'єкти) створюються”. Це правило запевняє, що за необхідності інженери дочірніх класів можуть змінювати клас об'єктів, примірники яких створює батьківський клас.
  2. Параметризовані методи фабрик. Інша варіація шаблону дозволяє методу фабрики створювати велику кількість типів продуктів. Метод фабрики отримує параметр, який ідентифікує тип об'єктів, які необхідно створити. У прикладі класу Document, клас Application може підтримувати різні типи класів Document. Ви надаєте методу CreateDocument особливий параметр для вказування типу документу, якого необхідно створити. Фреймворк графічного редактора Unidraw [VL90] використовує цей підхід для реконструювання об'єктів збережених на диску. Програма Unidraw визначає клас Creator з методом фабрики Create, якому необхідно передати ідентифікатор класу в якості аргументу. Ідентифікатор класу вказує клас, примірник якого необхідно створити. Коли програма Unidraw зберігає об'єкт на диск, вона спочатку записує ідентифікатор класу і тоді його примірники змінних. Коли вона реконструює об'єкт з диску, вона спочатку читає ідентифікатор класу. Як тільки ідентифікатор класу прочитаний, фреймворк викликає Create, передаючи ідентифікатор як параметр. Метод Create шукає конструктор для відповідного класу і використовує його для створення об'єкта-примірника. В кінцевому рахунку, метод Create викликає операцію Read об'єкта, яка читає відповідну інформацію з диску і ініціалізовує примірники змінних об'єкта. Параметризований метод фабрики має наступну загальну форму, де MyProduct і YourProduct являються потомками Product:
    class Creator 
    { 
    
      public: 
    
          virtual Product* Create(ProductId) ; 
    
    } ; 
    
    Product* Creator :: Create (ProductId id) 
    { 
          if (id == MINE) 
              return new MyProduct; 
    
          if (id == YOURS) 
              return new YourProduct; 
    
          // повторити для інших продуктів...
    
          return 0; 
    }
    Перевизначення параметризованої фабрики дозволяє вам легко і вибірково розширювати чи змінювати продукти, які створює метод Create. Ви можете вводити нові ідентифікатори для нових типів продуктів, або ви можете асоціювати існуючі ідентифікатори з іншими продуктами. Наприклад, потомок MyCreator може змінити місцями MyProduct i YourProduct, і підтримувати новий потомок TheirProduct:
    Product* MyCreator :: Create (ProductId id) 
    { 
          if (id == YOURS) return new MyProduct; 
          if (id == MINE) return new YourProduct; 
    
          // обернені умови
    
          if (id == THEIRS) return new TheirProduct; 
    
          return Creator :: Create(id); /* Викликається якщо 
                                        ** попередні умови провалились */
    }
    Зверніть увагу, що останньою дією, яку дана операція виконує, являється виклик методу Create з батьківського класу. Це через те, що метод MyCreator::Create обробляє тільки значення YOURS, MINE i THEIRS по-іншому ніж батьківський клас. Він не зацікавлений у інших класах. Отже MyCreator розширює типи продуктів, які створюються, і він відкладає відповідальність за створення усіх, окрім декількох продуктів, на його батьківського класу.
  3. Мовно-залежні варіанти і проблеми. Програми Smalltalk часто використовують метод, який повертає клас об'єкта, примірник якого повинен бути створеним. Метод фабрики Creator може використати це значення для створення продукту і ConcreteCreator (ПевнийСтворювач) може зберегти чи навіть обчислити це значення. Результатом являється відкладене зв'язування типу ConcreteProduct, примірник якого повинен бути створений. Версія Smalltalk прикладу Document може визначити метод documentClass у класі Application. Метод documentClass повертає відповідний клас Document для створення примірників документів. Реалізація documentClass у MyApplocation повертає клас MyDocument. Тобто у класі Application ми маємо:
    clientMethod 
       document := self documentClass new. 
    documentClass 
       self subclassResponsibility
    У класі MyApplication ми маємо
    documentClass 
       ^ MyDocument
    який повертає клас MyDocument, примірник якого повинен бути створений для Application. Навіть більш гнучкий підхід подібний до параметризованого методу фабрики полягає у збереженні класу, для якого створюється примірник, як класова змінна у Application. Цим шляхом ви не повинні створювати потомків Application для варіювання продукту. Методи фабрик у С++ завжди віртуальні функції і часто являються чисто віртуальними. Але будьте обережні у тому, щоб не викликати метод фабрики з конструктора Creator — метод фабрики у ConcreteCreator ще не буде доступним. Ви можете запобігти це, будучи обережними з доступом до продуктів виключно за допомогою операцій аксесорів, що створюють продукти за вимогою. Замість того, щоб створювати конкретний продукт у конструкторі, конструктор тільки ініціалізовує його у 0. Аксесор повертає продукт, Але спочатку він перевіряє, щоб впевнитись що продукт існує, а якщо ні — аксесор створює його. Ця техніка інколи називається лінивим ініціалізуванням (lazy initialization). Код наведений нижче, показує типову реалізацію:
    class Creator 
    { 
    
      public: 
    
          Product* GetProduct (); 
    
      protected: 
    
          virtual Product* CreateProduct (); 
    
      private: 
    
          Product* _product; 
    
    } ; 
    
    Product* Creator :: GetProduct () 
    { 
          if (_product == 0) 
          { 
                _product = CreateProduct() ; 
          } 
    
          return _product; 
    }
  4. Використання шаблонів для запобігання створення потомків. Так як ми згадували, інша потенціальна проблема у методі фабрики полягає у тому, що вони можуть змусити вас створювати нові потомки просто для створення відповідного об'єкта Product. Іншим шляхом для вирішення цієї проблеми у С++ полягає у створенні потомка-шаблона класу Creator, який параметризовується класом Product:
    class Creator 
    { 
    
      public: 
    
          virtual Product* CreateProduct() = 0; 
    
    } ;
    
    template <class TheProduct> 
    class StandardCreator: public Creator 
    { 
    
      public: 
    
          virtual Product* CreateProduct(); 
    
    } ; 
    
    template <class TheProduct> 
    Product* StandardCreator<TheProduct> :: CreateProduct () 
    { 
          return new TheProduct; 
    }
    У цьому прикладі, клієнт постачає просто клас продукту — немає необхідності у створенні потомків класу Creator.
    class MyProduct : public Product 
    { 
    
      public: 
    
          MyProduct(); 
          // ... 
    } ;
    
    StandardCreator <MyProduct> myCreator ;
  5. Найменування договорів. Хорошою практикою являється використання найменованих договорів, які роблять зрозумілим те, що ви використовуєте метод фабрики. Наприклад, програмний фреймворк Макінтош MacApp [App89] завжди оголошує абстрактну операцію, яка визначає метод фабрики як Class* DoMakeClass(), де Class являється класом Product.

Приклад коду

Функція CreateMaze Будує і повертає лабіринт. Одна проблема з цією функцією полягає у тому, що вона жорстко закодовує класи лабіринту, кімнат, дверей і стін. Ми запровадимо методи фабрик для того, щоб дозволити класам вибирати ці компоненти. Спочатку ми оголосимо методи фабрик у MazeGame для створення об'єктів лабіринту, кімнат, стін і дверей:
class MazeGame 
{ 

  public: 

      Maze* CreateMaze(); 

      // методи фабрик: 

      virtual Maze* MakeMaze() const 
      { return new Maze; } 

      virtual Room* MakeRoom(int n) const 
      { return new Room(n); } 

      virtual Wall* MakeWall() const 
      { return new Wall; } 

      virtual Door* MakeDoor(Room* r1, Room* r2) const 
      { return new Door(r1, r2); } 

} ;
Кожен метод фабрики повертає компоненти лабіринту даного типу. MazeGame забезпечує реалізацію за умовчанням, яка повертає найпростіший тип лабіринту, кімнат, стін і дверей. Тепер ми можемо перезаписати CreateMaze для використання цих методів фабрик.
Maze* MazeGame :: CreateMaze () 
{ 
      Maze* aMaze = MakeMaze(); 
      Room* r1 = MakeRoom(1); 
      Room* r2 = MakeRoom(2); 
      Door* theDoor = MakeDoor(r1, r2); 

      aMaze->AddRoom(r1); 
      aMaze->AddRoom(r2); 

      r1->SetSide(North, MakeWall()); 
      r1->SetSide(East, theDoor); 
      r1->SetSide(South, MakeWall()); 
      r1->SetSide(West, MakeWall()); 
      r2->SetSide(North, MakeWall()); 
      r2->SetSide(East, MakeWall()); 
      r2->SetSide(South, MakeWall()); 
      r2->SetSide(West, theDoor); 

      return aMaze; 
}
Різні ігри можуть створити дочірній клас MazeGame для специфікації частин лабіринту. Потомки MazeGame можуть перевизначити деякі чи усі методи фабрик для вказування варіацій у продуктах. Наприклад, BombedMazeGame може перевизначити продукти Room і Wall для повернення варіації з бомбами:
class BombedMazeGame : public MazeGame 
{ 

  public: 

      BombedMazeGame(); 

      virtual Wall* MakeWall() const 
      { return new BombedWall; } 

      virtual Room* MakeRoom(int n) const
      { return new RoomWithABomb(n); } 

} ;
Варіант EnchantedMazeGame може бути визначеним наступним чином:
class EnchantedMazeGame : public MazeGame 
{ 

  public: 

      EnchantedMazeGame(); 

      virtual Room* MakeRoom(int n) const 
      { return new EnchantedRoom(n, CastSpell()); } 

      virtual Door* MakeDoor(Room* r1, Room* r2) const 
      { return new DoorNeedingSpell(r1, r2); } 

  protected: 

      Spell* CastSpell() const; 

} ;

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

Методи фабрик пронизують фреймворки і інструментарії розробника. Попередній приклад документа являється типовим використанням у MacApp і ET++ [WGM88]. Приклад маніпулятора взятий з Unidraw. Клас View у Smalltalk-80 фреймворк Model/View/Controller (Модель/Представлення/Контролер) має метод defaultController, який створює контролер, і його можна трактувати як метод фабрики [Par90]. Але потомки View специфікують клас їхнього контолера за умовчанням за допомогою визначення defaultControllerClass, який повертає клас за допомогою якого defaultController створює примірники. Отож defaultControllerClass являється реальним методом фабрики, тобто методом, якого потомки повинні перевизначати. Іншим прикладом у Smalltalk-80 являється метод фабрики parserClass, визначений у Behavior (суперклас усіх об'єктів, які представляють класи). Це дозволяє класу використовувати сконфігурований аналізатор для його вихідного коду. Наприклад, клієнт може визначити клас SQLParser для аналізу вихідного коду класу з вбудованими виразами SQL. Клас Behavior реалізовує parserClass для повернення стандартного класу Smallalk Parser. Клас який включає вбудовані вирази SQL перезаписує цей метод (як методу класу) і повертає клас SQLParser. Система Orbix ORB від IONA Technologies [ION94] використовує шаблон Метод Фабрики для генерування типу подібного до проксі (перегляньте Проксі), коли об'єкт дає запит на посилання на віддалений об'єкт. Метод Фабрики полегшує заміну версію проксі за умовчанням на таку, яка використовує кешування на стороні клієнта, наприклад.

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

Абстрактна Фабрика часто реалізовується за допомогою методів фабрики. Приклад в секції Мотивації у шаблоні Абстрактна Фабрика також ілюструє Метод Фабрики. Методи фабрик зазвичай називають Шаблонним Методом. У попередньому прикладі документа, NewDocument являється шаблонним методом. Прототипи не вимагають створення потомків Creator'а. Однак вони часто вимагають операцію Initialize у класі Product. Creator використовує Initialize для ініціалізування об'єкту. Метод Фабрики не вимагає такої операції.