PERSONAL BLOG
Category: Programming

Шаблоны проектирования. Часть 1. Порождающие

Feb. 07, 2025

Порождающие шаблоны описывают создание (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

 

Заимствовано отсюда