Фабричний метод
Суть патерна
Фабричний метод — це породжувальний патерн проектування, який визначає загальний інтерфейс для створення об’єктів у суперкласі, дозволяючи підкласам змінювати тип створюваних об’єктів.
Проблема
Уявіть, що ви створюєте програму керування вантажними перевезеннями. Спочатку ви плануєте перевезення товарів тільки вантажними автомобілями. Тому весь ваш код працює з об’єктами класу Вантажівка
.
Згодом ваша програма стає настільки відомою, що морські перевізники шикуються в чергу і благають додати до програми підтримку морської логістики.
Чудові новини, чи не так?! Але як щодо коду? Велика частина існуючого коду жорстко прив’язана до класів Вантажівок
. Щоб додати до програми класи морських Суден
, знадобиться перелопачувати весь код. Якщо ж ви вирішите додати до програми ще один вид транспорту, тоді всю цю роботу доведеться повторити.
У підсумку ви отримаєте жахливий код, переповнений умовними операторами, що виконують ту чи іншу дію в залежності від вибраного класу транспорту.
Рішення
Патерн Фабричний метод пропонує відмовитись від безпосереднього створення об’єктів за допомогою оператора new
, замінивши його викликом особливого фабричного методу. Не лякайтеся, об’єкти все одно будуть створюватися за допомогою new
, але робити це буде фабричний метод.
На перший погляд це може здатись безглуздим — ми просто перемістили виклик конструктора з одного кінця програми в інший. Проте тепер ви зможете перевизначити фабричний метод у підкласі, щоб змінити тип створюваного продукту.
Щоб ця система запрацювала, всі об’єкти, що повертаються, повинні мати спільний інтерфейс. Підкласи зможуть виготовляти об’єкти різних класів, що відповідають одному і тому самому інтерфейсу.
Наприклад, класи Вантажівка
і Судно
реалізують інтерфейс Транспорт
з методом доставити
. Кожен з цих класів реалізує метод по-своєму: вантажівки перевозять вантажі сушею, а судна — морем. Фабричний метод класу ДорожноїЛогістики
поверне об’єкт-вантажівку, а класу МорськоїЛогістики
— об’єкт-судно.
Клієнт фабричного методу не відчує різниці між цими об’єктами, адже він трактуватиме їх як якийсь абстрактний Транспорт
. Для нього буде важливим, щоб об’єкт мав метод доставити
, а не те, як конкретно він працює.
Структура
-
Продукт визначає загальний інтерфейс об’єктів, які може створювати творець та його підкласи.
-
Конкретні продукти містять код різних продуктів. Продукти відрізнятимуться реалізацією, але інтерфейс у них буде спільним.
-
Творець оголошує фабричний метод, який має повертати нові об’єкти продуктів. Важливо, щоб тип результату цього методу співпадав із загальним інтерфейсом продуктів.
Зазвичай, фабричний метод оголошують абстрактним, щоб змусити всі підкласи реалізувати його по-своєму. Однак він може також повертати продукт за замовчуванням.
Незважаючи на назву, важливо розуміти, що створення продуктів не є єдиною і головною функцією творця. Зазвичай він містить ще й інший корисний код для роботи з продуктом. Аналогія: у великій софтверній компанії може бути центр підготовки програмістів, але все ж таки основним завданням компанії залишається написання коду, а не навчання програмістів.
-
Конкретні творці по-своєму реалізують фабричний метод, виробляючи ті чи інші конкретні продукти.
Фабричний метод не зобов’язаний створювати нові об’єкти увесь час. Його можна переписати так, аби повертати з якогось сховища або кешу вже існуючі об’єкти.
Псевдокод
У цьому прикладі Фабричний метод допомагає створювати крос-платформові елементи інтерфейсу, не прив’язуючи основний код програми до конкретних класів кожного елементу.
Фабричний метод оголошений у класі діалогів. Його підкласи належать до різних операційних систем. Завдяки фабричному методу, вам не потрібно переписувати логіку діалогів під кожну систему. Підкласи можуть успадкувати майже увесь код базового діалогу, змінюючи типи кнопок та інших елементів, з яких базовий код будує вікна графічного користувацього інтерфейсу.
Базовий клас діалогів працює з кнопками через їхній загальний програмний інтерфейс. Незалежно від того, яку варіацію кнопок повернув фабричний метод, діалог залишиться робочим. Базовий клас не залежить від конкретних класів кнопок, залишаючи підкласам прийняття рішення про тип кнопок, які необхідно створити.
Такий підхід можна застосувати і для створення інших елементів інтерфейсу. Хоча кожен новий тип елементів наближатиме вас до Абстрактної фабрики.
Застосування
Коли типи і залежності об’єктів, з якими повинен працювати ваш код, невідомі заздалегідь.
Фабричний метод відокремлює код виробництва продуктів від решти коду, який використовує ці продукти.
Завдяки цьому код виробництва можна розширювати, не зачіпаючи основний код. Щоб додати підтримку нового продукту, вам потрібно створити новий підклас та визначити в ньому фабричний метод, повертаючи звідти екземпляр нового продукту.
Коли ви хочете надати користувачам можливість розширювати частини вашого фреймворку чи бібліотеки.
Користувачі можуть розширювати класи вашого фреймворку через успадкування. Але як же зробити так, аби фреймворк створював об’єкти цих класів, а не стандартних?
Рішення полягає у тому, щоб надати користувачам можливість розширювати не лише бажані компоненти, але й класи, які їх створюють. Тому ці класи повинні мати конкретні створюючі методи, які можна буде перевизначити.
Наприклад, ви використовуєте готовий UI-фреймворк для свого додатку. Але — от халепа — вам необхідно мати круглі кнопки, а не стандартні прямокутні. Ви створюєте клас RoundButton
. Але як сказати головному класу фреймворку UIFramework
, щоб він почав тепер створювати круглі кнопки замість стандартних прямокутних?
Для цього з базового класу фреймворку ви створюєте підклас UIWithRoundButtons
, перевизначаєте в ньому метод створення кнопки (а-ля, createButton
) і вписуєте туди створення свого класу кнопок. Потім використовуєте UIWithRoundButtons
замість стандартного UIFramework
.
Коли ви хочете зекономити системні ресурси, повторно використовуючи вже створені об’єкти, замість породження нових.
Така проблема зазвичай виникає під час роботи з «важкими», вимогливими до ресурсів об’єктами, такими, як підключення до бази даних, файлової системи й подібними.
Уявіть, скільки дій вам потрібно зробити, аби повторно використовувати вже існуючі об’єкти:
- Спочатку слід створити загальне сховище, щоб зберігати в ньому всі створювані об’єкти.
- При запиті нового об’єкта потрібно буде подивитись у сховище та перевірити, чи є там невикористаний об’єкт.
- Потім повернути його клієнтському коду.
- Але якщо ж вільних об’єктів немає, створити новий, не забувши додати його до сховища.
Увесь цей код потрібно десь розмістити, щоб не засмічувати клієнтський код.
Найзручнішим місцем був би конструктор об’єкта, адже всі ці перевірки потрібні тільки під час створення об’єктів, але, на жаль, конструктор завжди створює нові об’єкти, тому він не може повернути існуючий екземпляр.
Отже, має бути інший метод, який би віддавав як існуючі, так і нові об’єкти. Ним і стане фабричний метод.
Кроки реалізації
-
Приведіть усі створювані продукти до загального інтерфейсу.
-
Створіть порожній фабричний метод у класі, який виробляє продукти. В якості типу, що повертається, вкажіть загальний інтерфейс продукту.
-
Пройдіться по коду класу й знайдіть усі ділянки, що створюють продукти. По черзі замініть ці ділянки викликами фабричного методу, переносячи в нього код створення різних продуктів.
Можливо, доведеться додати до фабричного методу декілька параметрів, що контролюють, який з продуктів потрібно створити.
Імовірніше за все, фабричний метод виглядатиме гнітюче на цьому етапі. В ньому житиме великий умовний оператор, який вибирає клас створюваного продукту. Але не хвилюйтеся, ми ось-ось все це виправимо.
-
Для кожного типу продуктів заведіть підклас і перевизначте в ньому фабричний метод. З суперкласу перемістіть туди код створення відповідного продукту.
-
Якщо створюваних продуктів занадто багато для існуючих підкласів творця, ви можете подумати про введення параметрів до фабричного методу, аби повертати різні продукти в межах одного підкласу.
Наприклад, у вас є клас
Пошта
з підкласамиАвіаПошта
іНаземнаПошта
, а також класи продуктівЛітак
,Вантажівка
йПотяг
.Авіа
відповідаєЛітакам
, але дляНаземноїПошти
є відразу два продукти. Ви могли б створити новий підклас пошти й для потягів, але проблему можна вирішити по-іншому. Клієнтський код може передавати до фабричного методуНаземноїПошти
аргумент, що контролює, який з продуктів буде створено. -
Якщо після цих всіх переміщень фабричний метод став порожнім, можете зробити його абстрактним. Якщо ж у ньому щось залишилося — не страшно, це буде його типовою реалізацією (за замовчуванням).
Переваги та недоліки
- Позбавляє клас від прив’язки до конкретних класів продуктів.
- Виділяє код виробництва продуктів в одне місце, спрощуючи підтримку коду.
- Спрощує додавання нових продуктів до програми.
- Реалізує принцип відкритості/закритості.
- Може призвести до створення великих паралельних ієрархій класів, адже для кожного класу продукту потрібно створити власний підклас творця.
Відносини з іншими патернами
-
Багато архітектур починаються із застосування Фабричного методу (простішого та більш розширюваного за допомогою підкласів) та еволюціонують у бік Абстрактної фабрики, Прототипу або Будівельника (гнучкіших, але й складніших).
-
Класи Абстрактної фабрики найчастіше реалізуються за допомогою Фабричного методу, хоча вони можуть бути побудовані і на основі Прототипу.
-
Фабричний метод можна використовувати разом з Ітератором, щоб підкласи колекцій могли створювати необхідні їм ітератори.
-
Прототип не спирається на спадкування, але йому потрібна складна операція ініціалізації. Фабричний метод, навпаки, побудований на спадкуванні, але не вимагає складної ініціалізації.
-
Фабричний метод можна розглядати як окремий випадок Шаблонного методу. Крім того, Фабричний метод нерідко буває частиною великого класу з Шаблонними методами.
Додаткові матеріали
- Якщо ви вже чули про Фабрику, Фабричний метод чи Абстрактну фабрику, але вам все одно важко їх розрізняти, прочитайте нашу статтю Порівняння фабрик.