Шаблон Чистой Архитектурой для приложений на Golang
Цель этого шаблона - показать принципы Чистой Архитектуры Роберта Мартина (дядюшки Боба):
- как структурировать проект и не дать ему превратиться в спагетти-код
- где хранить бизнес-логику, что бы она оставалась независимой, чистой и расширяемой
- как не потерять контроль при росте проекта
Go-clean-template создан и поддерживается Evrone.
Локальная разработка:
# Postgres, RabbitMQ
$ make compose-up
# Запуск приложения и миграций
$ make run
Интеграционные тесты (может быть использовано с CI):
# DB, app + migrations, integration tests
$ make compose-up-integration-test
Инициализация конфигурации и логгера. Здесь вызывается основаная часть приложения из internal/app/app.go
.
Конфигурация. Сначала читается config.yml
, а затем переменные окружения.
Переменные окружения имеют больший приоритет, чем конфигурация из yaml файла
и при совпадении ключа, значение берется из переменных окружения.
Структура конфигурации объявлена в файле config.go
.
Тег env-required: true
указывает, что значение обязательно. (либо в yaml, либо в переменных среды).
Для конфигурации мы используем библиотеку cleanenv. Она простая и соответствует всем нашим требования.
Чтение конфигурации из yaml файла противоречит идеологии Twelve-Factor App
, но на практике это более удобно,
чем получение значений из ENV.
Предполагается, что в yaml указываются значения по умолчанию, а переменные, чувствительные к безопасности
задаются в переменных ENV.
Документация Swagger. Генерируется автоматически с помощью библиотеки swag. Вам не нужно ничего редактировать вручную.
Интеграционные тесты. Они запускаются в отдельном контейнере, рядом с контейнером приложения. Rest API удобно тестировать с помощью go-hit.
Здесь находится только одна Run функция. Она размещена в файле app.go
и является логическим продолжением функции main.
Здесь создаются все основные объекты. Внедрение Зависимостей происходит через конструктор "New ...". Эта позволяет слоировать приложение, делая бизнес-логику независимой от других слоев.
Далее, запускается сервер и ожидается сигнал в select для корректного завершения работы.
Если app.go
стал слишком большим, вы можете разделить его на несколько файлов.
Если зависимостей много, то для удобства можно использовать wire.
Файл migrate.go
испольузется для автоматической миграции базы данных.
Он включается в компиляцию только при указании тега migrate.
Пример:
$ go run -tags migrate ./cmd/app
Слой хэндлеров сервера (MVC контроллеры). В шаблоне показана работа 2х серверов:
- RPC (RabbitMQ as transport)
- REST http (Gin framework)
Маршрутизаторы http сервера пишутся в едином стиле:
- Хэндлеры сгруппируются по области применения (по общему критерию)
- Для каждый группы создается свой маршрутизатор
- Объект бизнес-логики передается в маршрутизатор, что бы быть доступным внутри хэндлеров
Папка, как простой способ версионировать REST API.
Для создания версии v2, нужно создать папку http/v2
с таким же содержимым.
Добавить в файл internal/app/app.go
строки:
handler := gin.New()
v1.NewRouter(handler, t)
v2.NewRouter(handler, t)
Вместо Gin можно использовать любой другой http фреймворк или стандартную net/http
библиотеку.
В файле v1/router.go
над хэндлером написаны комментарии для генерации документации через swagger swag.
Сущности бизнес-логики (модели). Могут быть использованы в любом слое. Также они могуть иметь методы, например, для валидации.
Бизнес-логика.
- Методы сгруппированы по области применения (по общему критерию)
- У каждой группы своя отдельная структура
- Один файл - одна структура
Репозитории, webapi, rpc и другие структуры передаются в слой бизнес-логики в связующем файле internal/app/app.go
(смотрите Внедрение Зависимостей).
Репозиторий — это абстрактное хранилище (база данных), с которым взаимодействует бизнес-логика.
Это абстрактное web API, с которым взаимодействует бизнес-логика. Например, это может быть внешний микросервис, к которому бизнес-логика обращается через REST API. Название пакета выбирается таким, что бы соответствовать его назначению.
RabbitMQ RPC паттерн:
- Внутри RabbitMQ не используется маршрутизация
- Используется fanout-обмен, к которому привязана одна эксклюзивная очередь - это наиболее производительная конфигурация
- Переподключение при потере соединения
Для устранения зависимости бизнес-логики от внешних пакетов используется внедрение зависимостей.
Например, через конструктор "New" внедряется репозиторий в слой бизнес-логики.
Это делает бизнес-логику независимой и переносимой.
Мы можем переписать реализацию интерфейса репозитория не внося изменения в пакет бизнес-логики usecase
.
package usecase
import (
// Nothing!
)
type Repository interface {
Get()
}
type UseCase struct {
repo Repository
}
func New(r Repository) *UseCase{
return &UseCase{
repo: r,
}
}
func (uc *UseCase) Do() {
uc.repo.Get()
}
Благодаря разделению через интерфейсы можно генерировать моки (например, используя mockery) и легко писать юнит-тесты.
Мы не привязаны к конкретным реализациям и всегда можем заменить один компонент на другой. Если новый компонент реализует интерфейс, то в бизнес-логике ничего не нужно менять.
Программисты создают оптимальную архитектуру приложения после написания основной части кода.
Хорошая архитектура позволяет отклыдывать изменение как можно дольше.
Инверсия зависимостей (та же, что и в SOLID) используется как принцип для внедрения зависимостей. Зависимости направлены от внешнего слоя к внутреннему. Благодаря этому бизнес-логика и сущности остаются независимыми от других частей системы.
Например, приложение можно разделить на два слоя - внутренний и внешний:
- Бизнес-логика (например, стандартная библиотека Go).
- Инструменты (базы данных, сервера, брокеры сообщений и другие библиотеки и фреймворки).
Внутренний слой с бизнес-логикой должен быть чистым. Он обязан:
- Не импортировать пакеты с внешних слоев.
- Использовать только стандартную библиотеку.
- Взаимодействовать с внешними слоями через интерфейсы (!).
Бизнес-логика не должна ничего знать о Postgres или о реализации web API. Бизнес-логика имеет интерфейс для взаимодейсвтия с абстрактной базой данных или абстрактным web API.
Внешний слой имеет ограничения:
- Компоненты этого слоя не могут знать друг о друге и взаимодействать напрямую. Обращение друг к другу происходит через внутренний слой - слой бизнес-логики.
- Вызовы во внутренний слой выполняются через интерфейсы (!).
- Данные передаются в формате, удобном для бизнес-логики (структуры хранятся в
internal/entity
).
Например, нужно обратиться к базе данных из HTTP хэндера (в слое контроллер).
База данных и HTTP находятся во внешнем слое. Они не знаю друг о друге ничего и не могут взаимодействовать напрямую.
Взаимодействие будет происходить через слой бизнес-логики usecase
:
HTTP > usecase
usecase > repository (Postgres)
usecase < repository (Postgres)
HTTP < usecase
Символы > и < показывают пересечения слоев через интерфейсы и направления. Это же показано на схеме:
Пример более сложного пути данных:
HTTP > usecase
usecase > repository
usecase < repository
usecase > webapi
usecase < webapi
usecase > RPC
usecase < RPC
usecase > repository
usecase < repository
HTTP < usecase
- Entities (сущности) - это структуры, с которыми работает бизнес логика.
Они располагаются в папке
internal/entity
. В терминологии MVC сущности - это модели. - Use Cases это бизнес-логика. Располагается в папке
internal/usecase
.
Слой, с которым бизнес-логика взаимодействует напрямую, обычно, называется инфраструктурным слоем.
Это может быть репозиторий internal/usecase/repo
, внешнее webapi internal/usecase/webapi
, любой пакет или микросервис.
В шаблоне пакеты infrastructure размещены внутри internal/usecase
.
Вы можете выбирать, как называть точки входа, по своему усмотрению. Варианты такие:
- controller (в нашем случае)
- delivery
- transport
- gateways
- entrypoints
- primary
- input
В классической версии Чистой Архитектуры для создания больших монолитных приложений предложено 4 слоя.
В исходной версии внешний слой делится на два, которые также имеют инверсию зависимостей в другие слои и взаимодействуют через интерфесы.
Внутренний слой также делится на два (с использованием интерфейсов) в случае сложной логики.
Сложные инструменты могут быть разделены на дополнительные слои. Однако добавлять слои следует только в том случае, если это действительно необходимо.
Кроме Чистой Архитектуры есть и другие подоходы:
- Луковая Архитектура
- Гексогональная (Порты и адаптеры также на неё похожа) Они обе основаны на на принципе инверсии зависимостей. Порты и адаптеры очень похожи на Чистую Архитектуру. Различия в основном заключаются в терминологии.