Пример структуры приложения

3 minute read

Вместо предисловия

Ниже опишу подход к структуре кода, который использую с 2020-го года. Подход не нов - он объединяет проектирование по предметной области и использование слоев. Он способствует сохранению динамики развития приложения с течением времени, высокой тестируемости и гибкости, снижает порог входа и позволяет откладывать момент принятия решений.

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

TLDR

  • endpoints # точки входа в приложение. Помогает соседнему разработчику найти как приложение использует данные, которые он ему поставляет
    • http # обработка http запросов. Группируем по акторам, чтобы избежать смешивания контрактов
      • ControlPlaneController # обработчики запросов от панели управления
        • SomeRequest, SomeResponse, SomeError различные DTO для принятия запроса
      • StatsController # обработчики запросов от отдела статистики
    • consumer # обработка асинхронных сообщений
    • schedule # обработка запросов из расписания или очереди
  • infrastructure # фреймворк и реализации требований предметной области. Здесь прячем техническую сложность
    • authentication # разбор токена аутентификации в объект
    • events # система распространения предметных событий
      • SimpleObserver : DomainEventObserver # обозреватель событий запускает обработчики которые должны быть целостными с событием
      • TransactionalOutbox # контракт, целостно отправляет предметные события в интеграционные системы
    • storage # данные. Часто используются DataMapper или Repository
      • http # данные из внешних источников по http
      • postgresql # данные из postgresql
    • schedule # система обработки задач по расписанию или очереди
      • ScheduleOutbox : TransactionalOutbox # интеграция через систему расписаний. Используется для взаимодействия между контекстами
    • messages # транспорт через систему асинхронных сообщений, например Kafka
      • KafkaOutbox : TransactionalOutbox # интеграция через асинхронные сообщения. Используется для уведомления внешних систем
  • domain # логика предметной области. В каждом приложении своя. Здесь используем единый с бизнесом язык
    • showcase # логика(контекст) витрины
      • AddProductToCart # сценарий добавления продукта в корзину. Хороший сценарий читается бизнес-аналитиком
        • Request,Error DTO для запуска сценария, может содержать только простейшие типы (скаляры, встроенные типы)
      • contracts # контракты систем, которые необходимы для выполнения бизнес процессов(кейсов/сценариев)
        • ProductsStorage # контракт хранилища товаров
        • Order # сущность заказа. Контролирует инварианты
    • controlplane # логика(контекст) панели управления
      • authorization # авторизация аутентифицированных пользователей
      • contracts
        • Order # тоже сущность заказа, но заказ для витрины отличается от заказа для панели управления
    • events # события которые жизненно важны бизнесу
      • DomainEvent, DomainEventObserver, DomainEventProvider контракты системы распространения предметных событий
      • ProductAddedToCart
      • OrderConfirmed
      • OrderShipped
  • Application # запуск приложения

code structure actors

Словарик

  • Соседний - коллега, который не знаком с предметной областью или кодом приложения

Разделяй и властвуй

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

Не следует множить сущее без необходимости

“Бритва Оккама” в подходе позволяет откладывать момент принятия решений о дизайне до появления явных требований. В предметной области нам просто дизайн не нужен, поэтому просто садимся и пишем код. В процессе написания предметной области появляются контракты(требования), которые явно описывают что есть на входе и нужно получить на выходе. Еще более ярко бритва раскрывается, когда тесты пишутся прежде реализации.

Конвей

Следуя закону Конвея, разделяем акторов в разных частях приложения: предметную область пишем так, чтобы ее мог прочитать бизнес-аналитик; точки входа так, чтобы зашедший “поиннерсорсить” соседний разработчик понял, где его обработчики; а технические реализации можем спокойно группировать чтобы, например, работа с одной таблицей была в одном месте.

Своя рубашка ближе к телу

Держите связанные данные и поведение рядом

Best practices

  • Наиболее удобным образом показали себя кейсы из 3-х этапов: получить данные; выполнить предметную логику; отправить данные. Их просто читать, просто писать и все еще просто тестировать.
  • Кейс, запускаемый простыми данными(скаляры, встроенные типы) удобно запускать из разных точек входа. Пример: отмена заказа вручную пользователем или автоматически спустя время. Кроме того так мы проверяем себя, что не размазали логику по нескольким классам.

Что еще не решил

  • Как соседнему разработчику узнать как именно формируются данные, которые он получает от приложения