Пример структуры приложения
Вместо предисловия
Ниже опишу подход к структуре кода, который использую с 2020-го года. Подход не нов - он объединяет проектирование по предметной области и использование слоев. Он способствует сохранению динамики развития приложения с течением времени, высокой тестируемости и гибкости, снижает порог входа и позволяет откладывать момент принятия решений.
Ниже пример использования подхода - не стоит принимать его за фреймворк. В основе подхода - ряд принципов и целей, а как их реализовать - выбор реализующего. В качестве примера использую серверное приложение - в нем больше сценариев использования, что позволяет отразить больше примеров.
TLDR
endpoints
# точки входа в приложение. Помогает соседнему разработчику найти как приложение использует данные, которые он ему поставляетhttp
# обработка http запросов. Группируем по акторам, чтобы избежать смешивания контрактовControlPlaneController
# обработчики запросов от панели управленияSomeRequest
,SomeResponse
,SomeError
различные DTO для принятия запроса
StatsController
# обработчики запросов от отдела статистики
consumer
# обработка асинхронных сообщенийschedule
# обработка запросов из расписания или очереди
infrastructure
/internal
# фреймворк и реализации требований предметной области. Здесь прячем техническую сложность.authentication
# разбор токена аутентификации в объектevents
# система распространения предметных событийSimpleObserver : DomainEventObserver
# обозреватель событий запускает обработчики которые должны быть целостными с событиемTransactionalOutbox
# контракт, целостно отправляет предметные события в интеграционные системы
storage
# данные. Часто используются DataMapper или Repository.http
# данные из внешних источников по httppostgresql
# данные из postgresqlProductsRepository : domain.showcase.contracts.ProductsStorage
schedule
# система обработки задач по расписанию или очередиScheduleOutbox : TransactionalOutbox
# интеграция через систему расписаний. Используется для взаимодействия между контекстами
kafka
# транспорт через систему асинхронных сообщений, например KafkaKafkaOutbox : TransactionalOutbox
# интеграция через асинхронные сообщения. Используется для уведомления внешних систем
domain
# логика предметной области. В каждом приложении своя. Здесь используем единый с бизнесом языкshowcase
# логика(контекст) витриныAddProductToCart
# сценарий добавления продукта в корзину. Хороший сценарий читается бизнес-аналитикомRequest
,Error
DTO для запуска сценария, может содержать только простейшие типы (скаляры, встроенные типы)
contracts
# контракты систем, которые необходимы для выполнения бизнес процессов(кейсов/сценариев)ProductsStorage
# контракт хранилища товаровOrder
# сущность заказа. Контролирует инварианты
controlplane
# логика(контекст) панели управленияauthorization
# авторизация аутентифицированных пользователейcontracts
Order
# тоже сущность заказа, но заказ для витрины отличается от заказа для панели управления
events
# события которые жизненно важны бизнесуDomainEvent
,DomainEventObserver
,DomainEventProvider
контракты системы распространения предметных событийProductAddedToCart
OrderConfirmed
OrderShipped
Application
# запуск приложения
Словарик
- Соседний - коллега, который не знаком с предметной областью или кодом приложения
Разделяй и властвуй
Одна из задач разработчика пишущего в команде - писать так, что было легко читать коллегам. Один из способов снизить сложность написанного - снижать размер контекста необходимого для понимания. Поэтому один из основных принципов подхода - разделять, технический код от предметного, предметные области друг от друга, потребителей друг от друга. За счет разделения потребителей упростим параллельную работу снижением количества конфликтов.
Не следует множить сущее без необходимости
“Бритва Оккама” в подходе позволяет откладывать момент принятия решений о дизайне до появления явных требований. В предметной области нам просто дизайн не нужен, поэтому просто садимся и пишем код. В процессе написания предметной области появляются контракты(требования), которые явно описывают что есть на входе и нужно получить на выходе. Еще более ярко бритва раскрывается, когда тесты пишутся прежде реализации.
Конвей
Следуя закону Конвея, разделяем акторов в разных частях приложения: предметную область пишем так, чтобы ее мог прочитать бизнес-аналитик; точки входа так, чтобы зашедший “поиннерсорсить” соседний разработчик понял, где его обработчики; а технические реализации можем спокойно группировать чтобы, например, работа с одной таблицей была в одном месте.
Своя рубашка ближе к телу
Держите связанные данные и поведение рядом
Best practices
- Наиболее удобным образом показали себя кейсы из 3-х этапов: получить данные; выполнить предметную логику; отправить данные. Их просто читать, просто писать и все еще просто тестировать.
- Кейс, запускаемый простыми данными(скаляры, встроенные типы) удобно запускать из разных точек входа. Пример: отмена заказа вручную пользователем или автоматически спустя время. Кроме того так мы проверяем себя, что не размазали логику по нескольким классам.
Что еще не решил
- Как соседнему разработчику узнать как именно формируются данные, которые он получает от приложения