Шаблони проєктування: фабричний метод

Шаблони проєктування: фабричний метод
Переклад 18 хв. читання
07 вересня 2023

Призначення

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

🙁 Проблема

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

Через деякий час ваша програма стає досить популярною. Щодня ви отримуєте десятки запитів від морських транспортних компаній з проханням додати в програму морську логістику.

Шаблони проєктування: фабричний метод Додати новий клас до програми не так просто, якщо решта коду вже з'єднана з вже існуючими класами.

Чудова новина, правда? Але як щодо коду? Наразі більша частина вашого коду пов'язана з класом Truck. Додавання Ships до програми вимагатиме внесення змін до всієї кодової бази. Ба більше, якщо згодом ви вирішите додати інший тип транспорту, вам, ймовірно, доведеться вносити всі ці зміни знову.

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

😊 Рішення

Паттерн "Фабричний метод" пропонує замінити прямі виклики створення об'єктів (за допомогою оператора new) на виклики спеціального фабричного методу. Не хвилюйтеся: об'єкти все одно створюються за допомогою оператора new, але він викликається з фабричного методу. Об'єкти, що повертаються фабричним методом, часто називають продуктами.

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

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

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

Шаблони проєктування: фабричний метод Всі продукти повинні мати єдиний інтерфейс.

Наприклад, обидва класи Truck і Ship повинні реалізувати інтерфейс Transport, в якому оголошується метод deliver. Кожен клас реалізує цей метод по-різному: вантажівки доставляють вантаж наземним транспортом, а кораблі - морським. Фабричний метод класу RoadLogistics повертає об'єкти вантажівок, тоді як фабричний метод класу SeaLogistics повертає об'єкти кораблів.

Шаблони проєктування: фабричний метод Оскільки всі класи продуктів реалізують спільний інтерфейс, ви можете передавати їхні об'єкти клієнтському коду, не ламаючи його.

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

Структура

Шаблони проєктування: фабричний метод

  1. Product декларує інтерфейс, який є спільним для всіх об'єктів, що можуть бути створені створювачем та його підкласами.
  2. Concrete Products - це різні реалізації інтерфейсу продукту.
  3. У класі Creator оголошується фабричний метод, який повертає нові об'єкти продукту. Важливо, щоб тип повернення цього методу відповідав інтерфейсу продукту.

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

    Зауважте, що, незважаючи на назву, створення продукту не є основним обов'язком креатора. Зазвичай, клас Creator вже має деяку основну бізнес-логіку, пов'язану з продуктами. Метод factory допомагає відокремити цю логіку від конкретних класів продуктів. Наведу аналогію: велика компанія з розробки програмного забезпечення може мати відділ навчання програмістів. Однак основною функцією компанії в цілому все одно залишається написання коду, а не виробництво програмістів.
  4. Concrete Creators перевизначають базовий фабричний метод, щоб він повертав інший тип продукту.

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

Псевдокод

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

Шаблони проєктування: фабричний метод Приклад кросплатформного діалогу

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

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

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

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

// У класі creator оголошується фабричний метод, який повинен
// повернути об'єкт класу-продукту. Підкласи креатора
// зазвичай забезпечують реалізацію цього методу.
class Dialog is
    // Розробник може також надати деяку реалізацію фабричного методу 
    // за замовчуванням.
    abstract method createButton():Button

    // Зверніть увагу, що, незважаючи на назву, 
    // основним обов'язком творця не є створення продуктів. 
    // Зазвичай він містить деяку основну бізнес-логіку, яка покладається на об'єкти продукту, 
    // що повертаються фабричним методом. 
    // Підкласи можуть опосередковано змінювати цю бізнес-логіку, 
    // перевизначаючи фабричний метод і повертаючи з нього інший тип продукту.
    method render() is
        // Виклик фабричного методу для створення об'єкта продукту.
        Button okButton = createButton()
        // Тепер використовуйте продукт.
        okButton.onClick(closeDialog)
        okButton.render()


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

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


// Інтерфейс продукту оголошує операції, які повинні реалізовувати всі конкретні продукти.
interface Button is
    method render()
    method onClick(f)

// Конкретні продукти надають різні реалізації інтерфейсу  продукту.
class WindowsButton implements Button is
    method render(a, b) is
    // Відрендерити кнопку у стилі Windows.
    method onClick(f) is
    // Прив'язати власну подію кліку ОС.

class HTMLButton implements Button is
    method render(a, b) is
    // Повертає представлення кнопки у форматі HTML.
    method onClick(f) is
    // Прив'язати подію кліку в браузері.


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()

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

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

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

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


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

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

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

Розглянемо, як це буде працювати. Уявіть, що ви пишете програму, використовуючи фреймворк з відкритим вихідним кодом. Ваша програма повинна мати круглі кнопки, але фреймворк надає лише квадратні. Ви розширюєте стандартний клас Button чудовим підкласом RoundButton. Але тепер вам потрібно вказати головному класу UIFramework використовувати новий підклас кнопки замість стандартного.

Для цього ви створюєте підклас UIWithRoundButton з базового класу фреймворку і перевизначаєте його метод createButton. У той час як цей метод повертає об'єкти Button у базовому класі, ви робите так, щоб ваш підклас повертав об'єкти RoundButton. Тепер використовуйте клас UIWithRoundButton замість UIFramework. І це все!


🐞 Використовуйте фабричний метод, якщо ви хочете заощадити системні ресурси, повторно використовуючи існуючі об'єкти замість того, щоб створювати їх щоразу заново.

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

Поміркуймо, що потрібно зробити, щоб повторно використовувати наявний об'єкт:

По-перше, вам потрібно створити деяке сховище для відстеження всіх створених об'єктів. Коли хтось запитує об'єкт, програма повинна шукати вільний об'єкт у цьому сховищі. ... а потім повертати його клієнтському коду. Якщо вільних об'єктів немає, програма повинна створити новий об'єкт (і додати його до пулу). Це дуже багато коду! І його треба зібрати в одне місце, щоб не забруднювати програму дублікатами коду.

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

Тому вам потрібно мати звичайний метод, здатний створювати нові об'єкти, а також повторно використовувати наявні. Це дуже схоже на фабричний метод.

📋 Як це зробити

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

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

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

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

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

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

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

    Наприклад, уявіть, що у вас є така ієрархія класів: базовий клас Mail з кількома підкласами: AirMail та GroundMail; транспортні класи Plane, Truck та Train. У той час як клас AirMail використовує тільки об'єкти Plane, GroundMail може працювати з об'єктами Truck і Train. Ви можете створити новий підклас (скажімо, TrainMail) для роботи в обох випадках, але є й інший варіант. Клієнтський код може передати аргумент фабричному методу класу GroundMail, щоб вказати, який продукт він хоче отримати.

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

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

✅ Ви уникаєте тісного зв'язку між креатором і конкретними продуктами.
✅ Принцип єдиної відповідальності. Ви можете перемістити код створення продукту в одне місце в програмі, що полегшує підтримку коду.
✅ Принцип відкритості/закритості. Ви можете вводити в програму нові типи продуктів, не порушуючи існуючого клієнтського коду.

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

🔄 Взаємозв'язки з іншими патернами

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

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

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

Prototype не базується на успадкуванні, тому не має його недоліків. З іншого боку, Prototype вимагає складної ініціалізації клонованого об'єкта. Factory Method заснований на успадкуванні, але не вимагає етапу ініціалізації.

Фабричний метод є спеціалізацією Шаблонного методу. Водночас фабричний метод може служити кроком у великому шаблонному методі.

Приклад реалізації мовою Python

from __future__ import annotations
from abc import ABC, abstractmethod

class Creator(ABC):
    """
    У класі Creator оголошується фабричний метод, 
    який повинен повертати об'єкт класу Product. 
    Підкласи Creator зазвичай забезпечують реалізацію цього методу
    """

    @abstractmethod
    def factory_method(self):
        """
        Зауважте, що Creator також може надати деяку реалізацію заводського методу за замовчуванням
        """
        pass

    def some_operation(self) -> str:
        """
        Також зауважте, що, незважаючи на назву, першочерговим обов'язком Creator
        не є створенням продуктів. Зазвичай, він містить деяку основну бізнес-логіку 
        яка покладається на об'єкти Product, що повертаються фабричним методом. 
        Підкласи можуть опосередковано змінювати цю бізнес-логіку, 
        перевизначаючи  заводський метод і повертаючи з нього інший тип продукту.
        """

        # Виклик фабричного методу для створення об'єкту Product.
        product = self.factory_method()

        # Тепер ви можете використовувати продукт.
        result = f"Creator: The same creator's code has just worked with {product.operation()}"

        return result

"""
Concrete Creators перевизначають фабричний метод, щоб змінити кінцевий тип продукту.
"""

class ConcreteCreator1(Creator):
    """
    Зверніть увагу, що у сигнатурі методу все ще використовується абстрактний тип продукту, 
    хоча насправді з методу повертається конкретний продукт. 
    Таким чином таким чином Creator може залишатися незалежним від конкретних класів продуктів.
    """

    def factory_method(self) -> Product:
        return ConcreteProduct1()

class ConcreteCreator2(Creator):
    def factory_method(self) -> Product:
        return ConcreteProduct2()

class Product(ABC):
    """
    Інтерфейс продукту декларує операції, 
    які  всі конкретні продукти повинні реалізовувати.
    """

    @abstractmethod
    def operation(self) -> str:
        pass

"""
Concrete Products надавати різні реалізації інтерфейсу Product.
"""

class ConcreteProduct1(Product):
    def operation(self) -> str:
        return "{Result of the ConcreteProduct1}"


class ConcreteProduct2(Product):
    def operation(self) -> str:
        return "{Result of the ConcreteProduct2}"

def client_code(creator: Creator) -> None:
    """
    Клієнтський код працює з екземпляром Сoncrete Сreator, 
    хоча і через базовий інтерфейс. 
    Поки клієнт працює з Сreator через базовий інтерфейс,
    ви можете передати йому будь-який підклас творця.
    """

    print(f"Client: I'm not aware of the creator's class, but it still works.\n"
          f"{creator.some_operation()}", end="")

if __name__ == "__main__":
    print("App: Launched with the ConcreteCreator1.")
    client_code(ConcreteCreator1())
    print("\n")

    print("App: Launched with the ConcreteCreator2.")
    client_code(ConcreteCreator2())

Результат:

App: Launched with the ConcreteCreator1.
Client: I'm not aware of the creator's class, but it still works.
Creator: The same creator's code has just worked with {Result of the ConcreteProduct1}

App: Launched with the ConcreteCreator2.
Client: I'm not aware of the creator's class, but it still works.
Creator: The same creator's code has just worked with {Result of the ConcreteProduct2}
Джерело: Factory Method
Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Коментарі (1)
  1. leofun01
    🙁⚡⚖️✅❌

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

    7 місяців тому ·
    0
Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація