Призначення

Забезпечує інтерфейс для створення сімей зв'язаних чи залежних об'єктів без вказування їхніх конкретних класів.

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

Kit

Мотивація

Розглянемо інструментальний набір інтерфейсу користувача, який підтримує багато стандартів вигляд-і-поведінка (look-and-feel), на подобі Мотіф (Motif) і Менеджер Презентацій (Presentation Manager). Різні стандарти вигляд-і-поведінка (look-and-feel) визначають різні представлення і поведінки для “віджетів” інтерфейсу користувача на подобі смуг прокручування, вікон і кнопок. Для того щоб бути переносимим між стандартами вигляд-і-поведінка (look-and-feel), програма не повинна жорстко закодовувати її віджети в певному стандарті вигляд-і-поведінка. Створюючи усюди примірники класів віджетів специфічних для певного стандарту вигляд-і-поведінка (look-and-feel) у програмі, погіршує перенесення її на інший стандарт. fig.3.fig3_AbstractFactory_Structure_motivation Ми можемо вирішити цю проблему визначенням абстрактного класу WidgetFactory (ФабрикаВіджетів), який оголошує інтерфейс для створення кожного базового типу віджетів. Також існує абстрактний клас для кожного типу віджетів, і конкретні потомки, які реалізовують віджети для специфічного стандарту вигляд-і-поведінка. Інтерфейс класу WidgetFactory має операцію яка повертає новий об'єкт віджета для кожного абстрактного класу віджетів. Клієнти викликають ці операції для того, щоб отримати примірники віджетів, але клієнти не мають інформації про конкретні класи, які вони використовують. Так клієнти залишаються незалежними від віконної системи. Це є конкретні потомки класу WidgetFactory для кожного стандарту вигляд-і-поведінка. Кожний потомок реалізовує операції для створення відповідного віджету для вигляд-і-поведінка (look-and-feel). Наприклад, операція CreateScrollBar класу MotifWidgetFactory створює примірник і повертає смугу прокручування Мотіф, поки відповідна операція на PMWidgetFactory повертає смугу прокручування для Менеджера Презентацій (Presenation Manager). Клієнти створюють віджети виключно через інтерфейс класу WidgetFactory і не мають інформації про класи, які реалізовують віджети для певного стандарту вигляд-і-поведінка. Іншими словами, клієнти мають звертатися тільки до інтерфейсу визначеним абстрактним класом, а не до певного конкретного класу. Клас WidgetFactory також спонукає залежності між конкретними класами віджетів. Смуга прокручування Мотіф повинна використовуватися разом з кнопкою Мотіф і текстовим полем Мотіф, і це обмеження автоматично реалізовується як результат використання класу MotifWidgetFacory.

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

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

Структура

fig.3.fig3_AbstractFactory_Structure

Учасники

  • AbstractFactory (WidgetFactory)
    • оголошує інтерфейс для операцій, які створюють абстрактні об'єкти-продукти.
  • ConcreteFactory (MotifWidgetFactory, PMWidgetFactory)
    • реалізовує операції для створення конкретних об'єктів-продуктів.
  • AbstractProduct (Window, ScrollBar)
    • оголошує інтерфейс для типу об'єкту-продукта
  • ConcreteProduct (MotifWindow, MotifScrollBar)
    • визначає продукт-об'єкт, який повинен бути створеним відповідною конкретною фабрикою.
    • реалізовує інтерфейс AstractProduct.
  • Client
    • використовує тільки інтерфейс оголошений класами AbstractFactory і AbstractProduct.

Співпрацює

  • Зазвичай створюється тільки один примірник класу ConcreteFactory під час виконання. Ця конкретна фабрика створює об'єкт-продукти, які мають певну реалізацію. Для створення інших об'єктів-продуктів, клієнтам необхідно використовувати інші конкретні фабрики (ConcreteFactory).
  • AbstractFactory відкладає створення продукту-об'єкта до його дочірнього класу ConcreteFactory.

Наслідки

Шаблон Абстрактна Фабрика має наступні переваги і зобов'язання:
  • Вона ізолює конкретні класи. Шаблон Абстрактна Фабрика допомагає вам контролювати класи об'єктів, які створює програма. Через те, що фабрика інкапсулює відповідальності і процес створення об'єктів-продуктів, вона ізолює клієнтів від реалізації класів. Клієнти маніпулюють примірники через їхні абстрактні інтерфейси. Імена класів продуктів ізольовані у реалізації конкретної фабрики; вони не з'являються у клієнтському коді.
  • Шаблон полегшує заміну фамілії продуктів. Клас конкретної фабрики з'являється тільки один раз у програмі — тобто там, де створюється його примірник. Це полегшує заміну конкретної фабрики, яку використовує програма. Вона може використовувати різну конфігурацію продуктів, простою заміною конкретної фабрики. Через те, що абстрактна фабрика створює завершені фамілії продуктів, ціла сім'я продуктів змінюється тільки один раз. У нашому прикладі інтерфейсу користувача, ми можемо перейти від віджетів Мотіф до віджетів Менеджера Презентацій, простою заміною відповідного об'єкта-фабрики і обновленням інтерфейсу.
  • Шаблон підтримує сталість у продуктах. Коли продукти-об'єкти у фамілії створені працювати разом, дуже важливо, щоб програма використовувала об'єкти тільки від однієї сім'ї. Абстрактна Фабрика (Abstract Factory) полегшує підтримання такого порядку.
  • Підтримка нових типів продуктів є складною. Розширення абстрактних фабрик для забезпечення нових типів продуктів не є легким. Це через те, що інтерфейс Абстрактної Фабрики фіксує набір продуктів, що може бути створеним. Підтримка нових типів продуктів вимагає розширення інтерфейсу фабрики, що викликає зміну класу Абстрактної Фабрики і усіх його потомків. Ми обговоримо одне рішення цієї проблеми у секції реалізації.
  • Реалізація

    Ось тут декілька корисних технік для реалізовування шаблону Абстрактної Фабрики.
    1. Фабрики як синглтони. Програма зазвичай потребує тільки одного примірника класу ConcreteFactory (ПевнаФабрика) для одної сім'ї продуктів. Отож зазвичай її найкращим способом реалізації буде Синглтон.
    2. Створення продуктів. Абстрактна фабрика тільки оголошує інтерфейс для створення продуктів. Саме створення залежить від потомків ConcreteProduct (ПевнийПродукт). Найбільш поширеним шляхом реалізації цього являється визначення Методу фабрики (дивись Метод Фабрики (000)) для кожного продукту. Конкретна фабрика буде вказувати її продукти перевантажуючи метод фабрики для кожного. Ця реалізація проста, але вона вимагає нового потомка конкретної фабрики для кожної фамілії продуктів, навіть якщо фамілії продуктів злегка відрізняються. Якщо можлива велика кількість фамілій продуктів, конкретна фабрика може бути реалізованою використовуючи шаблон Прототип (Prototype — 000). Конкретна фабрика ініціалізовується за допомогою прототипного примірника кожного продукту у фамілії, і вона створює новий продукт клонуванням його прототипу. Заснований на такому підході метод виключає необхідність нових класів конкретної фабрики для нових фамілій продуктів. Ось шлях для реалізовування Прототипної фабрики у Smalltalk. Конкретна фабрика зберігає прототипи для клонування у словнику названому partCatalog. Метод make: отримує прототип і клонує його:
      make: partName
             ^ (partCatalog at: partName) copy
      Конкретна фабрика має метод для додавання частин до каталогу.
      addPart: partTemplate named: partName
          partCatalog at: partName put: partTemplate
      Прототипи додаються до фабрики ідентифікуючи їх за символом:
      aFactory addPart: aPrototype named: #ACMEWidget
      Варіація в заснованому на Прототипі підході можлива у мовах, які трактують класи як основні об'єкти (наприклад, Smalltalk i Objective С). Ви можете вважати, що клас у цих мовах являється дегенеративною фабрикою, яка створює тільки один тип продуктів. Ви можете зберегти класи всередині конкретної фабрики, що створює декілька конкретних продуктів всередині змінних, дуже подібних на прототипи. Ці класи створюють новий примірник від імені конкретної фабрики. Ви визначаєте нову фабрику ініціалізовуючи примірник конкретної фабрики за допомогою класів продуктів, замість того, щоб створювати потомки. Цей підхід використовує переваги характеристик мови, коли чистий підхід заснований на Прототипі являється мовно незалежним. На подобі щойно обговореної фабрики заснованої на Прототипі у Smalltalk, версія заснована на класах буде мати одинарний примірник змінної partCatalog, який являється словником, чиї ключі являються іменами частин. Замість того, щоб зберігати прототипи, які повинні бути клонованими, partCatalog зберігає класи продуктів. Метод make тепер виглядає наступним чином:
      make: partName
               ^ (partCatalog at: partName) new
    3. Визначення обширних фабрик. Абстрактна Фабрика зазвичай визначає різні операції для кожного типу продукту, які він може створити. Типи продуктів кодуються у сигнатурах операцій. Додаючи новий тип продукту, вимагає зміну інтерфейсу Абстрактної Фабрики і усії класів, які залежать від неї. Більш гнучкий, але менш безпечний дизайн полягає у тому, щоб додати параметр до операцій, які створюють об'єкти. Цей параметр вказує тип об'єкта, який повинен бути створеним. Це може бути ідентифікатор класу, ціле число, рядок символів (string), або що-небудь інше, що ідентифікує тип продукту. Фактично з цим підходом, Абстрактна Фабрика потребує тільки одну операцію “Make” з параметром, який ідентифікує тип об'єктів, які необхідно створити. Ця техніка використовується у абстрактних фабриках обговрених раніше, які базуються на Прототипі і класах. Цю варіацію легше використовувати у динамічно типізованих мовах на подобі Smalltalk, ніж у статично типізованих мовах на подобі С++. Ви можете використовувати її у С++ тільки коли усі об'єкти мають спільний абстрактний базовий клас, або коли об'єкти продуктів можуть бути примусово перетвореними до коректного типу клієнтом, який їх вимагає. Секція реалізації Методу Фабрики (000) показує як реалізовувати такі параметризовані операції у С++. Але навіть коли примусове перетворення не потрібне, залишається властива проблема: усі продукти повертаються клієнту з тим же абстрактним інтерфейсом, як у даного повертаючого типу. Клієнт не буде в змозі відрізнити чи зробити безпечні припущення про клас чи продукт. Якщо клієнти потребують виконати специфічні для потомка операції, вони не будуть доступними через абстрактний інтерфейс. Хоча клієнт зможе виконати перетворення (за допомогою dynamic_cast в С++), це не завжди оправдано чи безпечно, через те, що перетворення типу може провалитись. Це є класичний компроміс для дуже гнучкого і розширюваного інтерфейсу.

    Приклад коду

    Ми застосуємо шаблон Абстрактна Фабрика для створення лабіринтів, які ми обговорювали на початку цієї частини. Клас MazeFactory може створювати компоненти лабіринту (maze). Він створює кімнати, стіни і двері між кімнатами. Він може використовуватися програмою, яка читає плани лабіринтів з файлу і створює їх. Або він може використовуватися програмою, яка створює лабіринти випадково. Програми, які будують лабіринти беруть примірник класу MazeFactory як аргумент, отож програміст може вказувати класи кімнат, стін і дверей для конструювання.
    class MazeFactory 
    { 
    
     public: 
    
        MazeFactory(); 
    
        virtual Maze* MakeMaze () const 
        { return new Maze; } 
    
        virtual Wall* MakeWall () const 
        { return new Wall; } 
    
        virtual Room* MakeRoom (int n) const 
        { return new Room(n); } 
    
        virtual Door* MakeDoor (Room* r1, Room* r2) const 
        { return new Door(r1, r2); } 
    
    } ;
    Зверніть увагу на те, що метод CreateMaze створює малий лабіринт, який складається з двох кімнат з дверима між ними. Метод CreateMaze жорстко закодовує імена класів, що робить важчим створення лабіринтів з іншими компонентами. Ось версія методу CreateMaze, яка виправляє цей дефект, беручи MazeFactory як параметр:
    Maze* MazeGame :: CreateMaze (MazeFactory& factory) 
    { 
        Maze* aMaze = factory.MakeMaze(); 
        Room* r1 = factory.MakeRoom(1); 
        Room* r2 = factory.MakeRoom(2); 
        Door* aDoor = factory.MakeDoor(r1, r2); 
    
        aMaze->AddRoom(r1); 
        aMaze->AddRoom(r2); 
    
        r1->SetSide(North, factory.MakeWall()); 
        r1->SetSide(East, aDoor); 
        r1->SetSide(South, factory.MakeWall()); 
        r1->SetSide(West, factory.MakeWall()); 
        r2->SetSide(North, factory.MakeWall()); 
        r2->SetSide(East, factory.MakeWall()); 
        r2->SetSide(South, factory.MakeWall()); 
        r2->SetSide(West, aDoor); 
    
        return aMaze ; 
    }
    Ми можемо створити EnchantedMazeFactory, фабрика для зачарованих лабіринтів, створюючи потомки класу MazeFactory. EnchantedMazeFactory перезапише різні члени функцій і поверне різні потомки Room (Кімнати), Wall (Стіни) і т.д.
    class EnchantedMazeFactory : public MazeFactory 
    { 
    
     public: 
    
         EnchantedMazeFactory(); 
    
         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; 
    
    } ;
    Тепер припустимо, що ми бажаємо створити лабіринтну гру у якій кімната може мати встановлену бомбу всередині. Якщо бомба детонує, вона пошкодить стіни (принаймні). Ми можемо створити потомка класу Кімната (Room) для відслідковування чи кінмата містить бомбу всередині і чи бомба здетонувала. Нам також необхідний потомок класу Wall (Стіна) для відслідковування пошкоджень заподіяних стіні. Ми назвемо ці класи RoomWithABomb і BombedWall. Останній клас, який ми визначимо називатиметься BombedMazeFactory — потомок класу MazeFactory, який запевняє, що стіни являються примірниками класу BombedWall і кімнати являються примірниками RoomWithABomb. BombedMazeFactory потребує тільки перевизначення двох функцій:
    Wall* BombedMazeFactory :: MakeWall () const 
    { 
        return new BombedWall; 
    } 
    
    Room* BombedMazeFactory :: MakeRoom (int n) const 
    { 
        return new RoomWithABomb(n); 
    }
    Для побудови простого лабіринту, який може містити бомби, ми просто викликаємо CreateMaze з примірником класу BombedMazeFactory.
    MazeGame game; 
    BombedMazeFactory factory;
    
    game.CreateMaze (factory) ;
    CreateMaze може так само брати примірник класу EnchantedMazeFacory для побудови зачарованих (enchanted) лабіринтів. Зверніть увагу на те, що MazeFactory являється просто колекцією методів фабрики. Це є найбільш загальний шлях для реалізації шаблону Абстрактна Фабрика. Також зауважте, що MazeFactory не є абстрактним класом; тобто він поводить себе як AbstractFactory і ConcreteFactory. Це є іншою поширенню реалізацією для простих програм з шаблоном Абстрактна Фабрика. Через те, що MazeFactory являється конкретним класом, який повністю складається з методів фабрики, легко створити новий клас MazeFactory, створюючи потомок і перевизначаючи операції, які потребують зміни. CreateMaze використовує операцію SetSide на кімнатах для вказування їхніх сторін. Якщо він створює кімнати за допомогою BombedMazeFactory, тоді лабіринт буде зроблений з об'єктів RoomWithABomb і сторонами BombedWall. Якщо RoomWithABomb повинна мати доступ до операцій специфічних для потомків класу BombedWall, тоді вона повинна перетворити посилання до своїх стін від Wall* до BombedWall*. Це примусове перетворення типів є безпечним доти, доки аргумент являється фактично примірником класу BombedWall, що є гарантовано правдою, якщо стіни повністю побудовані за допомогою BombedMazeFacory. Динамічно типізовані мови на подобі Smalltalk, звичайно, не вимагають перетворення типів, але вони можуть генерувати помилка під час виконання, якщо вони зустрінуть клас Wall там, де вони очікують потомок класу Wall. Використовуючи Абстрактну Фабрику для побудови стін допомагає запобігти цих помилок часу виконання запевняючи, що тільки певні типи стін можуть бути створеними. Давайте розглянемо версію для Smalltalk класу MazeFactory, один з однією операцією make, яка бере тип об'єкта для створення як параметр. Більше того, конкретна фабрика зберігає класи продуктів, які вона створює. Для початку ми напишемо еквівалент методу CreateMaze у Smalltalk:
    createMaze: aFactory 
       | room1 room2 aDoor | 
       room1 := (aFactory make: #room) number: 1. 
       room2 := (aFactory make: #room) number: 2. 
       aDoor := (aFactory make: #door) from: room1 to: room2. 
       room1 atSide: #north put: (aFactory make: #wall). 
       room1 atSide: #east put: aDoor. 
       room1 atSide: #south put: (aFactory make: #wall).
       room1 atSide: #west put: (aFactory make: #wall). 
       room2 atSide: #north put: (aFactory make: #wall). 
       room2 atSide: #east put: (aFactory make: #wall). 
       room2 atSide: #south put: (aFactory make: #wall). 
       room2 atSide: #west put: aDoor. 
       ^ Maze new addRoom: room1; addRoom: room2; yourself
    Так як ми обговорили у секції Реалізація, MazeFactory потребує тільки одну змінну-примірник partCaalog для забезпечення словника, чий ключ являється класом компоненту. Також пригадайте як ми реалізовували метод make:
    make: partName 
        ^ (partCatalog at: partName) new
    Тепер ми можемо створити MazeFacory і використати для реалізації createMaze. Ми створимо фабрику використовуючи метод createMazeFactory класу MazeGame.
    createMazeFactory 
        ^ (MazeFactory new 
        addPart: Wall named: #wall; 
        addPart: Room named: #room; 
        addPart: Door named: #door; 
        yourself)
    BombedMazeFactory чи EchanredMazeFactory створюється асоціюванням різник класів з ключами. Наприклад, примірник класу EnchantedMazeFactory може бути створеним наступним чином:
    createMazeFactory 
        ^ (MazeFactory new 
        addPart: Wall named: #wall; 
        addPart: EnchantedRoom named: #room; 
        addPart: DoorNeedingSpell named: #door; 
        yourself)

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

    InterViews використовує суфікс “Kit” [Lin92] для позначення класів Абстрактної Фабрики. Вона визначає абстрактні фабрики WidgetKit і DialogKit для генерування об'єктів інтерфейсу користувача, специфічних для певних стандартів вигляд-і-поведінка (look-and-feel). InterViews також включає LayoutKit, що генерує різні композиційні об'єкти в залежності від бажаного плану. Наприклад, планування яке концептуально горизонтальне може вимагати різні об'єкти композиційні в залежності від орієнтації документу (книжна чи альбомна). ET++ [WGM88] використовує шаблон Абстрактна Фабрика щоб досягти переносимість між різними віконними системами (X Window i SunView, наприклад). Абстрактний базовий клас WindowSystem визначає інтерфейс для створення об'єктів, які представляють ресурси віконної системи (MakeWindow, MakeFont, MakeColor, наприклад). Конкретні потомки реалізовують інтерфейси для певних віконних систем. Під час виконання, ET++ створює примірник конкретного потомка класу WindowSystem, який створює конкретні системні об'єкти ресурсів.

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

    Класи шаблону Абстрактна Фабрика часто реалізовують за допомогою методів фабрик, але вони можуть також реалізовуватись використовуючи Прототип. Конкретна фабрика часто являється Синглтоном.