Autumn SALE

Фабричний метод

Також відомий як: Віртуальний конструктор, Factory Method

Суть патерна

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

Патерн Фабричний метод

Проблема

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

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

Проблема з додаванням нового класу до програми

Додати новий клас не так просто, якщо весь код вже залежить від конкретних класів.

Чудові новини, чи не так?! Але як щодо коду? Велика частина існуючого коду жорстко прив’язана до класів Вантажівок. Щоб додати до програми класи морських Суден, знадобиться перелопачувати весь код. Якщо ж ви вирішите додати до програми ще один вид транспорту, тоді всю цю роботу доведеться повторити.

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

Рішення

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

Структура класів-творців

Підкласи можуть змінювати клас створюваних об’єктів.

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

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

Структура ієрархії продуктів

Всі об’єкти-продукти повинні мати спільний інтерфейс.

Наприклад, класи Вантажівка і Судно реалізують інтерфейс Транспорт з методом доставити. Кожен з цих класів реалізує метод по-своєму: вантажівки перевозять вантажі сушею, а судна — морем. Фабричний метод класу ДорожноїЛогістики поверне об’єкт-вантажівку, а класу МорськоїЛогістики — об’єкт-судно.

Програма після застосування фабричного методу

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

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

Структура

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

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

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

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

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

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

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

Псевдокод

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

Структура класів прикладу патерна Фабричного методу

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

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

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

Такий підхід можна застосувати і для створення інших елементів інтерфейсу. Хоча кожен новий тип елементів наближатиме вас до Абстрактної фабрики.

// Патерн Фабричний метод має сенс лише тоді, коли в програмі є
// ієрархія класів продуктів.
interface Button is
    method render()
    method onClick(f)

class WindowsButton implements Button is
    method render(a, b) is
        // Відобразити кнопку в стилі Windows.
    method onClick(f) is
        // Навісити на кнопку обробник подій Windows.

class HTMLButton implements Button is
    method render(a, b) is
        // Повернути HTML-код кнопки.
    method onClick(f) is
        // Навісити на кнопку обробник події браузера.


// Базовий клас фабрики. Зауважте, що "фабрика" — це всього лише
// додаткова роль для цього класу. Скоріше за все, він вже має
// якусь бізнес-логіку, яка потребує створення продуктів.
class Dialog is
    method render() is
        // Щоб використати фабричний метод, ви маєте
        // пересвідчитися, що ця бізнес-логіка не залежить від
        // конкретних класів продуктів. Button — це загальний
        // інтейрфейс кнопок, тому все гаразд.
        Button okButton = createButton()
        okButton.onClick(closeDialog)
        okButton.render()

    // Ми виносимо весь код створення продуктів до особливого
    // методу, який називають "фабричним".
    abstract method createButton():Button


// Конкретні фабрики перевизначають фабричний метод і повертають
// з нього власні продукти.
class WindowsDialog extends Dialog is
    method createButton():Button is
        return new WindowsButton()

class WebDialog extends Dialog is
    method createButton():Button is
        return new HTMLButton()


class Application is
    field dialog: Dialog

    // Програма створює певну фабрику в залежності від
    // конфігурації або оточення.
    method initialize() is
        config = readApplicationConfigFile()

        if (config.OS == "Windows") then
            dialog = new WindowsDialog()
        else if (config.OS == "Web") then
            dialog = new WebDialog()
        else
            throw new Exception("Error! Unknown operating system.")

    // Якщо весь інший клієнтський код працює з фабриками та
    // продуктами тільки через загальний інтерфейс, то для нього
    // байдуже, якого типу фабрику було створено на початку.
    method main() is
        this.initialize()
        dialog.render()

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

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

Фабричний метод відокремлює код виробництва продуктів від решти коду, який використовує ці продукти.

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

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

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

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

Наприклад, ви використовуєте готовий UI-фреймворк для свого додатку. Але — от халепа — вам необхідно мати круглі кнопки, а не стандартні прямокутні. Ви створюєте клас RoundButton. Але як сказати головному класу фреймворку UIFramework, щоб він почав тепер створювати круглі кнопки замість стандартних прямокутних?

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

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

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

Уявіть, скільки дій вам потрібно зробити, аби повторно використовувати вже існуючі об’єкти:

  1. Спочатку слід створити загальне сховище, щоб зберігати в ньому всі створювані об’єкти.
  2. При запиті нового об’єкта потрібно буде подивитись у сховище та перевірити, чи є там невикористаний об’єкт.
  3. Потім повернути його клієнтському коду.
  4. Але якщо ж вільних об’єктів немає, створити новий, не забувши додати його до сховища.

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

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

Отже, має бути інший метод, який би віддавав як існуючі, так і нові об’єкти. Ним і стане фабричний метод.

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

  1. Приведіть усі створювані продукти до загального інтерфейсу.

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

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

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

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

  4. Для кожного типу продуктів заведіть підклас і перевизначте в ньому фабричний метод. З суперкласу перемістіть туди код створення відповідного продукту.

  5. Якщо створюваних продуктів занадто багато для існуючих підкласів творця, ви можете подумати про введення параметрів до фабричного методу, аби повертати різні продукти в межах одного підкласу.

    Наприклад, у вас є клас Пошта з підкласами АвіаПошта і НаземнаПошта, а також класи продуктів Літак, Вантажівка й Потяг. Авіа відповідає Літакам, але для НаземноїПошти є відразу два продукти. Ви могли б створити новий підклас пошти й для потягів, але проблему можна вирішити по-іншому. Клієнтський код може передавати до фабричного методу НаземноїПошти аргумент, що контролює, який з продуктів буде створено.

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

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

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

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

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

Фабричний метод на C# Фабричний метод на C++ Фабричний метод на Go Фабричний метод на Java Фабричний метод на PHP Фабричний метод на Python Фабричний метод на Ruby Фабричний метод на Rust Фабричний метод на Swift Фабричний метод на TypeScript

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

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