Порождающие шаблоны описывают создание (instantiate) объекта или группы связанных объектов.
Простая фабрика
Аналогия
Допустим, вы строите дом и вам нужны двери. Будет бардак, если каждый раз, когда вам требуется дверь, вы станете вооружаться инструментами и делать её на стройплощадке. Вместо этого вы закажете двери на фабрике.
Вкратце
Простая фабрика просто генерирует экземпляр для клиента без предоставления какой-либо логики экземпляра.
Пример
Для начала нам нужен интерфейс двери и его реализация.
class WoodenDoor:
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def get_width(self) -> float:
return self.width
def get_height(self) -> float:
return self.height
Теперь соорудим фабрику дверей, которая создаёт и возвращает нам двери.
class DoorFactory:
@staticmethod
def make_door(width: float, height: float) -> WoodenDoor:
return WoodenDoor(width, height)
Использование:
door = DoorFactory.make_door(100, 200)
print('Width: ' + door.get_width())
print('Height: ' + door.get_height())
Когда использовать?
Когда создание объекта подразумевает какую-то логику, а не просто несколько присваиваний, то имеет смысл делегировать задачу выделенной фабрике, а не повторять повсюду один и тот же код.
Фабричный метод
Аналогия
Одна кадровичка не в силах провести собеседования со всеми кандидатами на все должности. В зависимости от вакансии она может делегировать разные этапы собеседований разным сотрудникам.
Вкратце
Это способ делегирования логики создания объектов (instantiation logic) дочерним классам.
Пример
Сначала создадим интерфейс сотрудника, проводящего собеседование, и некоторые реализации для него.
from abc import abstractmethod, ABC
class Interviewer(ABC):
@abstractmethod
def ask_questions(self):
pass
class Developer(Interviewer):
def ask_questions(self):
print('Asking about design patterns!')
class CommunityExecutive(Interviewer):
def ask_questions(self):
print('Asking about community building')
Теперь создадим кадровичку HiringManager
.
from abc import abstractmethod, ABC
class HiringManager(ABC):
@abstractmethod
def make_interviewer(self) -> Interviewer:
pass
def take_interview(self):
interviewer = self.make_interviewer()
interviewer.ask_questions()
Любой дочерний класс может расширять его и предоставлять нужного собеседующего:
class DevelopmentManager(HiringManager):
def make_interviewer(self) -> Interviewer:
return Developer()
class MarketingManager(HiringManager):
def make_interviewer(self) -> Interviewer:
return CommunityExecutive()
Использование:
dev_manager = DevelopmentManager()
dev_manager.take_interview() # Output: Спрашивает о шаблонах проектирования.
marketing_manager = MarketingManager()
marketing_manager.take_interview() # Output: Спрашивает о создании сообщества.
Когда использовать?
Этот шаблон полезен для каких-то общих обработок в классе, но требуемые подклассы динамически определяются в ходе выполнения (runtime). То есть когда клиент не знает, какой именно подкласс может ему понадобиться.
Абстрактная фабрика
Аналогия
Вернёмся к примеру с дверями из «Простой фабрики». В зависимости от своих потребностей вы можете купить деревянную дверь в одном магазине, стальную — в другом, пластиковую — в третьем. Для монтажа вам понадобятся разные специалисты: деревянной двери нужен плотник, стальной — сварщик, пластиковой — спец по ПВХ-профилям.
Вкратце
Это фабрика фабрик. То есть фабрика, группирующая индивидуальные, но взаимосвязанные/взаимозависимые фабрики без указания для них конкретных классов.
Пример
Создадим интерфейс Door и несколько реализаций для него.
from abc import abstractmethod, ABC
class Door(ABC):
@abstractmethod
def get_description(self):
pass
class WoodenDoor(Door):
def get_description(self):
print('I am a wooden door')
class IronDoor(Door):
def get_description(self):
print('I am an iron door')
Теперь нам нужны специалисты по установке каждого вида дверей.
class DoorFittingExpert(ABC):
@abstractmethod
def get_description(self):
pass
class Welder(DoorFittingExpert):
def get_description(self):
print('I can only fit iron doors')
class Carpenter(DoorFittingExpert):
def get_description(self):
print('I can only fit wooden doors')
Мы получили абстрактную фабрику, которая позволяет создавать семейства объектов или взаимосвязанные объекты. То есть фабрика деревянных дверей создаст деревянную дверь и человека для её монтажа, фабрика стальных дверей — стальную дверь и соответствующего специалиста и т. д.
class DoorFactory(ABC):
@abstractmethod
def make_door(self):
pass
@abstractmethod
def make_fitting_expert(self):
pass
# Фабрика деревянных дверей возвращает плотника и деревянную дверь
class WoodenDoorFactory(DoorFactory):
@abstractmethod
def make_door(self):
return WoodenDoor()
@abstractmethod
def make_fitting_expert(self):
return Carpenter()
# Фабрика деревянных дверей возвращает плотника и деревянную дверь
class IronDoorFactory(DoorFactory):
@abstractmethod
def make_door(self):
return IronDoor()
@abstractmethod
def make_fitting_expert(self):
return Welder()
Использование:
wooden_factory = WoodenDoorFactory()
door = wooden_factory.make_door()
expert = wooden_factory.make_fitting_expert()
door.get_description() # Output: Я деревянная дверь
expert.get_description() # Output: Я могу устанавливать только деревянные двери
# Same for Iron Factory
iron_factory = IronDoorFactory()
door = iron_factory.make_door()
expert = iron_factory.make_fitting_expert()
door.get_description() # Output: Я стальная дверь
expert.get_description() # Output: Я могу устанавливать только стальные двери
Здесь фабрика деревянных дверей инкапсулировала carpenter
и wooden door
, фабрика стальных дверей — iron door
and welder
. То есть можно быть уверенными, что для каждой из созданных дверей мы получим правильного специалиста.
Когда использовать?
Когда у вас есть взаимосвязи с не самой простой логикой создания (creation logic).
Строитель
Аналогия
Допустим, вы пришли в забегаловку, заказали бургер дня, и вам выдали его без вопросов. Это пример «Простой фабрики». Но иногда логика создания состоит из большего количества шагов. К примеру, при заказе бургера дня есть несколько вариантов хлеба, начинки, соусов, дополнительных ингредиентов. В таких ситуациях помогает шаблон «Строитель».
Вкратце
Шаблон позволяет создавать разные свойства объекта, избегая загрязнения конструктора (constructor pollution). Это полезно, когда у объекта может быть несколько свойств. Или когда создание объекта состоит из большого количества этапов.
Поясню, что такое антипаттерн Telescoping constructor. Каждый из нас когда-либо сталкивался с подобным конструктором:
def __init__(self, cheese: bool = True, pepperoni: bool = True, tomato: bool = False, lettuce: bool = True):
...
Как видите, количество параметров может быстро разрастись, и станет трудно разобраться в их структуре. Кроме того, этот список параметров будет расти и дальше, если в будущем вы захотите добавить новые опции. Это и есть антипаттерн Telescoping constructor.
Пример
Разумная альтернатива — шаблон «Строитель». Сначала создадим бургер:
class Burger:
def __init__(self, builder: BurgerBuilder):
self.size = builder.size
self.cheese = builder.cheese
self.pepperoni = builder.pepperoni
self.lettuce = builder.lettuce
self.tomato = builder.tomato
А затем добавим «строителя»:
class BurgerBuilder:
def __init__(self, size: int):
self.size = size
self.cheese = False
self.pepperoni = False
self.lettuce = False
self.tomato = False
def add_pepperoni(self):
self.pepperoni = True
return self
def add_lettuce(self):
self.lettuce = True
return self
def add_cheese(self):
self.cheese = True
return self
def add_tomato(self):
self.tomato = True
return self
def build(self) -> Burger:
return Burger(self)
Использование:
burger = BurgerBuilder(14) \
.add_pepperoni() \
.add_lettuce() \
.add_tomato() \
.build()
Когда использовать?
Когда у объекта может быть несколько свойств и когда нужно избежать Telescoping constructor. Ключевое отличие от шаблона «Простая фабрика»: он используется в одноэтапном создании, а «Строитель» — в многоэтапном.
Прототип
Аналогия
Помните клонированную овечку Долли? Так вот, этот шаблон проектирования как раз посвящён клонированию.
Вкратце
Объект создаётся посредством клонирования существующего объекта.
Пример
В PHP это легко можно сделать с помощью clone
:
class Sheep:
def __init__(self, name: str, category: str = 'Mountain Sheep'):
self.name = name
self.category = category
def set_name(self, name: str):
self.name = name
def get_name(self) -> str:
return self.name
def set_category(self, category: str):
self.category = category
def get_category(self):
return self.category
Затем можно клонировать так:
original = Sheep('Jolly')
print(original.get_name())
print(original.get_category())
from copy import deepcopy
cloned = deepcopy(original)
cloned.set_name('Dolly')
print(original.get_name())
print(original.get_category())
Также для модификации процедуры клонирования можно обратиться к магическому методу __clone
.
Когда использовать?
Когда необходимый объект аналогичен уже существующему или когда создание с нуля дороже клонирования.
Одиночка
Аналогия
У страны может быть только один президент. Он должен действовать, когда того требуют обстоятельства и долг. В данном случае президент — одиночка.
Вкратце
Шаблон позволяет удостовериться, что создаваемый объект — единственный в своём классе.
Пример
Сделайте конструктор приватным, отключите расширения и создайте статическую переменную для хранения экземпляра:
class President:
instance = None
def __init__(self):
raise RuntimeError("Use `get_instance` instead")
def get_instance(self) -> 'President':
if self.instance is None:
self.instance = President()
return self.instance
Использование:
president1 = President.get_instance()
president2 = President.get_instance()
assert president1 == president2 # True
Заимствовано отсюда