Статья получилось длинной соглашусь, спасибо что прочитали ее всю.
В статье я не говорю как должно быть, а показываю когда и в каких условиях нужно использовать тот или иной подход.
Дополнения к вашему варианту "Как должно быть:"
а) для библиотек:
ошибки не выводить в лог, т.к. у всех логгеры разные
Верно, в статье также говорится, но есть slog и стандартный пакет log, которые при определенных условиях можно применять. Примеры: net/http, gin
делать "Текстовый wrapping %w" ошибок
Верно, в статье также говорится, плюс я говори еще про Custom Errors, который широко используется. os.PathError
все ошибки возвращать
Верно, в статье также говорится, но есть исключения описанные в статье. Примеры из stdlib найдены через поиск _ = : math, strconv.
не делать панику (или fatal)
Верно, но есть исключения описанные в статье. Примеры из stdlib: strings, strconv.
Зачему, что исключения я описывал в статье и в самих регламентах они помечены "?" - применять при определенных условиях.
б) для остальных приложений:
все ошибки выводить в лог сразу же в месте появления ошибки, даже если ошибку возвращаем наверх.
Логирование по месту захламляет лог дублями и усложняет понимания реального количества ошибок, поэтому не могу советовать этого.
при возвращении ошибки наверх - надо ошибку вывести в лог ещё раз, второй раз с новым "Текстовый wrapping %w" ошибок
Такая же проблема, как описанная к пункту выше.
делать "Текстовый wrapping %w" ошибок
Так и есть в статье, плюс Custom Errors. stacktrace и backtrace я поставил как практики под "?, но для получения быстрого результата в легаси проектах этот подход приемлем.
ошибки старться обработать на месте, возвращать как можно меньше ошибок
Опишите примеры таких ошибок. В сервисах я встречал ошибки с валидацией данных, некорректность работой внешних ресурсов (БД, микросервис, шина данных) и это все тяжело пропустить. Но удобно обработать централизовано показывая ошибку, но скрывая чувствительную информацию от пользователю.
при старте приложения при ошибке сразу делать панику, после старта паники стараться не делать.
Спасибо большое за отзыв и предложенное обсуждение.
Полностью согласен с вами это идеальный вариант из книг. Но в своем проекте мы были готовы делать доменный слой правильным, для слоев выше мы разрешили себе делать отступления.
В DDD комьюнити использование транзакций на уровне приложение применимо. Подробное описание можно найти в Implementing Domain-driven Design, Глава 12. Repositories, Managing Transactions или в Patterns, Principles, and Practices of Domain-Driven Design, Глава 20. Repositories, Transaction Management and Units of Work.
Согласен, но каждый паттерн стоит применять по надобности, CQS, а тем более CQRS мне было бы очень тяжело продать бизнесу в рамках нашего проекта в виде стартапа.
Доменные события очень объемная тема, но без них проект может существовать достаточно долго, поэтому я не включил их в статью и доклад. В книги 2002 года Эрика Эванса доменные события не упоминаются и появились только в Domain-Driven Design Reference начале 10-х.
Поделюсь как бы я делал. При старте разработки, когда необходимости быстро получить готовый продукт и когда не понятно на сколько два счета для разных пользователей отличаются я бы делал их одной сущность. Затем отвечал бы себе на вопрос на сколько комфортно работать с единой сущностью в зависимости от:
Разные ли действия производят над сущностями пользователи и бухгалтеры?
Разные ли представления счетов для бухгалтерии и пользователя?
У нас несколько тех/бизнесовых команд работает над этой системой?
Нужно ли нам горизонтально масштабироваться по производительности?
Чем больше, тем больше вероятность того, что их нужно делить на две.
Если под маппингом имеется в виду Домен <-> Слой приложение (Handler, Endpoint, Controller, RPC, CLI-command и другое), То слой приложение будет отдавать ответ в нужном виде и формате (json, xml и др.) или вы предлагаете что данные из Домена можно сразу в json положить и отдать пользователю?
Если я неправильно поняла о каком именно маппинге говорится, уточните пожалуйста)
Как вариант, но хочу заметит в InTx(ctx context.Context, f func(userRepo, orderRepo, otherRepos...)) будут класться все репозитории проекта. В примере не увидел, что tx сохраняется в контекст, поэтому могут возникнуть сложности при работе с вложенными сценариями.
Кладу интерфейс в домен по примеру Из синей книжки, Implementing Domain-Driven Designпример, Principles, Practices and Patterns of Domain-Driven Design, и примеру от ThreeDots. А вот почему интерфейс лежит в домене - отличный вопрос. Могу предположить интерфейс таким образом явно обозначениет, что мы можем делать с моделью не собирая общую картину изучая интерфейсы с одним методом в разных местах. Плюс в Java интерфейсы работают по другому, нам нужно указать интерфес явно при реализации класса.
Замечу, что реализация репозитория лежит на уроне пирложения, инфраструктуры или адаптера, в зависимости от вами выбранной способа разбиения на слои.
Тем не менее общий интерфейс в домене не мешает использовать небольшие интерфейсы для каждого места использования. UserRepo свободно конвертируется в интерфейс рядом с местом использования.
// В домене
type UserRepo interface {
GetByID(...)
Save(...)
}
// В месте использования
type userGetter interface {
GetByID(...)
}
По второй части вопроса. Книгу "Чистая Архитектура" читал давно и точно сопоставить слои с ней я не смогу, но слово usecase используется не только применительно к Чистой Архитектуре. У нас в приложение usecase находятся на application уровне. Можно попытаться привязать наши usecase к концепту command из CQRS или Application Service из DDD. usecase в данном случае точно, не контроллер, т.к. входными данными является узкие DTO, а не http.Request, не данные из cli и не gRPC.
Для таких случаев используем паттерн transaction outbox. Он помогает избежать ситуации когда данные сохранились, но сообщение не отправилось в очередь. Или наоборот - сообщение отправилось, а данные не сохранились.
Паттерн использует транзакцию для атомарного сохранения в БД обновляемых данных и сообщений для очереди. После этого сообщение вычитывается из БД и отправляется в очередь.
Статья получилось длинной соглашусь, спасибо что прочитали ее всю.
В статье я не говорю как должно быть, а показываю когда и в каких условиях нужно использовать тот или иной подход.
Дополнения к вашему варианту "Как должно быть:"
Верно, в статье также говорится, но есть slog и стандартный пакет log, которые при определенных условиях можно применять. Примеры: net/http, gin
Верно, в статье также говорится, плюс я говори еще про Custom Errors, который широко используется. os.PathError
Верно, в статье также говорится, но есть исключения описанные в статье. Примеры из stdlib найдены через поиск
_ =: math, strconv.Верно, но есть исключения описанные в статье. Примеры из stdlib: strings, strconv.
Зачему, что исключения я описывал в статье и в самих регламентах они помечены "?" - применять при определенных условиях.
Логирование по месту захламляет лог дублями и усложняет понимания реального количества ошибок, поэтому не могу советовать этого.
Такая же проблема, как описанная к пункту выше.
Так и есть в статье, плюс Custom Errors. stacktrace и backtrace я поставил как практики под "?, но для получения быстрого результата в легаси проектах этот подход приемлем.
Опишите примеры таких ошибок. В сервисах я встречал ошибки с валидацией данных, некорректность работой внешних ресурсов (БД, микросервис, шина данных) и это все тяжело пропустить. Но удобно обработать централизовано показывая ошибку, но скрывая чувствительную информацию от пользователю.
Про паники так и указано в статье.
Спасибо большое за отзыв и предложенное обсуждение.
Полностью согласен с вами это идеальный вариант из книг. Но в своем проекте мы были готовы делать доменный слой правильным, для слоев выше мы разрешили себе делать отступления.
В DDD комьюнити использование транзакций на уровне приложение применимо. Подробное описание можно найти в Implementing Domain-driven Design, Глава 12. Repositories, Managing Transactions или в Patterns, Principles, and Practices of Domain-Driven Design, Глава 20. Repositories, Transaction Management and Units of Work.
Согласен, но каждый паттерн стоит применять по надобности, CQS, а тем более CQRS мне было бы очень тяжело продать бизнесу в рамках нашего проекта в виде стартапа.
Доменные события очень объемная тема, но без них проект может существовать достаточно долго, поэтому я не включил их в статью и доклад. В книги 2002 года Эрика Эванса доменные события не упоминаются и появились только в Domain-Driven Design Reference начале 10-х.
Рад, что event storming вам так зашел.
Простой ответ: если два разных контекст - значит две разные сущности.
Но согласен с @Yakamoz, нужно больше вводных.
Поделюсь как бы я делал. При старте разработки, когда необходимости быстро получить готовый продукт и когда не понятно на сколько два счета для разных пользователей отличаются я бы делал их одной сущность.
Затем отвечал бы себе на вопрос на сколько комфортно работать с единой сущностью в зависимости от:
Разные ли действия производят над сущностями пользователи и бухгалтеры?
Разные ли представления счетов для бухгалтерии и пользователя?
У нас несколько тех/бизнесовых команд работает над этой системой?
Нужно ли нам горизонтально масштабироваться по производительности?
Чем больше, тем больше вероятность того, что их нужно делить на две.
Если под маппингом имеется в виду Домен <-> Слой приложение (Handler, Endpoint, Controller, RPC, CLI-command и другое),
То слой приложение будет отдавать ответ в нужном виде и формате (json, xml и др.) или вы предлагаете что данные из Домена можно сразу в json положить и отдать пользователю?
Если я неправильно поняла о каком именно маппинге говорится, уточните пожалуйста)
Как вариант, но хочу заметит в
InTx(ctx context.Context, f func(userRepo, orderRepo, otherRepos...))будут класться все репозитории проекта.В примере не увидел, что tx сохраняется в контекст, поэтому могут возникнуть сложности при работе с вложенными сценариями.
Кладу интерфейс в домен по примеру Из синей книжки, Implementing Domain-Driven Designпример, Principles, Practices and Patterns of Domain-Driven Design, и примеру от ThreeDots.
А вот почему интерфейс лежит в домене - отличный вопрос. Могу предположить интерфейс таким образом явно обозначениет, что мы можем делать с моделью не собирая общую картину изучая интерфейсы с одним методом в разных местах.
Плюс в Java интерфейсы работают по другому, нам нужно указать интерфес явно при реализации класса.
Замечу, что реализация репозитория лежит на уроне пирложения, инфраструктуры или адаптера, в зависимости от вами выбранной способа разбиения на слои.
Тем не менее общий интерфейс в домене не мешает использовать небольшие интерфейсы для каждого места использования.
UserRepo свободно конвертируется в интерфейс рядом с местом использования.
По второй части вопроса.
Книгу "Чистая Архитектура" читал давно и точно сопоставить слои с ней я не смогу, но слово
usecaseиспользуется не только применительно к Чистой Архитектуре.У нас в приложение
usecaseнаходятся на application уровне. Можно попытаться привязать нашиusecaseк концептуcommandиз CQRS или Application Service из DDD.usecaseв данном случае точно, не контроллер, т.к. входными данными является узкие DTO, а не http.Request, не данные из cli и не gRPC.Для таких случаев используем паттерн transaction outbox. Он помогает избежать ситуации когда данные сохранились, но сообщение не отправилось в очередь. Или наоборот - сообщение отправилось, а данные не сохранились.
Паттерн использует транзакцию для атомарного сохранения в БД обновляемых данных и сообщений для очереди. После этого сообщение вычитывается из БД и отправляется в очередь.