Upgrade to Pro — share decks privately, control downloads, hide ads and more …

D в DDD: где D — это доменная логика

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers. →
Avatar for MaxST MaxST
February 24, 2025

D в DDD: где D — это доменная логика

рассказал, как использует принципы Domain-Driven Design (DDD) в своей работе. Из доклада вы узнаете, почему так важно понимать бизнес-процессы вашего проекта и как это помогает создавать качественный и гибкий код.

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

Avatar for MaxST

MaxST

February 24, 2025

More Decks by MaxST

Other Decks in Programming

Transcript

  1. Обо мне Столпасов Максим Teamlead Backend МП Чижик Telegram: @Max_S_T

    E-mail: [email protected] ✅ С 2006 автоматизирую предприятия ✅ С 2012 Python ✅ C 2019 в Х5 ✅ Мое хобби моя работа
  2. Что такое DDD? Domain-Driven Design (DDD) – это подход к

    разработке программного обеспечения, который фокусируется на изучении и моделировании предметной области бизнеса 4 / 69
  3. Основная цель DDD? ✅ Все в DDD нацелено на то,

    чтобы победить сложность 5 / 69
  4. Сложность? Существенная Сложность (Essential Complexity) Сложность того что делает система,

    сложность реального мира, не зависящая от способа и деталей реализации. 😫 Бизнес-правила, зависящие от сложных условий 😫 Обработка большого количества форматов данных 😫 Алгоритмы с высокой вычислительной сложностью 😫 Системы реального времени 😫 Проектирование сложных пользовательских интерфейсов 6 / 69
  5. Сложность? Побочная Сложность (Accidental Complexity) Сложность, возникающая в процессе реализации,

    например, количество классов в программе образуют побочную сложность. 😫 Избыточное использование паттернов проектирования 😫 Излишняя абстракция 😫 Использование неудачных инструментов или фреймворков 😫 Сложные конфигурации сборки 😫 Запутанная обработка ошибок 7 / 69
  6. Основное деление DDD на части Стратегическая часть Тактическая часть ✅

    Универсальный язык ✅ Ограниченный контекст ✅ Карта контекстов ✅ ... ✅ Агрегаты ✅ Объекты значения ✅ Репозитории ✅ ... 8 / 69
  7. Луковая архитектура Слои ✅ Интерфейсный слой - для реализации api/очереди/cli

    ✅ Инфраструктурный слой - для взаимодействия с системой хранения 12 / 69
  8. Луковая архитектура Слои ✅ Интерфейсный слой - для реализации api/очереди/cli

    ✅ Инфраструктурный слой - для взаимодействия с системой хранения ✅ Прикладной слой - для оформления бизнес процессов/ пользовательских сценариев 13 / 69
  9. Луковая архитектура Слои ✅ Интерфейсный слой - для реализации api/очереди/cli

    ✅ Инфраструктурный слой - для взаимодействия с системой хранения ✅ Прикладной слой - для оформления бизнес процессов/ пользовательских сценариев ✅ Доменный слой - для реализации доменной логики 14 / 69
  10. DDD в D: Интро name_context ├── admin │ │ ├──

    api │ ├── tasks │ ├── infrastructure │ │ ├── services │ ├── application │ └── domain # содержит модули, связанные с административными функциями # приложения. # содержит модули, связанные с обработкой API интерфейса. # содержит определения задач, выполняемых асинхронно в приложении. # содержит модули, связанные с реализацией инфраструктурных # механизмов. # содержит модули, связанные с интеграцией с внешними сервисами. # содержит модули, связанные с бизнес-логикой приложения. # содержит модули, относящиеся к бизнес-логике приложения, а также # объединяющие сущности, сервисы и репозитории. 17 / 69
  11. DDD в D: Интро └── domain │ │ ├── aggregates

    │ ├── events │ │ ├── handlers │ │ ├── mappers │ │ ├── typings| │ interfaces| │ repositories │ └── value_objects # содержит модули, относящиеся к бизнес-логике приложения, а # также объединяющие сущности, сервисы и репозитории. # содержит агрегаты, отображающие комплексные бизнес-сущности. # содержит классы событий, позволяющих прослеживать изменения # состояния бизнес-сущностей. # содержит обработчики событий и изменения состояния # бизнес-сущностей. # содержит мапперы, отображающие данные из одного # представления в другое. # содержит интерфейсы(протоколы), используемые в бизнес-логике. # содержит классы, представляющие простые бизнес-сущности # (например, деньги, адреса). 18 / 69
  12. Доменная логика в DDD Представляет из себя центральную часть бизнес-приложения,

    которая инкапсулирует правила, поведение и бизнес-логику конкретной предметной области. 19 / 69
  13. Доменная логика в DDD Представляет из себя центральную часть бизнес-приложения,

    которая инкапсулирует правила, поведение и бизнес-логику конкретной предметной области. ✅ Использует единый язык. Может являться словарем 20 / 69
  14. Доменная логика в DDD Представляет из себя центральную часть бизнес-приложения,

    которая инкапсулирует правила, поведение и бизнес-логику конкретной предметной области. ✅ Использует единый язык. Может являться словарем ✅ Отражает бизнес правила 21 / 69
  15. Доменная логика в DDD Представляет из себя центральную часть бизнес-приложения,

    которая инкапсулирует правила, поведение и бизнес-логику конкретной предметной области. ✅ Использует единый язык. Может являться словарем ✅ Отражает бизнес правила ✅ Содержит знания, которые необходимы для выполнения задач 22 / 69
  16. Паттерны Доменного слоя ✅ Агрегаты (Aggregates) ✅ Объекты-значения (Value Objects)

    ✅ События предметной области (Events) ✅ Сущности (Entities) ✅ Сервисы домена (Domain Services) ✅ Репозитории (Repositories) 23 / 69
  17. Анемичная модель class Basket(Aggregate): basket_id: BasketId | None = None

    user_id: UserId = Field(description='идентификатор пользователя') shop: Shop | None = Field(None, description='магазин') address: Address | None = None max_weight: WeightBasket = 0 items: list[BasketItem] = Field(default_factory=list) 26 / 69
  18. Анемичная модель class Basket(Aggregate): basket_id: BasketId | None = None

    user_id: UserId = Field(description='идентификатор пользователя') shop: Shop | None = Field(None, description='магазин') address: Address | None = None max_weight: WeightBasket = 0 items: list[BasketItem] = Field(default_factory=list) ✅ Есть атрибуты 27 / 69
  19. Анемичная модель class Basket(Aggregate): basket_id: BasketId | None = None

    user_id: UserId = Field(description='идентификатор пользователя') shop: Shop | None = Field(None, description='магазин') address: Address | None = None max_weight: WeightBasket = 0 items: list[BasketItem] = Field(default_factory=list) ✅ Есть атрибуты ✅ Понятна обязательность 28 / 69
  20. Анемичная модель class Basket(Aggregate): basket_id: BasketId | None = None

    user_id: UserId = Field(description='идентификатор пользователя') shop: Shop | None = Field(None, description='магазин') address: Address | None = None max_weight: WeightBasket = 0 items: list[BasketItem] = Field(default_factory=list) ✅ Есть атрибуты ✅ Понятна обязательность ✅ Абстрагировались от инфраструктуры 29 / 69
  21. Анемичная модель class Basket(Aggregate): basket_id: BasketId | None = None

    user_id: UserId = Field(description='идентификатор пользователя') shop: Shop | None = Field(None, description='магазин') address: Address | None = None max_weight: WeightBasket = 0 items: list[BasketItem] = Field(default_factory=list) ✅ Есть атрибуты ✅ Понятна обязательность ✅ Абстрагировались от инфраструктуры ✅ Непонятно, что может происходить 30 / 69
  22. Анемичная модель Самоинкапсуляция class Basket(Aggregate): ... def change_address(self, address: Address)

    -> None: if address and address != self.address: self.address = address def check_max_weight(self) -> None: if (basket_weight := self.weight) > self.max_weight: raise BasketTooHeavyError( basket_id=self.basket_id, basket_weight=basket_weight, max_weight=self.max_weight, ) 31 / 69
  23. Анемичная модель Самоинкапсуляция class Basket(Aggregate): ... def change_address(self, address: Address)

    -> None: if address and address != self.address: self.address = address def check_max_weight(self) -> None: if (basket_weight := self.weight) > self.max_weight: raise BasketTooHeavyError( basket_id=self.basket_id, basket_weight=basket_weight, max_weight=self.max_weight, ) ✅ Видно, что адрес может меняться 32 / 69
  24. Анемичная модель Самоинкапсуляция class Basket(Aggregate): ... def change_address(self, address: Address)

    -> None: if address and address != self.address: self.address = address def check_max_weight(self) -> None: if (basket_weight := self.weight) > self.max_weight: raise BasketTooHeavyError( basket_id=self.basket_id, basket_weight=basket_weight, max_weight=self.max_weight, ) ✅ Есть проверки, которые можно переиспользовать в разных процессах 33 / 69
  25. Анемичная модель Самоинкапсуляция class Basket(Aggregate): ... def change_address(self, address: Address)

    -> None: if address and address != self.address: self.address = address def check_max_weight(self) -> None: if (basket_weight := self.weight) > self.max_weight: raise BasketTooHeavyError( basket_id=self.basket_id, basket_weight=basket_weight, max_weight=self.max_weight, ) 34 / 69
  26. Анемичная модель Самоинкапсуляция class Basket(Aggregate): ... def change_address(self, address: Address)

    -> None: if address and address != self.address: self.address = address def check_max_weight(self) -> None: if (basket_weight := self.weight) > self.max_weight: raise BasketTooHeavyError( basket_id=self.basket_id, basket_weight=basket_weight, max_weight=self.max_weight, ) ✅ Уже хорошо, но этого мало 35 / 69
  27. Анемичная модель Самоинкапсуляция class Basket(Aggregate): class ChangedAddressEvent(AggregateEvent): address: Address old_address:

    Address | None = None class ChangedShopEvent(AggregateEvent): shop: Shop class ChangedShopAtEvent(AggregateEvent): choice_shop_at: datetime class BasketCheckoutEvent(AggregateEvent): """Оформить заказ по корзине.""" ✅ События агрегата 37 / 69 A+ES+EDA
  28. Анемичная модель Самоинкапсуляция A+ES+EDA class Basket(Aggregate): def checkout(self) -> None:

    self.add_event(self.BasketCheckoutEvent()) def change_address(self, address: Address) -> None: if address and address != self.address: old_address = self.address self.address = address self.add_event( self.ChangedAddressEvent( address=address, old_address=old_address, ) ) ✅ События агрегата 38 / 69
  29. Инфраструктура для доменной логики Логика не должна зависеть от инфраструктуры,

    но без инфраструктуры логика остается лишь моделью 39 / 69
  30. Простой подход class BasketRepo: def get_by_user(self, user_id: UserId) -> BasketModel

    | None: return BasketModel.objects.filter(user_id=user_id).first() def get(self, basket_id: BasketId) -> BasketModel | None: ... 40 / 69
  31. Простой подход class BasketRepo: def get_by_user(self, user_id: UserId) -> BasketModel

    | None: return BasketModel.objects.filter(user_id=user_id).first() def get(self, basket_id: BasketId) -> BasketModel | None: ... ✅ Нет агрегатов ✅ Нет интерфейсов 41 / 69
  32. Простой подход class BasketRepo: def get_by_user(self, user_id: UserId) -> BasketModel

    | None: return BasketModel.objects.filter(user_id=user_id).first() def get(self, basket_id: BasketId) -> BasketModel | None: ... ✅ Нет агрегатов ✅ Нет интерфейсов ✅ Работаем с объектами ORM 42 / 69
  33. Простой подход class BasketRepo: def get_by_user(self, user_id: UserId) -> BasketModel

    | None: return BasketModel.objects.filter(user_id=user_id).first() def get(self, basket_id: BasketId) -> BasketModel | None: ... ✅ Нет агрегатов ✅ Нет интерфейсов ✅ Работаем с объектами ORM ✅ Изменяя структуру БД приходилось менять много слоев 43 / 69
  34. Простой подход Агрегатный class BasketRepo(IBasketRepo): def get_by_user(self, user_id: UserId) ->

    Basket | None: return to_basket(BasketModel.objects.filter(user_id=...).first()) def get(self, basket_id: BasketId) -> Basket | None: ... def save(self, basket: Basket) -> None: BasketModel.objects.filter(id=...).update(address=...) 44 / 69
  35. Простой подход Агрегатный class BasketRepo(IBasketRepo): def get_by_user(self, user_id: UserId) ->

    Basket | None: return to_basket(BasketModel.objects.filter(user_id=...).first()) def get(self, basket_id: BasketId) -> Basket | None: ... def save(self, basket: Basket) -> None: BasketModel.objects.filter(id=...).update(address=...) ✅ Добавили мапперы 45 / 69
  36. Простой подход Агрегатный class BasketRepo(IBasketRepo): def get_by_user(self, user_id: UserId) ->

    Basket | None: return to_basket(BasketModel.objects.filter(user_id=...).first()) def get(self, basket_id: BasketId) -> Basket | None: ... def save(self, basket: Basket) -> None: BasketModel.objects.filter(id=...).update(address=...) ✅ Добавили мапперы ✅ Добавили интерфейсы 46 / 69
  37. Простой подход Агрегатный class BasketRepo(IBasketRepo): def get_by_user(self, user_id: UserId) ->

    Basket | None: return to_basket(BasketModel.objects.filter(user_id=...).first()) def get(self, basket_id: BasketId) -> Basket | None: ... def save(self, basket: Basket) -> None: BasketModel.objects.filter(id=...).update(address=...) ✅ Добавили мапперы ✅ Добавили интерфейсы ✅ В контрактах теперь всегда работаем с доменными объектами 47 / 69
  38. Простой подход Агрегатный class BasketRepo(IBasketRepo): def get_by_user(self, user_id: UserId) ->

    Basket | None: return to_basket(BasketModel.objects.filter(user_id=...).first()) def get(self, basket_id: BasketId) -> Basket | None: ... def save(self, basket: Basket) -> None: BasketModel.objects.filter(id=...).update(address=...) ✅ Добавили мапперы ✅ Добавили интерфейсы ✅ В контрактах теперь всегда работаем с доменными объектами ✅ Добавили сохранение (1 агрегат 1 репозиторий) 48 / 69
  39. Простой подход Агрегатный С событиями class BasketRepo(IBasketRepo): def __init__(self): self.handle_changes

    = BasketChangesHandler() ✅ Добавили обработчик событий в репозиторий 50 / 69
  40. Простой подход Агрегатный С событиями from functools import singledispatchmethod class

    BasketChangesHandler: def __init__(self): self.update_fields = {} self.domain_events = [] def __call__(self, event: AggregateEvent, basket: Basket) -> None: self.handle_changes(event, basket) @singledispatchmethod def handle_changes(self, event: AggregateEvent, basket: Basket) -> None: # Это запускается если обработчик не был найден raise NotImplementedError(f'Не найдена реализация {event!r}') 51 / 69
  41. Простой подход Агрегатный С событиями from functools import singledispatchmethod class

    BasketChangesHandler: def __init__(self): self.update_fields = {} self.domain_events = [] def __call__(self, event: AggregateEvent, basket: Basket) -> None: self.handle_changes(event, basket) @singledispatchmethod def handle_changes(self, event: AggregateEvent, basket: Basket) -> None: # Это запускается если обработчик не был найден raise NotImplementedError(f'Не найдена реализация {event!r}') ✅ Используем singledispatchmethod 52 / 69
  42. Простой подход Агрегатный С событиями from functools import singledispatchmethod class

    BasketChangesHandler: def __init__(self): self.update_fields = {} self.domain_events = [] def __call__(self, event: AggregateEvent, basket: Basket) -> None: self.handle_changes(event, basket) @singledispatchmethod def handle_changes(self, event: AggregateEvent, basket: Basket) -> None: # Это запускается если обработчик не был найден raise NotImplementedError(f'Не найдена реализация {event!r}') ✅ Используем singledispatchmethod ✅ Объявляем метод обработки события 53 / 69
  43. Простой подход Агрегатный С событиями class BasketChangesHandler: @handle_changes.register def _(self,

    event: Basket.ChangedAddressEvent, basket: Basket) -> None self.update_fields['address'] = event.address self.domain_events.append( ChangedAddressEvent( basket_id=basket.basket_id, user_id=basket.user_id, old_address=event.old_address, address=basket.address, ) ) ✅ Регистрируем обработку события по его типу 54 / 69
  44. Простой подход Агрегатный С событиями class BasketChangesHandler: @handle_changes.register def _(self,

    event: Basket.ChangedAddressEvent, basket: Basket) -> None self.update_fields['address'] = event.address self.domain_events.append( ChangedAddressEvent( basket_id=basket.basket_id, user_id=basket.user_id, old_address=event.old_address, address=basket.address, ) ) ✅ Собираем, что изменилось для сохранения в БД 55 / 69
  45. Простой подход Агрегатный С событиями class BasketChangesHandler: @handle_changes.register def _(self,

    event: Basket.ChangedAddressEvent, basket: Basket) -> None self.update_fields['address'] = event.address self.domain_events.append( ChangedAddressEvent( basket_id=basket.basket_id, user_id=basket.user_id, old_address=event.old_address, address=basket.address, ) ) ✅ Собираем события которые нужно транслировать выше уровнем 56 / 69
  46. Простой подход Агрегатный С событиями class BasketRepo(IBasketRepo): def save(self, basket:

    Basket) -> None: handle_changes = self.handle_changes for event in basket.get_events(): handle_changes(event, basket) ✅ Обрабатываем все события агрегата 57 / 69
  47. Простой подход Агрегатный С событиями class BasketRepo(IBasketRepo): def save(self, basket:

    Basket) -> None: ... if create_fields := handle_changes.create_fields: data = create_fields | handle_changes.update_fields BasketModel.objects.create(**data) elif update_fields := handle_changes.update_fields: BasketModel.objects.filter(pk=basket_id).update(**update_field ✅ Вносим все изменения в систему хранения 58 / 69
  48. Простой подход Агрегатный С событиями class BasketRepo(IBasketRepo): def save(self, basket:

    Basket) -> None: ... if added_changed_items := handle_changes.items: BasketProductModel.objects.bulk_create( added_changed_items, batch_size=min(len(added_changed_items), 100), update_conflicts=True, unique_fields=('basket_id', 'product_id'), update_fields=('amount', 'modified_at'), ) ✅ Вносим все изменения в систему хранения 59 / 69
  49. Простой подход Агрегатный С событиями class BasketRepo(IBasketRepo): def save(self, basket:

    Basket) -> None: ... domain_events = handle_changes.domain_events for event in domain_events: handle_event(event) ✅ Получаем события для публикации 60 / 69
  50. Простой подход Агрегатный С событиями class BasketRepo(IBasketRepo): def save(self, basket:

    Basket) -> None: ... domain_events = handle_changes.domain_events for event in domain_events: handle_event(event) ✅ Получаем события для публикации ✅ Публикуем события 61 / 69
  51. Простой подход Агрегатный С событиями class BasketRepo(IBasketRepo): def save(self, basket:

    Basket) -> None: ... domain_events = handle_changes.domain_events for event in domain_events: handle_event(event) ✅ Получаем события для публикации ✅ Публикуем события ✅ Агрегат является границей транзакции. Где-то тут она должна заканчиваться 62 / 69
  52. Простой подход Агрегатный С событиями # notification/infrastructure/handlers.py @EVENT_BROKER.instance(ChangedAddressEvent) def handle_changes_address(event:

    ChangedAddressEvent) -> None: send_push( user_token=get_by_user_id(event.user_id), message='Вы сменили адрес доставки' f'с {event.old_address} на {event.address}', ) ✅ Вот так мы используем опубликованные события 63 / 69
  53. Простой подход Агрегатный С событиями # notification/infrastructure/handlers.py @EVENT_BROKER.instance(ChangedAddressEvent) def handle_changes_address(event:

    ChangedAddressEvent) -> None: send_push( user_token=get_by_user_id(event.user_id), message='Вы сменили адрес доставки' f'с {event.old_address} на {event.address}', ) ✅ Вот так мы используем опубликованные события ✅ Получаем событие 64 / 69
  54. Простой подход Агрегатный С событиями # notification/infrastructure/handlers.py @EVENT_BROKER.instance(ChangedAddressEvent) def handle_changes_address(event:

    ChangedAddressEvent) -> None: send_push( user_token=get_by_user_id(event.user_id), message='Вы сменили адрес доставки' f'с {event.old_address} на {event.address}', ) ✅ Вот так мы используем опубликованные события ✅ Получаем событие ✅ Выполняем нужную логику 65 / 69
  55. Рекомендации ✅ 1 агрегат - 1 репозиторий ✅ 1 таблица

    - 1 DAO объект ✅ Если бизнес говорит о каких-либо проверках, то это должно быть в домене ✅ БД/Система хранения не должны влиять на доменную логику ✅ Доменная логика - это лишь модель реальной предметной области, это нужно держать в голове. Доскональная модель может быть хуже 67 / 69