June 28

Паттерн Builder в Python

Builder, он же строитель - порождающий паттерн проектирования в программировании, который позволяет довольно удобно работать с построением сложных объектов.

Паттерн позволяет вызывать функции объекта для его сборки цепочкой, и это может выглядеть примерно так:
UserBuilder().with_name(“Ivan“).with_email(“i_connor@mail.ru“).with_subscription().build()

Пример применения

Реализация паттерна может отличаться, но мы не будем рассматривать сложные примеры. Допустим, перед нами стоит задача - написать автотест для проверки запроса на создание пользователя в какой-либо системе. json для отправки содержит в себе поля:

  • *имя: str
  • *фамилия: str
  • *почта: str
  • подписка:
    • *тип подписки: str
    • *наличие подписки (да/нет): bool
  • *количество бонусных баллов: int
  • *адрес: str
  • о себе: str
  • друзья: list[str]

где * означает обязательность поля.

В формате json это выглядит так:

{
    "name": "Vova",
    "last_name": "Ivanov",
    "email": "vivanov@mail.ru",
    "subscription": {
        "type": "hd+",
        "active": True
    },
    "bonuses": 1500,
    "address": "Belgorod, Lenina str., b. 10",
    "about": "Some additional info",
    "friends": ["12354", "1435462", "626245", "62461134"]
}

Простор для перебора параметров довольно обширен. Рассмотрим несколько вариантов для параметризации теста:

  1. Хранить заранее заготовленные  json-ы где-то в константах или отдельно в файлах, передавать их в тест
  2. Сделать функцию / фикстуру с параметрами для нужных полей
  3. Использовать faker для генерации рандомных данных и возвращать только их, без чуткого контроля над данными
  4. Создать удобный конструктор, где мы можем сами контролировать нужные поля, их наличие и значения

Выберем последний вариант и имплементируем наш класс-билдер.

Перед написанием кода нам понадобится установить дополнительную библиотеку faker.
Для этого используем команду: pip install Faker
import copy
import random
from pprint import pprint
from faker import Faker


class UserBuilder:
    def __init__(self):
        self._final_dict = {}  # Изначально пустой словарь, который будет пополняться даннными
        self.faker = Faker(locale="ru_RU")

    # Ниже перечислены методы, отвечающие за интересующие нас поля для сборки финального json-а

    def with_name(self, name: str | None = None) -> "UserBuilder":
        self._final_dict["name"] = name or self.faker.first_name_female()
        return self

    def with_last_name(self, last_name: str | None = None) -> "UserBuilder":
        self._final_dict["last_name"] = last_name or self.faker.last_name_female()
        return self

    def with_email(self, email: str | None = None) -> "UserBuilder":
        self._final_dict["email"] = email or self.faker.email()
        return self

    def with_subscription(self, is_active: bool, s_type: str) -> "UserBuilder":
        self._final_dict["subscription"] = {"active": is_active, "type": s_type}
        return self

    def with_bonuses(self, amount: int | None = None) -> "UserBuilder":
        self._final_dict["bonuses"] = amount or random.randint(500, 2000)
        return self

    def with_address(self, address: str | None = None) -> "UserBuilder":
        self._final_dict["address"] = address or self.faker.address()
        return self

    def with_about(self, about: str | None = None) -> "UserBuilder":
        self._final_dict["about"] = about or self.faker.text(max_nb_chars=100)
        return self

    def with_friends(self, friends_list: list[str]) -> "UserBuilder":
        self._final_dict["friends"] = friends_list
        return self

    def build(self) -> dict:
        user_data = copy.deepcopy(self._final_dict)
        self._final_dict.clear()
        return user_data  # Финальный аккорд билдера. Возвращает собранный словарь

Коротко о том, что у нас получилось:

  • Создали класс с пустым словарем в приватном атрибуте self._final_dict в __init__
  • Описали поля, которыми и будет заполняться наш пустой словарик.
    - Возврат self здесь является важной особенностью, т.к. это позволяет нам вызывать методы цепочкой через точку.
    - Упрощенно: при использовании билдера мы создаем объект класса и именно его и возвращаем каждый раз при вызове методов. Это позволяет всегда иметь доступ к методам и атрибутам экземпляра класса.
    - Конструкции вида email or self.faker.email() позволяют нам использовать фейковую генерацию данных, если мы ничего не передали в аргумент метода.
  • В методе build уже возвращается не экземпляр класса, а собранный нами словарь. Это завершающий метод билдера.
    Кроме того, для безопасной работы и возможности создания одного экземпляра класса UserBuilder для генерации нескольких словарей, внутри реализован механизм копирования данных в отдельную переменную, а затем очистка собранного нами словаря.

На этом кратком объяснении и остановимся, перейдем к использованию:

# Создаем экземпляр класса UserBuilder
builder = UserBuilder()


# Первый пользователь с полным набором полей
user1 = (
    builder
    .with_name("Егор")
    .with_last_name("Летов")
    .with_email("eletov@mail.ru")
    .with_subscription(is_active=True, s_type="hd+")
    .with_bonuses(2000)
    .with_address("Омск")
    .with_about("Музыкант")
    .with_friends(["1234141", "35152446"])
    .build()
)


# Второй пользователь с набором полей поменьше
user2 = (
    builder
    .with_name("Денис")
    .with_last_name("Сергеев")
    .with_email("dsergeev@mail.ru")
    .with_bonuses(1000)
    .build()
)


# Третий пользователь с полностью сгенерированными данными с использованием библиотеки faker
user3 = (
    builder
    .with_name()
    .with_last_name()
    .with_email()
    .with_address()
    .build()
)


# Распечатаем все с помощью функции pprint, которая сделает наш вывод немного красивее
pprint(user1, indent=2, sort_dicts=False)
pprint(user2, indent=2, sort_dicts=False)
pprint(user3, indent=2, sort_dicts=False)

Давайте посмотрим на результат:

{ 'name': 'Егор',
  'last_name': 'Летов',
  'email': 'eletov@mail.ru',
  'subscription': {'active': True, 'type': 'hd+'},
  'bonuses': 2000,
  'address': 'Омск',
  'about': 'Музыкант',
  'friends': ['1234141', '35152446']}
{ 'name': 'Денис',
  'last_name': 'Сергеев',
  'email': 'dsergeev@mail.ru',
  'bonuses': 1000}
{ 'name': 'Феврония',
  'last_name': 'Кузнецова',
  'email': 'naina_1999@example.com',
  'address': 'с. Ухта, ш. Революционное, д. 2 к. 4/1, 906254'}

Конечное использование данных может быть разным, не будем на нем концентрироваться.

Паттерны - это не магия или что-то переусложненное, а реальные механизмы, которые помогут структурировать код и облегчить работу над сложными объектами в повседневных задачах.

Конкретно билдер позволяет нам собирать нужный объект как конструктор, при этом шаг за шагом контролируя весь процесс.


Мой telegram-канал, присоединяйтесь :)

⬇⬇⬇

https://t.me/python3_with_love