Информация
- В рейтинге
- 1 074-й
- Откуда
- Санкт-Петербург, Санкт-Петербург и область, Россия
- Дата рождения
- Зарегистрирован
- Активность
Специализация
Бэкенд разработчик
Python
Clean Architecture
Асинхронное программирование
Многопоточность
Базы данных
Высоконагруженные системы
CI/CD
Мониторинг
Тестирование ПО
C
Да, Swarm тут подходит. Но переносить VPS кмк не так уж часто приходится, поэтому не хочу усложнять инфраструктуру ради такого случая. Также можно сделать скрипт, который подготовит новый сервер и сам переезд.
Бутылочное горлышко пока-что это ресурсы сервера)
Все упирается в контекст кейса. У меня никаким хайлоадом особо и не пахнет, никакого бутылочного горлышка у бд нет.
Про порядок: я осознанно оставил БД на старой машине и перевозил сначала stateless. Все очень легко и просто - приложение легко откатить, а базу нет. Пока новое приложение не подтвердило стабильность, я не хочу трогать данные.
Да, чтобы хорошо выделить контракты, нужно очень глубоко понимать бизнес домен. Без знания бизнес процессов, ограничений и т.д - очень легко промахнуться с осью изменчивости. Поэтому да, согласен, правило трех хорошо работает на практике.
Подписался, у вас отличные статьи!
передавать* =) что-то с утра плохо соображаю
но термин 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 выполняется только к новым сообщениям
Спасибо за идею. Думаю, переработаю подход в эту сторону