Перший патерн, який ми з вами розглянемо у даній серії буде Абстрактна Фабрика. Англійською мовою його назва виглядає наступним чином: Abstract Factory.
В даній статті ми оглянемо:
- що являє собою даний патерн, та для чого він потрібен;
- розглянемо приклад: Войнушки;
- Плюси та Мінуси даного патерна;
- нюанси використання даного патерна в мові Python.
Отже:
Означення
Даний патерн належить до типу Породжуючих патернів і його означення звучить наступним чином: абстрактна фабрика надає інтерфейс створення сімейств взаємоповязанних і взаємо залежних обєктів без вказування їхніх конкретних класів.
Щось зрозуміли? Я – ні. Коли вперше прочитав дане непросте речення.
Проте після низки загуглених сторінок з прикладами на різних мовах програмування, серед англійських блогів та журналів, зрозумів, в чому суть. А ще зрозумів, що насправді давно використовую даний патерн у своїй щоденній роботі (правда у трохи іншій формі), просто до певного часу не свідомо і дійшов до нього читаючи чужий код.
Приклад: Войнушки
Розберемо даний патерн на старому доброму наглядному прикладі.
Уявимо, що воюють дві видумані (щоб уникнути аналогій та паралелей 😉 країни: Ліліпути та Велетні. В кожної країни є по три роди військ: піхота, повітряні війська та морські війська. Відповідно у кожної країни є військові наступних типів: піхотинець, льотчик та моряк. Кожен рід військ кожної з країн воює між собою. Тобто піхотинець із піхотинцем, моряк з моряком і льотчик з льотчиком.
А тепер давайте спробуємо окреслити необхідний мінімум коду та класів, щоб описати ці воєнізовані дії програмно.
Для кожного типу бійця та своєї країни буде визначений свій клас. На мові Python це вигладатиме наступним чином:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# -*- coding: utf-8 -*- # ліліпут: боєць піхоти class PihotaLiliput(object): def __init__(self, name): self.name = name def attack(self, voroh): print 'Liliput Pihota "%s" attacked Velikan Pihota "%s"' % (self.name, voroh.name) # ліліпут: льотчик class LetchikLiliput(object): def __init__(self, name): self.name = name def attack(self, voroh): print 'Liliput Letchik "%s" attacked Velikan Letchik "%s"' % ( self.name, voroh.name) # ліліпут: моряк class MorjakLiliput(object): def __init__(self, name): self.name = name def attack(self, voroh): print 'Liliput Morjak "%s" attacked Velikan Morjak "%s"' % (self.name, voroh.name) # велетень: боєць піхоти class PihotaVelikan(object): def __init__(self, name): self.name = name def attack(self, voroh): print 'Velikan Pihota "%s" attacked Liliput Pihota "%s"' % ( self.name, voroh.name) # велетень: льотчик class LetchikVelikan(object): def __init__(self, name): self.name = name def attack(self, voroh): print 'Velikan Letchik "%s" attacked Liliput Letchik "%s"' % ( self.name, voroh.name) # велетень: моряк class MorjakVelikan(object): def __init__(self, name): self.name = name def attack(self, voroh): print 'Velikan Morjak "%s" attacked Liliput Morjak "%s"' % (self.name, voroh.name) |
Вище описані шість Python класів по 3 на кожну країну Ліліпутію та країну Велетнів. Кожен із класів має методі ініціалізації, який встановлює ім’я бійцю. Також є метод attack, який дозволяє бійцю атакувати іншого бійця, того ж роду військ.
Тепер давайте глянемо як виглядатиме війна:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# війни в повітрі >>> letchik_lil = LetchikLiliput('Small Air') >>> letchik_vel = LetchikVelikan('Big Air') >>> letchik_lil.attack(letchik_vel) Liliput Letchik "Small Air" attacked Velikan Letchik "Big Air" # війна на суші >>> pihota_lil = PihotaLiliput('Ho') >>> pihota_vel = PihotaVelikan('Go') >>> pihota_vel.attack(pihota_lil) Velikan Pihota "Go" attacked Liliput Pihota "Ho" # війна на морі >>> morjak_lil = MorjakLiliput('Sea') >>> morjak_vel = MorjakVelikan('Ocean') >>> morjak_vel.attack(morjak_lil) Velikan Morjak "Ocean" attacked Liliput Morjak "Sea" >>> morjak_lil.attack(morjak_vel) Liliput Morjak "Sea" attacked Velikan Morjak "Ocean" |
Загалом непогано. Але…
Нам довелось використовувати явно імена класів в коді нашої війни. Для 3-х типів військ і двох країн це не проблема. Але, якщо матимемо 20 родів військ і 10 країн, які воюють між собою, тоді отримаємо масу проблем із підтримкою такого кучерявого коду.
Тому давайте спробуємо написати Абстрактну Фабрику, яка перебере на себе цю нудну справу – Створення нового об’єкта для сімейства (в нашому випадку це країна). Абстрактна Фабрика використовуватиметься для створення вояків для обидвох країн. Така собі заготовка для заводу виробництва солдат. Давайте спочатку кінцевий код оглянемо, а тоді більш детально розжуємо усе:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
# -*- coding: utf-8 -*- # абстракна фабрика кожного солдата у нашій війні # це по суті інтерфейс, який будуть імплементувати класи фабрик # солдатів конкретних країн class AbstractSoldatFactory(object): """Абстрактна фабрика описує три методи створення вояків. Один метод для кожного з видів військ. """ def createMorjak(self, name): """Створює моряка""" pass def createPihota(self, name): """Створює піхотинця""" pass def createLetchik(self, name): """Створює льотчика""" pass # а тепер вже конкретні фабрики для кожної із країн, імплементують # інтерфейс абстрактної фабрики # завод з виробництва вояків Ліліпутії class LiliputFactory(object): def createMorjak(self, name): return MorjakLiliput(name) def createPihota(self, name): return PihotaLiliput(name) def createLetchik(self, name): return LetchikLiliput(name) # завод з виробництва вояків Велетнів class VelikanFactory(object): def createMorjak(self, name): return MorjakVelikan(name) def createPihota(self, name): return PihotaVelikan(name) def createLetchik(self, name): return LetchikVelikan(name) |
Клас ‘AbstractSoldatFactory’ якраз і є Абстрактною Фабрикою. Такою собі заготовкою (інтерфейсом), з якої вже можна допиляти конкретний завод з виготовлення різноманітних солдат для тої чи іншої країни. Один завод може виготовляти солдат лише для однієї країни.
Інтерфейс – це свого роду домовленість, декларація про певний набір атрибутів та методів, який клас (той що імплементує даний інтерфейс) повинен реалізовувати.
В мові Python інтерфейсів немає вбудованих в інтерпретатор. Вони прийшли як додаток із світу Zope, у вигляді пакету zope.interfaces. І навіть, якщо використовувати даний пакет, все одно інтерфейс залишається не обов’язковим для класу і може використовуватись в наступних цілях:
- дійсно для декларації набору атрибутів та методів, що клас реалізує;
- для документації логіки та функціоналу класів;
- для маркування класів та об’єктів (це ми детальніше розглянемо в патерні програмування: адаптер).
Отже, маючи дані абстрактну та дві конкретних фабрики з виробництва трьох видів вояків для двох різних країн, тепер можемо переписати нашу війну наступним чином:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
# а тепер підготовка до війни, по суті перепишемо код попереднього прикладу # війни, але цього разу використовуючи наші заводи (фабрики) бійців # обидві країни створюють собі заводи, що вироблятимуть бійців >>> liliputy = LiliputFactory() >>> velikany = VelikanFactory() # наштампуємо кілька солдат по обидві сторони різних видів >>> letchik_lil = liliputy.createLetchik('Small Air') >>> letchik_vel = velikany.createLetchik('Big Air') >>> letchik_lil.attack(letchik_vel) >>> Liliput Letchik "Small Air" attacked Velikan Letchik "Big Air" # війна на суші >>> pihota_lil = liliputy.createPihota('Ho') >>> pihota_vel = velikany.createPihota('Go') >>> pihota_vel.attack(pihota_lil) Velikan Pihota "Go" attacked Liliput Pihota "Ho" # війна на морі >>> morjak_lil = liliputy.createMorjak('Sea') >>> morjak_vel = velikany.createMorjak('Ocean') >>> morjak_vel.attack(morjak_lil) >>> morjak_lil.attack(morjak_vel) Velikan Morjak "Ocean" attacked Liliput Morjak "Sea" Liliput Morjak "Sea" attacked Velikan Morjak "Ocean |
Таким чином на війні нам не треба переживати про те звідки брати солдат, як називаються їхні класи і т.д. Перед війною ми гарно підготувались – створили заводити на базі класів абстрактної та конкретних фабрик, і вже на війні з легкістю створюємо солдатів для обидвох країн використовуючи методи ‘create<Тип Солдата>’ кожної із фабрик.
Отже, основною задачею Абстрактої Фабрики є лише декларація інтерфейса (домовленості) для створення об’єктів того чи іншого сімейства. А вже конкретні імплементації абстрактної фабрики (у нашому випадку це LiliputFactory та VelikanFactory) займаються самим виробництвом (створенням) об’єктів згідно домовленостей Абстрактної Фабрики.
Плюси патерна та коли використовувати
- абстрактна фабрика ізолює створення об’єктів від основної логіки програми, даючи можливість працювати вам на рівні інтерфейсів та не задумуватись над конкретними імплементаціями кожного із сімейства об’єктів;
- також, якщо об’єкти одного сімейства повинні працювати один із одним, тоді абстрактна фабрика з легкістю дозволить вам працювати лише на рівні однотипних об’єктів;
- можливість з легкістю переключатись на нові сімейства об’єктів маючи процес створення нового об’єкту централізовано в специфічній фабриці, а не розкиданий по усьому коду програми.
Варта використовувати при розробці кросплатформенного коду. Також коли в майбутньому прийдеться переключатись між різними сімействами об’єктів. Абстрактна Фабрика дозволить зробити це одним махом.
Породжувальні Патерни конкурують між собою. У наступних статтях даної серії, коли розглядатимемо інші породжувальні патерни, детальніше розглядатимемо, який патерн і коли краще працює.
Недоліки патерна
Трудоємко додавати новий тип об’єкта у сімейство об’єктів. Для цього потрібно додати опис до Абстракної Фабрики (інтерфейсу), а потім пройтись по кожній із специфічних фабрик та додати до кожної із них новий факторі метод.
Щоб цього уникнути, можна мати лише один факторі метод (напр. універсальний createObject), який прийматиме ідентифікатор необхідного для створення типу об’єкта. Тут виникає нова проблемка, але вникати в деталі поки далі не будемо.
Доцільність в мові програмування Python
Взагалі для Python програмістів деякі із Патернів Програмування звучать досить подібно один з іншим. І навіть деякі з них зазвичай непотрібні. В основному тому, що оригінальні патерни були в першу чергу придумані для мов типу C++ для того, щоб обходити певні обмеження мови. Мова Python даних обмежень не має, саме тому частина патернів може виглядати при програмуванні на цій мові трохи надуманою.
Конкретно в мові Пітон Абстрактна Фабрика не надто поширена в її оригінальному вигляді. Навіть не зважаючи на введення статичних методів класу, метакласів (модуля abs – abstract base class – абстрактний базовий клас) все одно широкої популярності даний патерн серед програмістів не набув. Думаю ще однією причиною малої популярності даного патерну є відсутність, як таких, обов’язкових інтерфейсів у мові Python, а також динамічність мови.
Спочатку приклад коду з використанням Python примочок для абстрактних класів:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# занадто багато коду import abc class AbstractFactory(object): __metaclass__ = abc.ABCMeta @abc.abstractmethod def make_object(self): return class ConcreteFactory1(AbstractFactory): def make_object(self): # do something return SomeObject1() class ConcreteFactory2(AbstractFactory): def make_object(self): # do something else return SomeObject2() |
А тепер приклад як зазвичай реалізовують потребу створення сімейств об’єктів пітонщики. Пітон є динамічно типізованою мовою і нам не потрібен абстрактний клас, щоб передавати класи функціям в якості аргументів. Зауважте на скільки поменшало коду:
1 2 3 4 5 6 7 8 9 |
class ConcreteFactory1(object): def make_object(self): # do something return SomeObject1() class ConcreteFactory2(object): def make_object(self): # do something else return SomeObject2() |
Оскільки Абстрактна Фабрика не виглядає надто природньою в інтерпретації Пітона, відповідно і реалізації є найрізноманітніші починаючи від порожніх методів і закінчуючи ‘raise NotImplementedError’ методами, щоб написати абстрактний клас із функціями.
***
Надіюсь вдалось дохідливо пояснити суть Абстрактної Фабрики та для чого вона може вам знадобитися, незважаючи на дивний приклад з ліліпутами та велетнями льотчиками 😉 Через тиждень розглянемо наступний патерн: Будівельник (Builder).
Чекаю ваших коментарів та критичних зауважень!
А як на вашій мовій можна наглядно пояснити патерн Абстрактної Фабрики?
Хочете більше дізнатись про веб-розробку та навчитись створювати веб-сайти використовуючи мову Python та веб-фреймворк Django? Гляньте дану пропозицію:
Було б круто без російської мови у іменах змінних(!) у лістингах до статті українською мовою. 🙂
Дякую за стаття 😉 Я зробив зразок в JavaScript
класно. з javascript я в патерни ще не бавився