Паттерн 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'}
Конечное использование данных может быть разным, не будем на нем концентрироваться.
Паттерны - это не магия или что-то переусложненное, а реальные механизмы, которые помогут структурировать код и облегчить работу над сложными объектами в повседневных задачах.
Конкретно билдер позволяет нам собирать нужный объект как конструктор, при этом шаг за шагом контролируя весь процесс.