План
- Вводная
- Бизнес-требования
- Требования к архитектуре
- Требования к веб-фреймворку
- Выбор веб-фреймворка
- Начало работы над проектом
Вводная
Всем привет, сегодня мы начинаем серию статей о том, как разработать коммерческое веб-приложение с нуля. В этом материале будут продемонстрированы решения, используемые командой разработки подписки «Огонь» Ogon.ru.
Прежде чем начать писать код, давайте поймем, что мы хотим получить в итоге. Для этого составим бизнес-требования и технические требования.
Бизнес-требования
В рамках серии статей мы разработаем интернет-магазин. Давайте разложим функционал магазина на несколько простых пользовательских историй. Я, как пользователь сайта интернет-магазина, могу:
- Зарегистрироваться на сайте.
- Авторизоваться на сайте.
- Посмотреть список товаров.
- Посмотреть карточку товара.
- Положить товар в корзину.
- Управлять товарами в корзине.
- Оплатить товары в корзине.
- Получить чек на почту.
Требования к архитектуре
Как мы видим, весь функционал раскладывается на три предметные области или домена:
- Пользователи.
- Товары.
- Заказы.
Разделение всего функционала на домены является одним из принципов DDD. Разделять проект на домены хорошо, потому что:
- Домен описывается набором сущностей, характерных для него.
- Домен имеет набор сервисов, которые управляют этими сущностями.
- Домены общаются между собой через сервисы.
Архитектура, разработанная на принципах DDD, имеет очевидные преимущества:
- Бизнес-логика пишется один раз внутри домена и переиспользуется другими. Значит изменение логики либо не затронет остальные части проекта, либо будут дешевле с точки зрения времени и денег.
- Все домены имеют схожую структуру, значит разработчикам будет легче переключаться между доменами и разбираться, что там происходит.
Таким образом, мы получаем проект, который проще разрабатывать и тестировать потому, что каждый домен является центром силы, раз он делает только свою часть работы. Исходя из того, что мы выделили три домена, нам необходимо реализовать три микросервиса:
- Пользователи.
- Товары.
- Заказы.
Команда разработки Ogon.ru начинает новую фичу с проектирования API и схемы СУБД, т.к. готовая фича — это работающая и серверная, и клиентская часть. Кроме того, фича должна соответствовать критериям приемки, значит и тестировщики должны быть сразу вовлечены в процесс. Другими словами, надо организовать работу N человек, а для этого надо с чего-то начать. Давайте начнем с API, но сначала поймем, что мы хотим от веб-фреймворка.
Требования к веб-фреймворку
Во время разработки API Ogon.ru мы использовали свой положительный опыт из прошлых проектов и рекомендации REST API Tutorial. Таким образом, веб-фреймворк должен поддерживать:
- Маршрутизацию запросов по схеме.
- Маршрутизацию запросов по заголовкам.
- Маршрутизацию по URL-пути.
- Переменные в URL-пути.
- HTTP-методы.
- Контекст запроса.
- Middleware.
- Получение запросов в JSON, XML и т. д.
- Встроенную десериализацию запросов и ответов.
- Генерацию документации для фронтэндщиков, тестировщиков и наших партнеров.
- Минимизацию трафика между микросервисами.
Выбор веб-фреймворка
Проведя несложный поиск, можно легко убедиться, что:
- Веб-фреймворков много.
- Все они в той или иной степени реализуют все наши требования, кроме документации и минимизации трафика.
Как уже говорилось ранее, над фичей работают много человек, с фичей могут работать партнеры, значит документирование API становится важной задачей. Для документирования API можно воспользоваться:
На наш взгляд RAML и API Blueprint слабо представлены в golang, т.к. количество проектов, использующих эти технологии, невелико по сравнению с OpenAPI. Строить коммерческое веб-приложение на непопулярных технологиях — это риски для компании и для команды разработки, т.к. при самом плохом раскладе придется поддерживать выбранную технологию. Мы остановили свой выбор на OpenAPI, и тут на помощь нам пришел GRPC в реализации Go Protobuf и GRPC-Gateway.
GRPC используется в ситуациях, когда микросервисы общаются между собой внутри сети. GRPC-Gateway оборачивает GRPC-методы в красивое REST API. В качестве альтернативы можно было взять:
- Go Swagger, но сам swagger более многословный, чем protobuf, вдобавок не хочется гонять JSON внутри сети, поэтому он нам не подошел.
- Swag, но этот проект выглядит костыльным решением для команд, которым потребовалось документировать свое API через полгода разработки на своем любимом фреймворке. Есть риски, что документация API на основе комментариев в коде и сама реализация разойдутся, потому что разработчик забыл их актуализировать, при этом нет никаких инструментов валидации этой ситуации.
Начало работы над проектом
Теперь давайте потрогаем технологии руками и для начала:
- Установим зависимости:
a. protoc
b. docker
c. golang
d. buf, после 24 февраля введена блокировка для России, решается установкой VPN
e. make - Создадим структуру проекта на основании популярного решения.
- Настроим генерацию сервисов и сущностей из protobuf.
Генерация сервисов и сущностей из protobuf
Установим зависимости для GRPC и GRPC-Gateway:
go get google.golang.org/grpc
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2
go install github.com/alta/protopatch/cmd/protoc-gen-go-patch@latest
Настроим buf
- Создадим enterprise-application/buf.yaml
- Создадим enterprise-application/buf.gen.yaml
- Выполним buf mod update
Теперь мы готовы описывать сервисы, используя рекомендации buf и дополнительные опции protopatch, которые сделают генерацию кода более лаконичной.
Давайте опишем сервисы и сущности корзины enterprise-application/api/proto/v1/order.proto:
enterprise-application/api/proto/v1/order.proto:
syntax = "proto3";
package pb.v1;
option go_package = "github.com/byorty/enterprise-application/pkg/common/gen/pbv1";
import "patch/go.proto";
import "google/protobuf/timestamp.proto";
import "api/proto/v1/common.proto";
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0 [(go.value) = {
name: "OrderStatusUnspecified"}];
ORDER_STATUS_CREATED = 1 [(go.value) = {
name: "OrderStatusCreated"
}];
ORDER_STATUS_PAID = 2 [(go.value) = {
name: "OrderStatusPaid"
}];
ORDER_STATUS_DELIVERED = 3 [(go.value) = {
name: "OrderStatusDelivered"
}];
}
message Order {
string uuid = 1;
double amount = 2;
string address = 3;
OrderStatus status = 4;
repeated UserProduct products = 5;
google.protobuf.Timestamp created_at = 7;
google.protobuf.Timestamp delivered_at = 8;
}
enterprise-application/api/proto/v1/order_service.proto:
syntax = "proto3";
package pb.v1;
option go_package = "github.com/byorty/enterprise-application/pkg/common/gen/pbv1";
import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "api/proto/v1/common.proto";
import "api/proto/v1/order.proto";
service OrderService {
rpc Create(CreateOrderRequest) returns (Order) {
option (google.api.http) = {
post: "/v1/orders";
body: "*";
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Создание заказа";
};
};
rpc Checkout(CheckoutOrderRequest) returns (Order) {
option (google.api.http) = {
post: "/v1/orders/{order_uuid}";
body: "params";
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Оплата заказа";
};
};
}
message CreateOrderRequest {
repeated UserProduct products = 1;
string address = 2;
google.protobuf.Timestamp delivered_at = 3;
}
message CheckoutOrderRequest {
string order_uuid = 1;
CheckoutOrderRequestParams params = 2;
}
message CheckoutOrderRequestParams {
double amount = 1;
OrderStatus status = 2;
}
По аналогии с сервисом корзины описываем:
- Микросервис продуктов и его сущности.
- Микросервис пользователей и его сущности.
Генерируем golang-код:
buf generate
В результате мы должны получить сгенерированный код.
Теперь давайте приступим к реализации бизнес-логики, начиная с микросервиса продуктов, как самого простого.
Внутренняя архитектура микросервиса будет основана на принципах DDD:
- Доменный слой — описание сущностей, интерфейсов сервисов и репозиториев.
- Инфраструктурный слой — реализация интерфейсов репозиториев и сервисов.
- Слой приложения — реализация GRPC-сервиса.
Доменный слой
В рамках этой статьи мы опустим описание сущностей и репозиториев, т.к. на текущий момент не работаем с каким-либо постоянных хранилищем. Опишем сервис для управления продуктами.
pkg/product/domain/service/product.go:
package productsrv
import (
"context"
pbv1 "github.com/byorty/enterprise-application/pkg/common/gen/api/proto/v1"
)
type ProductService interface {
GetAllByFilter(ctx context.Context, params *pbv1.ProductsRequestFilter, paginator *pbv1.Paginator) ([]*pbv1.Product, uint32, error)
GetByUUID(ctx context.Context, uuid string) (*pbv1.Product, error)
}
Обратите внимание, что имя пакета отличается от имени папки из-за того, что в каждом домене будут свои сервисы, соответственно будут конфликты при импорте между order/domain/service и product/domain/service. С помощью такого простого трюка современные IDE будут импортировать пакеты и сразу вешать на него алиас.
Микросервисы заказов и пользователей реализуются и собираются аналогичным способом.
Теперь необходимо собрать все воедино и проверить работоспособность, для этого соберем docker-compose.yml.
deployments/docker-compose.yml:
version: "3.8"
services:
nginx:
image: nginx:alpine
volumes:
- ${PROJECT_DIR}/deployments/nginx.conf:/etc/nginx/conf.d/default.conf:delegated
ports:
- "80:80"
- "443:443"
swagger_ui:
image: swaggerapi/swagger-ui
environment:
SWAGGER_JSON: /spec/api.swagger.yaml
volumes:
- ${PROJECT_DIR}/api/openapi-spec/api.swagger.yaml:/spec/api.swagger.yaml
user_server:
build:
context: ..
dockerfile: ${PROJECT_DIR}/deployments/Dockerfile
args:
BUILD_APP_NAME: user-server
order_server:
build:
context: ..
dockerfile: ${PROJECT_DIR}/deployments/Dockerfile
args:
BUILD_APP_NAME: order-server
product_server:
build:
context: ..
dockerfile: ${PROJECT_DIR}/deployments/Dockerfile
args:
BUILD_APP_NAME: product-server
Поднимаем наш проект
docker-compose -p enterprise_application -f deployments/docker-compose.yml up -d --build --force-recreate
Пропишем 127.0.0.1 enterprise.application.local в /etc/hosts.
Введем в адресную строку браузера http://enterprise.application.local и увидим нашу документацию в виде swagger-файла:
Swagger-файл — это не только наглядный, но и рабочий документ, который позволяет провалиться в конкретный метод, выполнить запрос и получить ответ:
Итак, в рамках данной статьи нам удалось
- Получить бизнес-требования.
- На их основании выработать требования к архитектуре.
- Подобрать веб-фреймворк.
- Реализовать микросервисы.
- Собрать и развернуть микросервисы на локальном стенде.
- Подготовить документацию по API.
Код проекта представлен в репозитории.
В следующей статье мы научимся:
- Работать с middleware.
- Настраивать приложения с помощью конфигурационного файла и переменных окружения.
- Автоматически внедрять зависимости.
- Мокать и тестировать код.