Information
- Rating
- 1,153-rd
- Location
- Санкт-Петербург, Санкт-Петербург и область, Россия
- Date of birth
- Registered
- Activity
Specialization
Бэкенд разработчик
Python
Clean Architecture
Асинхронное программирование
Многопоточность
Базы данных
Высоконагруженные системы
CI/CD
Мониторинг
Тестирование ПО
Веб-разработка
передавать* =) что-то с утра плохо соображаю
но термин sessionmaker не такой явный, в разных контекстах это называется по разному - например, DbContext, EntityManagerFactory, etc. Uow более универсален и сразу понятен читателю
Репозиторий отвечает только за одну сущность - save, update_status, delete, etc.
Uow отвечает за транзакцию и координирует несколько репозиториев в рамках одной транзакции. Это тоже про зону ответственности. По идее можно упростить и передовать sessionmaker и нужные репозитории в юскейс и там уже координировать
может сделаю в рамках другой статьи, но тема слишком слишком широкая
можно решить с помощью ретраев, можно сделать outbox pattern с кроном, да много чего еще. Но это выходит за рамки этой статьи. Тут только про clean architecture без доп сложностей
и еще тут немного дополню - сущность никак не должна знать про какие-то инфрастуктурные евенты, это не ее зона ответственности
Я пошел спать. Давай продолжим завтра, чтобы обсудить детали спокойно и с ясной головой
Это очень спорная тема, думаю, что нет смысла продолжать ее.
Все просто, поток примерно такой:
Транспортный слой - consumer получает событие из внешней системы
Он вызывает use case, передавая данные события.
Use case выполняет всю бизнес-логику: проверяет состояние сущностей, модифицирует их, взаимодействует с внешними системами, при необходимости генерирует события для других сервисов
Entity внутри use case - только контейнер для состояния и локальных процессов, никаких интеграций нет
Сущность не знает про event bus или внешние сервисы
И пример антипаттерна, которого мы избегаем:
В этом антипаттерне сущность сама знает про внешнюю систему event_bus - это приводит к сильной связанности и ломает принцип Clean Architecture
примеры игрушечные, но концепция, наверное, стала понятной.
Еще раз пишу, в который раз.
Сущность может оперировать только своими собственными атрибутами. Она не имеет права обращаться к другим сущностям, которые не хранятся в её полях, и уж тем более использовать внешние ресурсы или инфраструктуру (БД, API, файловую систему и т.д.). Любая логика, требующая проверки или изменения других объектов, должна выноситься в сервис или use case. Это фундаментальный принцип объектно-ориентированного дизайна: сущность отвечает только за себя, а не за весь мир.
Разве в примере этого не видно?
Сервисный слой(use cases) в любом случае остаётся, вопрос тут скорее не "сервис или нет", а в том, где живут бизнес операции, относящиеся к конкретной сущности.
Если такая логика находится в сервисах, то при росте системы мы получаем дублирование правил или вынуждены выносить их в хелперы, что увеличивает связность.
Например, допустим у нас есть invoice.signers, и сегодня это set, потому что нам важна уникальность. Завтра мы меняем его на list, потому что появилась необходимость хранить порядок подписей.
Если логика проверки "уже подписывал или нет" находится в нескольких сервисах, нам придётся менять её во всех этих местах.
Если же локальные бизнес-операции сущности инкапсулированы внутри сущности (например, invoice.add_signer() или invoice.is_signed_by(user)), то изменение set -> list затрагивает только саму сущность, а внешний код остаётся стабильным
Спасибо, что заметили поверхностность. Сейчас исправлю с "Доменная модель описывает бизнес-сущность и её правила." на "Доменная модель описывает бизнес-сущность и её локальные правила". Также добавлю небольшое уточнение
Domain model в моём примере не пытается инкапсулировать вообще весь бизнес процесс системы. Её задача в локальных инвариантах сущности:
допустимых переходах состояний
защите собственного состояния
правилах, которые принадлежат именно этому объекту Entity не должна отвечать за то, что ей не принадлежит (Привет, первый pattern из GRASP)
Да, в статье сейчас написано это немного неудачно. Исправлю.
Не понял почему "не самодостаточна даже для соблюдения своих собственных инвариантов", можете подробнее раскрыть, пожалуйста.
Это можно оформить и функцией, и классом - это уже вопрос стиля и архитектурных предпочтений. Например, я использую DI фреймворк и мне так более предпочтительнее.
Бизнес-сущность должна отвечать только за своё состояние и свои инварианты. Это её граница ответственности. Всё, что выходит за пределы управления одним объектом, а тем более задействует какие-то инфрастуктурные компоненты - выносим в юскейс.
Я как раз это и разбирал в разделе “Нюансы написания Use Cases на практике”. И там оставил как раз вопрос про best practice написания юскейсов, может вы знаете?
и да, если пользователь условно отправит евент через час или день - ничего страшного не будет, его может обработать любой воркер. Т.е я к тому что мы выставляем graceful timeout как максимальное время выполнения стадии в приложении
В нашем случае “просто поднять новые воркеры и переключить трафик” проблематично, потому что система построена как event-driven DAG-пайплайн с 7-9 стадиями на каждый доменный агрегат. Т.е для обработки каждой стадии у нас есть consumer, который прослушивает определенную очередь. Это сделано для того, чтобы пользователь мог постепенно контролировать и взаимодействовать с процессом генерации.
В rabbitmq переключить трафик можно:
Через bindings (создание + удаление) - сразу отметаем по понятным причинам
через разные blue/green exchange + прокидывать в приложение current_deploy(blue/green) - но это может привести к такому состоянию, когда веб и воркер могут работать с разными версиями current_deploy
в любом случае в самом приложении(не на уровне инфры) мы должны будем реагировать на sigterm и ожидать выполнения текущих задач
У нас это сделано как обёртка над consumer - проверка is_accepting происходит до начала обработки сообщения. сценарий такой:
мы получаем сообщение в нашем фоновом процессе, назовем его GREEN
мы сразу проверяем is_accepting
если False - мы даже не заходим в бизнес логику - сразу делаем nack(requeue=true)
сообщение уходит обратно в очередь и его обработает уже новый активный фоновый процесс BLUE, у которого при инициализации is_accepting=True
сам GREEN процес при этом находится в режиме graceful shutdown: он перестает принимать новые задачи, но при этом корректно завершает уже запущенные
получается, что выполняющиеся задачи никогда не прерываются - nack выполняется только к новым сообщениям
Спасибо за идею. Думаю, переработаю подход в эту сторону
не использовал профили. спасибо, посмотрю
Да, на одном сервере. Спасибо за ссылку, посмотрю
Спасибо, посмотрю на k3s. Надеюсь 4гб оперативки на сервере хватит)
мы не прерываем уже выполняющиеся задачи, мы просто не принимаем новые, т.е отправляем их обратно в очередь на обработку нашему новому активному фоновому процессу