diff --git a/README.md b/README.md index f1299d52..343c4769 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,27 @@ -Шаблон сервиса на Go -=========================================== -С развитием любого проекта, логика работы приложения усложняется и запутывается. -Разработка новых фич замедляется. Если не планировать архитектуру, довольно быстро -приходит время, когда переписать сервис становится дешевле, чем делать изменения в -существующем. - -Цели: -1. Предоставить готовый шаблон для быстрого развертывания -2. Стандартизировать подходы к организации кода -3. Аккумулировать лучшие практики - -Quick start ----- -Скопировать и переименовать файл `.env.example` в `.env`. +# Go service template +Clean Architecture template for Golang services + +[![Go Report Card](https://goreportcard.com/badge/github.com/evrone/go-service-template)](https://goreportcard.com/report/github.com/evrone/go-service-template) +![CircleCI](https://img.shields.io/circleci/build/github/evrone/go-service-template) +[![License](https://img.shields.io/github/license/evrone/go-service-template.svg)](https://github.com/evrone/go-service-template/blob/master/LICENSE) +[![Release](https://img.shields.io/github/v/release/evrone/go-service-template.svg)](https://github.com/evrone/go-service-template/releases/) + +## Overview +Цель шаблона показать: +- как организовать проект, чтобы он не превратился в spaghetti code +- где хранить бизнес-логику, чтобы она оставалась независимой, чистой и расширяемой +- как не терять контроль при разрастании микросервиса + +Используя принципы Роберта Мартина (aka Uncle Bob). + +## Content +- [Quick start](#quick-start) +- [Project structure](#project-structure) +- [Dependency Injection](#dependency-injection) +- [Clean Architecture](#clean-architecture) + +## Quick start Локальная разработка: ```sh # Postgres, RabbitMQ @@ -28,88 +36,112 @@ $ make run $ make compose-up-integration-test ``` -Clean Architecture ----- -### Главный принцип -Dependency Inversion (тот самый из SOLID) — принцип инверсии зависимостей, играет -ключевую роль в построении архитектуры приложения. Чтобы принцип начал работать, -нам нужно поделить приложение на слои. +## Project structure +### `cmd/app/main.go` +Инициализация конфигурации и логгера. Далее функция _main_ "продолжается" в +`internal/app/app.go`. -![Clean Architecture](docs/img/layers.png) +### `config` +Конфигурация. Сперва читается `config.yml`, далее переменные окружения (перезаписывают +yaml конфиг при совпадении). В `config.go` структуры конфига. Тег `env-required: true` +обязывает указать значение (или в yaml, или в переменных окружения). -Итак, приложение делится на 2 слоя, внутренний и внешний: -1. **Бизнес-логика** (стандартная библиотека Go). -2. **Инструменты** (база данных, HTTP сервер, брокер сообщений, любые другие пакеты -и фреймворки). +Для конфигурирования была выбрана библиотека [cleanenv](https://github.com/ilyakaznacheev/cleanenv), +которая имеет не так много звёзд на GitHub, однако проста и отвечает всем требованиям. -**Внутренний** слой с бизнес-логикой должен быть чистым, то есть: -- Не иметь в себе импортов пакетов из внешнего слоя. -- Не иметь в себе импортов любых других пакетов, кроме стандартной библиотеки. -- Делать обращения к внешнему слою через интерфейс(!). +Чтение конфига из yaml противоречит идеологии 12-ти факторов, однако на практике это более +удобно, чем чтение всего конфига из ENV. Подразумевается что в yaml находятся дефолтные +значения, а в ENV определяются чувствительные к безопасности переменные. -Бизнес-логика ничего не знает о Postgres или RabbitMQ. Бизнес-логика знает -интерфейс для работы с _абстрактной_ базой данных, _абстрактным_ брокером -сообщений или _абстрактным_ web api. +### `docs` +Swagger документация. Авто сгенерированная библиотекой [swag](https://github.com/swaggo/swag). +Править руками не нужно. -**Внешний** слой имеет другие ограничения: -- Все компоненты этого слоя не знают о существовании друг друга. -- Как из одного инструмента вызвать другой? Напрямую никак, только через -внутренний слой логики. -- Обращения к внутреннему слою происходят через интерфейс(!). -- Данные передаются в том формате, который удобен для бизнес-логики. +### `integration-test` +Интеграционные тесты. Запускаются в виде отдельного контейнера, рядом с контейнером +приложения. Rest API удобно тестировать с помощью [go-hit](https://github.com/Eun/go-hit) +(жаль не так популярна, библиотека топчик). -**Например**, вам нужно обратиться из http хендлера к базе данных. И http, и -БД находятся во внешнем слое, значит они ничего не знают друг о друге. Связь -между ними осуществляется через `service` (бизнес-логику): +### `internal/app` +В файле `app.go` всегда одна функция _Run_, которая "продолжает" функцию _main_. + +Здесь происходит создание всех основных объектов. Через конструкторы "New..." происходит +внедрение зависимостей (см. [Dependency Injection](#dependency-injection)). +Этот приём позволяет нам разделить приложение на слои, придерживаясь принципа инверсии +зависимостей (Dependency Inversion). Благодаря этому бизнес-логика становится независимой +от других слоев. + +Далее мы запускаем сервер и ожидаем в _select_ сигналы для graceful завершения. + +Если `app.go` начнет разрастаться, можно разделить его не несколько файлов. + +При большом количестве инъекций, можно использовать [wire](https://github.com/google/wire). + +Файл `migrate.go` используется для авто миграций БД. Он подключается если указан аргумент с +тегом _migrate_. Например: +```sh +$ go run -tags migrate ./cmd/app +``` + +### `internal/delivery` +Слой с обработчиками серверов (контроллеры по MVC). В шаблоне показаны 2 сервера: +- RPC (RabbitMQ в качестве транспорта) +- REST http (Gin фреймворк) + +Роутеры серверов написаны в одном стиле: +- Обработчики группируются по области применения (по общему признаку) +- Для каждой группы создаётся своя структура-роутер, методы которой обрабатывают пути +- В структуру-роутер инжектится структура бизнес-логики, которую будут вызывать обработчики + +#### `internal/delivery/http` +Простое версионирование REST. Для версии v2 потребуется добавить папку `http/v2` с +тем же содержанием. И в файле `internal/app` добавить строку: ``` - HTTP (delivery) > service - service > Postrges (repo) - service < Postrges (repo) - HTTP (delivery) < service +handler := gin.New() +v1.NewRouter(handler, translationService) +v2.NewRouter(handler, translationService) ``` -Стрелочками > и < показано пересечение границ слоев через Интерфейсы. +Вместо Gin можно использовать любой другой http фреймворк или даже стандартную `net/http` +библиотеку. -То же самое на картинке: +В `v1/ruoter.go` и над методами обработчиков присутствуют комментарии для генерации +swagger документации с помощью [swag](https://github.com/swaggo/swag). -![Example](docs/img/example-http-db.png) +### `internal/domain` +Сущности бизнес-логики (модели), их можно использовать в любых слоях. +Так же здесь могут быть методы, например для валидации. -### Терминология Чистой Архитектуры -- **Entities** — объекты, которыми оперирует бизнес-логика. В коде называются -по именам принятым в организации. Находятся в папке `internal/domain`. Domain -намекает на то что мы придерживаемся принципов DDD (domain driven design), это -отчасти так, но без фанатизма. В терминах MVC entity это модели. Удобно когда -модели называются по именам предметной области. +### `internal/service` +Бизнес-логика. +- Методы группируются по области применения (по общему признаку) +- Для каждой группы — своя структура +- В одном файле — одна структура +В структуры бизнес-логики инжектятся (см. [Dependency Injection](#dependency-injection)) +репозитории, webapi, rpc, другие структуры бизнес-логики. -- **Use Cases** — это бизнес-логика, манипулирующая внешними пакетами через -интерфейсы. Находится в папке `internal/service`. Называть бизнес-логику словом -_service_ не очень идеоматично с точки зрения Чистой Архитектуры, но слово -_service_ удобнее использовать для названия пакета, чем _usecase_. +#### `internal/service/repo` +Репозиторий — это абстрактное хранилище (база данных), с которой работает бизнес-логика. -Слой с которым непосредственно взаимодействует бизнес-логика обычно называют -инфраструктурным слоем - _infrastructure_. Это могут быть репозитории -`internal/service/repo`, внешние webapi `internal/service/webapi`, любые pkg -и другие -микросервисы. В шаблоне инфраструктурные пакеты находятся внутри каталога -`internal/service`. +#### `internal/service/webapi` +Это абстрактный web api, с которым работает бизнес-логика. Например, это может быть другой +микросервис, к которому по REST API обращается бизнес-логика. Название пакета меняется в +зависимости от назначения. -Как называть точки входа, вопрос открыт. Варианты: -- delivery -- transport -- controllers -- adaptors -- gateways -- input -- entry_points -- primary +### `pkg/rabbitmq` +RPC паттерн RabbitMQ: +- Внутри RabbitMQ нет никакой маршрутизации +- Используются Exchange fanout, к которому биндится 1 exclusive queue, это самый +производительный конфиг +- Реконнект при потере соединения -### Dependency Injection +## Dependency Injection Для того чтобы убрать зависимость бизнес-логики от внешних пакетов, используется инъекция зависимостей. -Через конструктор NewService мы делаем инъекцию зависимости в структуру бизнес-логики. -Таким образом бизнес-логика становится независимой (и даже переносимой). Мы можем -подменить реализацию интерфейса и при этом не вносить правки в пакет `service`. +Например, через конструктор NewService мы делаем инъекцию зависимости в структуру +бизнес-логики. Таким образом бизнес-логика становится независимой (и переносимой). +Мы можем подменить реализацию интерфейса и при этом не вносить правки в пакет `service`. ```go package service @@ -134,12 +166,102 @@ func (s *Service) Do() { s.repo.Get() } ``` -Это так же позволит нам делать автогенерацию моков (например с помощью библиотеки -_mockery_) и легко писать юнит-тесты. +Это так же позволит нам делать автогенерацию моков (например с помощью [mockery](https://github.com/vektra/mockery)) +и легко писать юнит-тесты. + +> Мы не привязываемся к конкретным реализациям, чтобы всегда иметь возможность +> безболезненно менять один компонент на другой. Если новый компонент будет +> реализовывать интерфейс, в бизнес-логике менять ничего не потребуется. + +## Clean Architecture +### Ключевая идея +Программисты осознают оптимальную архитектуру приложения уже после того как большая часть +кода была написана. + +> Хорошая архитектура позволяет оттягивать принятие решений на как можно позднее время. + +### Главный принцип +Dependency Inversion (тот самый из SOLID) — принцип инверсии зависимостей. Направление +зависимостей идет из внешнего слоя во внутренний. Благодаря чему бизнес-логика и сущности +остаются независимыми от других частей системы. + +Итак, приложение делится на 2 слоя, внутренний и внешний: +1. **Бизнес-логика** (стандартная библиотека Go). +2. **Инструменты** (базы данных, серверы, брокеры сообщений, любые другие пакеты +и фреймворки). + +![Clean Architecture](docs/img/layers.png) + +**Внутренний** слой с бизнес-логикой должен быть чистым, то есть: +- Не иметь в себе импортов пакетов из внешнего слоя. +- Использовать возможности лишь стандартной библиотеки. +- Делать обращения к внешнему слою через интерфейс(!). + +Бизнес-логика ничего не знает о Postgres или конкретном web api. Бизнес-логика имеет +интерфейс для работы с _абстрактной_ базой данных или _абстрактным_ web api. + +**Внешний** слой имеет другие ограничения: +- Все компоненты этого слоя не знают о существовании друг друга. Как из одного инструмента +вызвать другой? Напрямую никак, только через внутренний слой бизнес-логики. +- Все обращения к внутреннему слою происходят через интерфейс(!). +- Данные передаются в том формате, который удобен для бизнес-логики (`internal/domain`). + +**Например**, нужно обратиться из HTTP (контроллера) к базе данных. И HTTP, и +БД находятся во внешнем слое, значит они ничего не знают друг о друге. Связь +между ними осуществляется через `service` (бизнес-логику): +``` + HTTP > service + service > repository (Postgres) + service < repository (Postgres) + HTTP < service +``` +Стрелочками > и < показано пересечение границ слоев через Интерфейсы. + +То же самое на картинке: + +![Example](docs/img/example-http-db.png) + +Или более сложная бизнес-логика: +``` + HTTP > service + service > repository + service < repository + service > webapi + service < webapi + service > RPC + service < RPC + service > repository + service < repository + HTTP < service +``` + +### Терминология Чистой Архитектуры +- **Entities** — структуры, которыми оперирует бизнес-логика. Находятся в папке +`internal/domain`. Domain намекает на то что мы придерживаемся принципов DDD (domain +driven design), это отчасти так, но без фанатизма. В терминах MVC, entities - это модели. + +- **Use Cases** — это бизнес-логика, находится в `internal/service`. Называть бизнес-логику +словом _service_ не очень идеоматично с точки зрения Чистой Архитектуры, но одно слово +_service_ удобнее использовать для названия пакета, чем два: _use case_. + +Слой с которым непосредственно взаимодействует бизнес-логика обычно называют +инфраструктурным слоем - _infrastructure_. Это могут быть репозитории +`internal/service/repo`, внешние webapi `internal/service/webapi`, любые pkg, другие +микросервисы. В шаблоне инфраструктурные пакеты находятся внутри `internal/service`. + +Как называть точки входа, вопрос открыт. Можно выбрать на свой вкус. Варианты: +- delivery (в нашем случае) +- controllers +- transport +- adaptors +- gateways +- entrypoints +- primary +- input ### Дополнительные слои Классический вариант [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) -разрабатывался для построения больших монолитных приложений и имеет 4 слоя абстракций. +разрабатывался для построения больших монолитных приложений и имеет 4 слоя. То есть, в оригинальной версии, внешний слой делится ещё на два, которые так же имеют обратную инверсию зависимостей друг к другу (направленную во внутрь) @@ -149,18 +271,21 @@ _mockery_) и легко писать юнит-тесты. сложной логики. _______________________________ -Сложные инструменты из внешнего слоя рекомендуется делить на дополнительные -слои абстракции. Следует руководствоваться здравым смыслом и добавлять слои -лишь в том случае, если это действительно необходимо. +Сложные инструменты можно делить на дополнительные слои. Однако, следует +руководствоваться здравым смыслом и добавлять слои лишь в том случае, если это +действительно необходимо. ### Альтернативные подходы Кроме Чистой архитектуры есть очень близкие по духу — Луковая архитектура и -Гексагональная (Порты и адаптеры). В основе всех подходов лежит базовый принцип -инверсии зависимостей. Все подходы преследуют цель уменьшить зацепление и -разграничить ответственность. +Гексагональная (Порты и адаптеры). В основе обеих лежит базовый принцип +инверсии зависимостей. _Порты и адаптеры_ очень близка к _Чистой Архитектуре_, различия +в основном в терминологии. + +## Похожие проекты +- [https://github.com/bxcodec/go-clean-arch](https://github.com/bxcodec/go-clean-arch) +- [https://github.com/zhashkevych/courses-backend](https://github.com/zhashkevych/courses-backend) -Полезные ссылки ---------------- -- [The Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) +## Полезные ссылки +- [Статья The Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) - [Книга Чистая архитектура](https://www.ozon.ru/context/detail/id/144499396/) - [12 факторов](https://12factor.net/ru/) \ No newline at end of file diff --git a/integration-test/Dockerfile b/integration-test/Dockerfile index c45620a1..85619909 100644 --- a/integration-test/Dockerfile +++ b/integration-test/Dockerfile @@ -4,8 +4,8 @@ COPY go.mod go.sum /modules/ WORKDIR /modules RUN go mod download -# Step 2: Builder -FROM golang:1.16.2-alpine3.13 as builder +# Step 2: Tests +FROM golang:1.16.2-alpine3.13 COPY --from=modules /go/pkg /go/pkg COPY . /app WORKDIR /app diff --git a/internal/delivery/amqp_rpc/translation.go b/internal/delivery/amqp_rpc/translation.go index 1b3ba875..2d6d04b5 100644 --- a/internal/delivery/amqp_rpc/translation.go +++ b/internal/delivery/amqp_rpc/translation.go @@ -28,7 +28,7 @@ func (r *translationRoutes) getHistory() server.CallHandler { return func(d *amqp.Delivery) (interface{}, error) { translations, err := r.translationService.History() if err != nil { - return nil, errors.Wrap(err, "amqp_rpc - router - getHistory - r.translationService.History") + return nil, errors.Wrap(err, "amqp_rpc - translationRoutes - getHistory - r.translationService.History") } response := historyResponse{translations}