Паттерн Builder в Python
Builder, он же строитель - порождающий паттерн проектирования в программировании, который позволяет довольно удобно работать с построением сложных объектов.
Паттерн позволяет вызывать функции объекта для его сборки цепочкой, и это может выглядеть примерно так:UserBuilder().with_name(“Ivan“).with_email(“i_connor@mail.ru“).with_subscription().build()
Пример применения
Реализация паттерна может отличаться, но мы не будем рассматривать сложные примеры. Допустим, перед нами стоит задача - написать автотест для проверки запроса на создание пользователя в какой-либо системе. json для отправки содержит в себе поля:
- *имя: str
- *фамилия: str
- *почта: str
- подписка:
- *количество бонусных баллов: 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"]
}Простор для перебора параметров довольно обширен. Рассмотрим несколько вариантов для параметризации теста:
- Хранить заранее заготовленные json-ы где-то в константах или отдельно в файлах, передавать их в тест
- Сделать функцию / фикстуру с параметрами для нужных полей
- Использовать faker для генерации рандомных данных и возвращать только их, без чуткого контроля над данными
- Создать удобный конструктор, где мы можем сами контролировать нужные поля, их наличие и значения
Выберем последний вариант и имплементируем наш класс-билдер.
Перед написанием кода нам понадобится установить дополнительную библиотеку 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'}Конечное использование данных может быть разным, не будем на нем концентрироваться.
Паттерны - это не магия или что-то переусложненное, а реальные механизмы, которые помогут структурировать код и облегчить работу над сложными объектами в повседневных задачах.
Конкретно билдер позволяет нам собирать нужный объект как конструктор, при этом шаг за шагом контролируя весь процесс.