Весняний РОЗПРОДАЖ

Абстрактна фабрика

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

Суть патерна

Абстрактна фабрика — це породжувальний патерн проектування, що дає змогу створювати сімейства пов’язаних об’єктів, не прив’язуючись до конкретних класів створюваних об’єктів.

Патерн Абстрактна фабрика

Проблема

Уявіть, що ви пишете симулятор меблевого магазину. Ваш код містить:

  1. Сімейство залежних продуктів. Скажімо, Крісло + Диван + Столик.

  2. Кілька варіацій цього сімейства. Наприклад, продукти Крісло, Диван та Столик представлені в трьох різних стилях: Ар-деко, Вікторіанському і Модерн.

Таблиця відповідності сімейства продуктів до їхніх варіацій

Сімейства продуктів та їхніх варіацій.

Вам потрібно створювати об’єкти продуктів у такий спосіб, щоб вони завжди пасували до інших продуктів того самого сімейства. Це дуже важливо, адже клієнти засмучуються, коли отримують меблі, що не можна поєднати між собою.

Клієнти засмучуються, якщо отримують продукти, що не поєднуються.

Крім того, ви не хочете вносити зміни в існуючий код під час додавання в програму нових продуктів або сімейств. Постачальники часто оновлюють свої каталоги, але ви б не хотіли змінювати вже написаний код кожен раз при надходженні нових моделей меблів.

Рішення

Для початку, патерн Абстрактна фабрика пропонує виділити загальні інтерфейси для окремих продуктів, що складають одне сімейство, і описати в них спільну для цих продуктів поведінку. Так, наприклад, усі варіації крісел отримають спільний інтерфейс Крісло, усі дивани реалізують інтерфейс Диван тощо.

Схема ієрархії класів крісел.

Всі варіації одного й того самого об’єкта мають жити в одній ієрархії класів.

Далі ви створюєте абстрактну фабрику — загальний інтерфейс, який містить методи створення всіх продуктів сімейства (наприклад, створитиКрісло, створитиДиван і створитиСтолик). Ці операції повинні повертати абстрактні типи продуктів, представлені інтерфейсами, які ми виділили раніше — Крісла, Дивани і Столики.

Схема ієрархії класів фабрик.

Конкретні фабрики відповідають певній варіації сімейства продуктів.

Як щодо варіацій продуктів? Для кожної варіації сімейства продуктів ми повинні створити свою власну фабрику, реалізувавши абстрактний інтерфейс. Фабрики створюють продукти однієї варіації. Наприклад, ФабрикаМодерн буде повертати тільки КріслаМодерн,ДиваниМодерн і СтоликиМодерн.

Клієнтський код повинен працювати як із фабриками, так і з продуктами тільки через їхні загальні інтерфейси. Це дозволить подавати у ваші класи будь-які типи фабрик і виробляти будь-які типи продуктів, без необхідності вносити зміни в існуючий код.

Для клієнтського коду повинно бути не важливо, з якою фабрикою працювати.

Наприклад, клієнтський код просить фабрику зробити стілець. Він не знає, якому типу відповідає ця фабрика. Він не знає, отримає вікторіанський або модерновий стілець. Для нього важливо, щоб на цьому стільці можна було сидіти та щоб цей стілець відмінно виглядав поруч із диваном тієї ж фабрики.

Залишилося прояснити останній момент: хто ж створює об’єкти конкретних фабрик, якщо клієнтський код працює лише із загальними інтерфейсами? Зазвичай програма створює конкретний об’єкт фабрики під час запуску, причому тип фабрики вибирається на підставі параметрів оточення або конфігурації.

Структура

Структура класів патерна Абстрактна фабрикаСтруктура класів патерна Абстрактна фабрика
  1. Абстрактні продукти оголошують інтерфейси продуктів, що пов’язані один з одним за змістом, але виконують різні функції.

  2. Конкретні продукти — великий набір класів, що належать до різних абстрактних продуктів (крісло/столик), але мають одні й ті самі варіації (Вікторіанський/Модерн).

  3. Абстрактна фабрика оголошує методи створення різних абстрактних продуктів (крісло/столик).

  4. Конкретні фабрики кожна належить до своєї варіації продуктів (Вікторіанський/Модерн) і реалізує методи абстрактної фабрики, даючи змогу створювати всі продукти певної варіації.

  5. Незважаючи на те, що конкретні фабрики породжують конкретні продукти, сигнатури їхніх методів мусять повертати відповідні абстрактні продукти. Це дозволить клієнтського коду, що використовує фабрику, не прив’язуватися до конкретних класів продуктів. Клієнт зможе працювати з будь-якими варіаціями продуктів через абстрактні інтерфейси.

Псевдокод

У цьому прикладі Абстрактна фабрика створює крос-платформові елементи інтерфейсу і стежить за тим, щоб вони відповідали обраній операційній системі.

Структура класів прикладу патерна Абстрактної фабрики

Приклад крос-платформового графічного інтерфейсу користувача.

Крос-платформова програма може відображати одні й ті самі елементи інтерфейсу по-різному, в залежності від обраної операційної системи. Важливо, щоб у такій програмі всі створювані елементи завжди відповідали поточній операційній системі. Ви ж не хотіли б, аби програма, запущена на Windows, раптом почала показувати чек-бокси в стилі macOS?

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

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

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

Таким чином, щоб додати до програми нову варіацію елементів інтерфейсу (наприклад, для підтримки Linux), вам не потрібно змінювати клієнтський код. Достатньо створити ще одну фабрику, що виготовляє ці елементи.

// Цей патерн передбачає, що ви маєте кілька сімейств продуктів,
// які знаходяться в окремих ієрархіях класів (Button/Checkbox).
// Продукти одного сімейства повинні мати спільний інтерфейс.
interface Button is
    method paint()

// Cімейства продуктів мають однакові варіації (macOS/Windows).
class WinButton implements Button is
    method paint() is
        // Відобразити кнопку в стилі Windows.

class MacButton implements Button is
    method paint() is
        // Відобразити кнопку в стилі macOS.


interface Checkbox is
    method paint()

class WinCheckbox implements Checkbox is
    method paint() is
        // Відобразити чекбокс в стилі Windows.

class MacCheckbox implements Checkbox is
    method paint() is
        // Відобразити чекбокс в стилі macOS.


// Абстрактна фабрика знає про всі абстрактні типи продуктів.
interface GUIFactory is
    method createButton():Button
    method createCheckbox():Checkbox


// Кожна конкретна фабрика знає лише про продукти своєї варіації
// і створює лише їх.
class WinFactory implements GUIFactory is
    method createButton():Button is
        return new WinButton()
    method createCheckbox():Checkbox is
        return new WinCheckbox()

// Незважаючи на те, що фабрики оперують конкретними класами,
// їхні методи повертають абстрактні типи продуктів. Завдяки
// цьому фабрики можна заміняти одну на іншу, не змінюючи
// клієнтського коду.
class MacFactory implements GUIFactory is
    method createButton():Button is
        return new MacButton()
    method createCheckbox():Checkbox is
        return new MacCheckbox()


// Для коду, який використовує фабрику, не важливо, з якою
// конкретно фабрикою він працює. Всі отримувачі продуктів
// працюють з ними через загальні інтерфейси.
class Application is
    private field factory: GUIFactory
    private field button: Button
    constructor Application(factory: GUIFactory) is
        this.factory = factory
    method createUI()
        this.button = factory.createButton()
    method paint()
        button.paint()


// Програма вибирає тип конкретної фабрики й створює її
// динамічно, виходячи з конфігурації або оточення.
class ApplicationConfigurator is
    method main() is
        config = readApplicationConfigFile()

        if (config.OS == "Windows") then
            factory = new WinFactory()
        else if (config.OS == "Mac") then
            factory = new MacFactory()
        else
            throw new Exception("Error! Unknown operating system.")

        Application app = new Application(factory)

Застосування

Коли бізнес-логіка програми повинна працювати з різними видами пов’язаних один з одним продуктів, незалежно від конкретних класів продуктів.

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

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

У будь-якій добротній програмі кожен клас має відповідати лише за одну річ. Якщо клас має занадто багато фабричних методів, вони здатні затуманити його основну функцію. Тому є сенс у тому, щоб винести усю логіку створення продуктів в окрему ієрархію класів, застосувавши абстрактну фабрику.

Кроки реалізації

  1. Створіть таблицю співвідношень типів продуктів до варіацій сімейств продуктів.

  2. Зведіть усі варіації продуктів до загальних інтерфейсів.

  3. Визначте інтерфейс абстрактної фабрики. Він повинен мати фабричні методи для створення кожного типу продуктів.

  4. Створіть класи конкретних фабрик, реалізувавши інтерфейс абстрактної фабрики. Цих класів має бути стільки ж, скільки й варіацій сімейств продуктів.

  5. Змініть код ініціалізації програми так, щоб вона створювала певну фабрику й передавала її до клієнтського коду.

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

Переваги та недоліки

  • Гарантує поєднання створюваних продуктів.
  • Звільняє клієнтський код від прив’язки до конкретних класів продукту.
  • Виділяє код виробництва продуктів в одне місце, спрощуючи підтримку коду.
  • Спрощує додавання нових продуктів до програми.
  • Реалізує принцип відкритості/закритості.
  • Ускладнює код програми внаслідок введення великої кількості додаткових класів.
  • Вимагає наявності всіх типів продукту в кожній варіації.

Відносини з іншими патернами

Приклади реалізації патерна

Абстрактна фабрика на C# Абстрактна фабрика на C++ Абстрактна фабрика на Go Абстрактна фабрика на Java Абстрактна фабрика на PHP Абстрактна фабрика на Python Абстрактна фабрика на Ruby Абстрактна фабрика на Rust Абстрактна фабрика на Swift Абстрактна фабрика на TypeScript

Додаткові матеріали

  • Якщо ви вже чули про Фабрику, Фабричний метод чи Абстрактну фабрику, але вам все одно важко їх розрізняти, прочитайте нашу статтю Порівняння фабрик.