DDD архитектура на практике
26 y.o.
Введение
Начнем серию постов про разработку IT продукта с базовых определений, и сегодня поговорим про domain driven design. Придерживаясь именно этих принципов я буду строить свое приложение.
Раз уж так вышло, что я пишу на языке Go, то и архитектура будет применима к нему. Для других языков, принципы могут различаться, но я постарался передать общую концепцию.
Определение
DDD - domain driven design или предметно-ориентированное программирование. Это не технология, это набор принципов. DDD обязывает разработчика создавать абстракции, в которые входит бизнес логика. По сути это связующее звено между продуктом (бизнесом) и программным кодом.
Краткое описание
Полное описание DDD выходит далеко за рамки этой статьи, тут я всего лишь сжато пробегусь по основным моментам и понятиям.
Зачем нужен этот подход?
Такой подход ускоряет процесс проектирования ПО в незнакомой для разработчика области. В проектах, в которых сложность и запутанность бизнес-логики достаточно велика, DDD должен снизить эту сложность.
Domain
DDD — подход к разработке, основанный на выделении логики в domain (домены). Домен — предметная область, для которой ведется разработка. Например, у бухгалтеров есть свои термины и важные понятия, которые отличаются от терминов, используемых в банковской сфере или сфере общепита. Эти термины отражают важные для заказчика процессы, связи, сущности. Именно поэтому важно знать их и уделять внимание терминам, которые используются в данной сфере. Разработка не должна быть в отрыве от бизнес-задач.
Ubiquitous language
Вне DDD, когда программист пишет код, больше внимания он уделяет технологиям и инфраструктуре, например, как и в какую БД данные сохранить, обновить, получить, удалить, какую библиотеку использовать и т.д. В DDD все это вторично. Бизнес важнее. DDD учит разработчиков разговаривать с бизнесом на одном языке, на языке бизнеса. Это называется Ubiquitous language (единый язык)
Bounded Context
Bounded Context (ограниченный Контекст) — это некая граница, в которой существует модель предметной области. На основании контекста код разделяют на папки/файлы/пакеты/компоненты таким образом, чтобы изменения в каждом из них не оказывали (или оказывали минимальное) влияние на другие компоненты системы. Разработчикам это помогает эффективно работать в команде (один человек правит одну модель, второй параллельно другую. при слиянии изменений конфликтов быть не должно) и смелее вносить изменения в код, не боясь что где-то в другом месте, другая модель будет затронута/сломана.
Пример
Для примера, представим что у нас есть проект простого блога, в котором есть следующие сущности:
- Post (модель поста). Может быть черновиком. Может быть лайкнут. У поста могут быть реакции. Посты хранят в себе количество просмотров
- Author (модель пользователя). Хранит почту, имя, фамилию, позволяет их менять.
У наших сущностей будут бизнес методы (addLike, removeLike, addReaction, и т.д.), тогда мы смело можем сказать что принцип Ubiquitous language соблюден. И в разговоре с заказчиком, говоря слово post мы будем иметь именно нашу сущность с набором методов, а не пост из инстаграмма.
Для упрощения, представим что мы используем паттерн репозиторий (о нем я расскажу в отдельной статье). Примеры без привязки к языку, читайте их как псевдокод.
Есть несколько вариантов хранения кода:
1) По функциональному назначению. В файле usecase.file хранится все бизнес-логика для постов и для авторов, включая моки. В файле repo.file хранится вся логика, связанная с базой данных, для постов и для авторов, включая различные базы данных. В transport.file хранится вся транспортная логика для постов и авторов (включая разные транспорты: http hadnlers / grpc / graphql и т.д.). Минус такого подхода в том, что при растущем количестве сущностей, эти файлы станут очень большими, в них будет неудобно ориентироваться, и при командной разработке скорее всего будут конфликты слияния, когда несколько разработчиков внесут изменения в один файл.
usecase.file
repo.file
transport.file
2) Смесь функционального назначения и DDD. Тут мало того что для каждой сущности есть свой файл, так еще и файлы сгруппированы по своему логическому смыслу. usecase отдельно, transport отдельно, repo отдельно. Ориентироваться тут значительно проще. Шанс получить конфликт при слиянии веток намного меньше. Но это все еще не чистый DDD. Плюс файлы довольно объемные (например post_repo.file может хранить в себе реализации репозитория для mysql, sqlite, моки для тестов, кеш и много чего еще).
usecase
post_usecase.file
author_usecase.file
repo
post_repo.file
author_repo.file
transport
post_transport.file
author_transport.file
3) DDD. В данном примере файлы расположены в папках согласно DDD. Но тут могут возникнуть циклические зависимости. О них я расскажу позже, в отдельной статье. Плюс проблема с объемными файлами из прошлого пункта не решена
post
transport.file
usecase.file
repo.file
author
transport.file
usecase.file
repo.file
4) DDD + чистая архитектура. При таком расположении файлов и папок мы полностью соблюдаем DDD и избегаем циклических зависимостей. Я добавил несколько новых файлов в post, что бы показать зачем нужна такая группировка. У нас есть 2 домена: post и author. Эти сущности соблюдают контекст: изменения в author не затрагивают post и наоборот. Разработчики могут параллельно вносить в них изменения. Внутри каждой сущности есть подпапки, в которых по логике сгруппированы файлы: в post/transport можно положить все, что связано с транспортом для post (http, grpc, rest), в post/usecase кладем всю бизнес логику постов и ее моки для тестов, в post/repo будет вся логика для постов, которая касается баз данных и т.д.
post
transport
transport_http.file
transport_grpc.file
transport_graphql.file
usecase
usecase.file
usecase_mock.file
repo
repo_mysql.file
repo_sqlite.file
repo_mock.file
author
transport
transport_http.file
usecase
usecase.file
repo
repo_mysql.file
Заключение
Не стоит воспринимать эту статью как единственную верную истину. Все зависит от проекта. Я показал примерное расположение файлов и папок, которое я делаю в своих проектах, которое я считаю за DDD. Если у вас небольшой проект (~2000 строк кода), можно вообще не дробить его не файлы и написать все в одном (в языке go). Так же я упомянул чистую архитектуру. Эта тема выходит за рамки данной статьи, о ней пишут книги и бесконечно спорят в комментариях. Я постараюсь в рамках похожей статьи немного осветить чистую архитектуру.