Обновить
8K+
2
Лев@merra123

Люблю писать код

4
Рейтинг
1
Подписчики
Отправить сообщение

передавать* =) что-то с утра плохо соображаю

но термин sessionmaker не такой явный, в разных контекстах это называется по разному - например, DbContext, EntityManagerFactory, etc. Uow более универсален и сразу понятен читателю

Репозиторий отвечает только за одну сущность - save, update_status, delete, etc.
Uow отвечает за транзакцию и координирует несколько репозиториев в рамках одной транзакции. Это тоже про зону ответственности. По идее можно упростить и передовать sessionmaker и нужные репозитории в юскейс и там уже координировать

может сделаю в рамках другой статьи, но тема слишком слишком широкая

можно решить с помощью ретраев, можно сделать outbox pattern с кроном, да много чего еще. Но это выходит за рамки этой статьи. Тут только про clean architecture без доп сложностей

Будете городить городьбу внутри сущности, возвращать из нее события

и еще тут немного дополню - сущность никак не должна знать про какие-то инфрастуктурные евенты, это не ее зона ответственности

Я пошел спать. Давай продолжим завтра, чтобы обсудить детали спокойно и с ясной головой

Логика в сущности - это бесполезная полу-мера. Никакого толку от метода is_signed в сущности нет, потому что по итогу она мне не отвечает на вопрос - подписано ли что-то или нет, мне нужно еще искать другую половинку логики. Проще все концентрировать в одном месте где таких ограничений нет.

Это очень спорная тема, думаю, что нет смысла продолжать ее.

Потом будет еще веселей, когда захотите реагировать на события. Будете городить городьбу внутри сущности, возвращать из нее события, пришивать эти события к юзкейсам и прочий мрак. Знаем проходили.

Все просто, поток примерно такой:

  • Транспортный слой - consumer получает событие из внешней системы

  • Он вызывает use case, передавая данные события.

  • Use case выполняет всю бизнес-логику: проверяет состояние сущностей, модифицирует их, взаимодействует с внешними системами, при необходимости генерирует события для других сервисов

  • Entity внутри use case - только контейнер для состояния и локальных процессов, никаких интеграций нет

# вот consumer:
async def handle_invoice_signed(
    event: InvoiceSignedEvent,
    use_case: FromInjector[SignInvoiceUseCase],
):
  await use_case.execute(invoice_id=event.invoice_id, user_id=event.user_id)


  
# __________________________________________________________
# вот юскейс:
class SignInvoiceUseCase:
    def __init__(self, uow: IUoW, publisher: EventPublisher):
        self.uow = uow
        self.publisher = publisher

    async def execute(self, invoice_id: UUID, user_id: UUID):
        async with self.uow:
            invoice = await self.uow.invoice_gate.get_by_id(invoice_id)

            # Локальная логика сущности, не знает про внешние события
            invoice.add_signer(user_id)

            await self.uow.invoice_gate.save(invoice)
            await self.uow.commit()

            await self.publisher.publish(
                invoice_exchange, InvoiceSignedNotification(invoice_id=invoice.id)
            )

Сущность не знает про event bus или внешние сервисы

И пример антипаттерна, которого мы избегаем:

# невероятный антипаттерн
@dataclass
class Invoice:
    id: UUID
    signers: set[UUID]

    def add_signeyr(self, user_id: UUID):
        self.signers.add(user_id)
        event_bus.publish(InvoiceSignedNotification(self.id))

В этом антипаттерне сущность сама знает про внешнюю систему 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)

Да, в статье сейчас написано это немного неудачно. Исправлю.

У нас получилась модель которая не самодостаточна даже для соблюдения своих собственных инвариантов а значит толку от нее ноль, она все равно нормально не работает и намного удобнее сконцентрировать всю доменную логику в одном месте - в сервисе.

Не понял почему "не самодостаточна даже для соблюдения своих собственных инвариантов", можете подробнее раскрыть, пожалуйста.

Окей. Зачем нам этот класс? Вы по сути написали очень странную и неуклюжую функцию. Просто передайте storage и uow в параметры и выкинете никому не нужный класс

Это можно оформить и функцией, и классом - это уже вопрос стиля и архитектурных предпочтений. Например, я использую DI фреймворк и мне так более предпочтительнее.

Во-вторых, у вас в этом классе, как я и писал раньше, торчат инварианты инварианты, которые размазаны между несколькими классами.

Бизнес-сущность должна отвечать только за своё состояние и свои инварианты. Это её граница ответственности. Всё, что выходит за пределы управления одним объектом, а тем более задействует какие-то инфрастуктурные компоненты - выносим в юскейс.

В-третих, что будете делать, когда появиться общая логика между usecases? Дублировать? Или выносить в еще одну костыль-абстракцию?

Я как раз это и разбирал в разделе “Нюансы написания Use Cases на практике”. И там оставил как раз вопрос про best practice написания юскейсов, может вы знаете?

и да, если пользователь условно отправит евент через час или день - ничего страшного не будет, его может обработать любой воркер. Т.е я к тому что мы выставляем graceful timeout как максимальное время выполнения стадии в приложении

В нашем случае “просто поднять новые воркеры и переключить трафик” проблематично, потому что система построена как event-driven DAG-пайплайн с 7-9 стадиями на каждый доменный агрегат. Т.е для обработки каждой стадии у нас есть consumer, который прослушивает определенную очередь. Это сделано для того, чтобы пользователь мог постепенно контролировать и взаимодействовать с процессом генерации.
В rabbitmq переключить трафик можно:

  1. Через bindings (создание + удаление) - сразу отметаем по понятным причинам

  2. через разные blue/green exchange + прокидывать в приложение current_deploy(blue/green) - но это может привести к такому состоянию, когда веб и воркер могут работать с разными версиями current_deploy

в любом случае в самом приложении(не на уровне инфры) мы должны будем реагировать на sigterm и ожидать выполнения текущих задач

У нас это сделано как обёртка над consumer - проверка is_accepting происходит до начала обработки сообщения. сценарий такой:

  1. мы получаем сообщение в нашем фоновом процессе, назовем его GREEN

  2. мы сразу проверяем is_accepting

  3. если False - мы даже не заходим в бизнес логику - сразу делаем nack(requeue=true)

  4. сообщение уходит обратно в очередь и его обработает уже новый активный фоновый процесс BLUE, у которого при инициализации is_accepting=True

сам GREEN процес при этом находится в режиме graceful shutdown: он перестает принимать новые задачи, но при этом корректно завершает уже запущенные
получается, что выполняющиеся задачи никогда не прерываются - nack выполняется только к новым сообщениям

Спасибо за идею. Думаю, переработаю подход в эту сторону

не использовал профили. спасибо, посмотрю

Да, на одном сервере. Спасибо за ссылку, посмотрю

Спасибо, посмотрю на k3s. Надеюсь 4гб оперативки на сервере хватит)

мы не прерываем уже выполняющиеся задачи, мы просто не принимаем новые, т.е отправляем их обратно в очередь на обработку нашему новому активному фоновому процессу

Информация

В рейтинге
1 153-й
Откуда
Санкт-Петербург, Санкт-Петербург и область, Россия
Дата рождения
Зарегистрирован
Активность

Специализация

Бэкенд разработчик
Python
Clean Architecture
Асинхронное программирование
Многопоточность
Базы данных
Высоконагруженные системы
CI/CD
Мониторинг
Тестирование ПО
Веб-разработка