0
Обложка: Как разработать веб-приложение уровня Enterprise Application с нуля

Как разработать веб-приложение уровня Enterprise Application с нуля

План

  1. Вводная
  2. Бизнес-требования
  3. Требования к архитектуре
  4. Требования к веб-фреймворку
  5. Выбор веб-фреймворка
  6. Начало работы над проектом

Вводная

Всем привет, сегодня мы начинаем серию статей о том, как разработать коммерческое веб-приложение с нуля. В этом материале будут продемонстрированы решения, используемые командой разработки подписки «Огонь» Ogon.ru.

Алексей Соломонов
Алексей Соломонов
СТО подписки «Огонь»

Прежде чем начать писать код, давайте поймем, что мы хотим получить в итоге. Для этого составим бизнес-требования и технические требования.

Бизнес-требования

В рамках серии статей мы разработаем интернет-магазин. Давайте разложим функционал магазина на несколько простых пользовательских историй. Я, как пользователь сайта интернет-магазина, могу:

  1. Зарегистрироваться на сайте.
  2. Авторизоваться на сайте.
  3. Посмотреть список товаров.
  4. Посмотреть карточку товара.
  5. Положить товар в корзину.
  6. Управлять товарами в корзине.
  7. Оплатить товары в корзине.
  8. Получить чек на почту.

Требования к архитектуре

Как мы видим, весь функционал раскладывается на три предметные области или домена:

  1. Пользователи.
  2. Товары.
  3. Заказы.

Разделение всего функционала на домены является одним из принципов DDD. Разделять проект на домены хорошо, потому что:

  1. Домен описывается набором сущностей, характерных для него.
  2. Домен имеет набор сервисов, которые управляют этими сущностями.
  3. Домены общаются между собой через сервисы.

Архитектура, разработанная на принципах DDD, имеет очевидные преимущества:

  1. Бизнес-логика пишется один раз внутри домена и переиспользуется другими. Значит изменение логики либо не затронет остальные части проекта, либо будут дешевле с точки зрения времени и денег.
  2. Все домены имеют схожую структуру, значит разработчикам будет легче переключаться между доменами и разбираться, что там происходит.

Таким образом, мы получаем проект, который проще разрабатывать и тестировать потому, что каждый домен является центром силы, раз он делает только свою часть работы. Исходя из того, что мы выделили три домена, нам необходимо реализовать три микросервиса:

  1. Пользователи.
  2. Товары.
  3. Заказы.

Команда разработки Ogon.ru начинает новую фичу с проектирования API и схемы СУБД, т.к. готовая фича — это работающая и серверная, и клиентская часть. Кроме того, фича должна соответствовать критериям приемки, значит и тестировщики должны быть сразу вовлечены в процесс. Другими словами, надо организовать работу N человек, а для этого надо с чего-то начать. Давайте начнем с API, но сначала поймем, что мы хотим от веб-фреймворка.

Требования к веб-фреймворку

Во время разработки API Ogon.ru мы использовали свой положительный опыт из прошлых проектов и рекомендации REST API Tutorial. Таким образом, веб-фреймворк должен поддерживать:

  1. Маршрутизацию запросов по схеме.
  2. Маршрутизацию запросов по заголовкам.
  3. Маршрутизацию по URL-пути.
  4. Переменные в URL-пути.
  5. HTTP-методы.
  6. Контекст запроса.
  7. Middleware.
  8. Получение запросов в JSON, XML и т. д.
  9. Встроенную десериализацию запросов и ответов.
  10. Генерацию документации для фронтэндщиков, тестировщиков и наших партнеров.
  11. Минимизацию трафика между микросервисами.

Выбор веб-фреймворка

Проведя несложный поиск, можно легко убедиться, что:

  1. Веб-фреймворков много.
  2. Все они в той или иной степени реализуют все наши требования, кроме документации и минимизации трафика.

Как уже говорилось ранее, над фичей работают много человек, с фичей могут работать партнеры, значит документирование API становится важной задачей. Для документирования API можно воспользоваться:

  1. RAML
  2. API Blueprint
  3. OpenAPI (Swagger)

На наш взгляд RAML и API Blueprint слабо представлены в golang, т.к. количество проектов, использующих эти технологии, невелико по сравнению с OpenAPI. Строить коммерческое веб-приложение на непопулярных технологиях — это риски для компании и для команды разработки, т.к. при самом плохом раскладе придется поддерживать выбранную технологию. Мы остановили свой выбор на OpenAPI, и тут на помощь нам пришел GRPC в реализации Go Protobuf и GRPC-Gateway.

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

  1. Go Swagger, но сам swagger более многословный, чем protobuf, вдобавок не хочется гонять JSON внутри сети, поэтому он нам не подошел.
  2. Swag, но этот проект выглядит костыльным решением для команд, которым потребовалось документировать свое API через полгода разработки на своем любимом фреймворке. Есть риски, что документация API на основе комментариев в коде и сама реализация разойдутся, потому что разработчик забыл их актуализировать, при этом нет никаких инструментов валидации этой ситуации.

Начало работы над проектом

Теперь давайте потрогаем технологии руками и для начала:

  1. Установим зависимости:
    a. protoc
    b. docker
    c. golang
    d. buf, после 24 февраля введена блокировка для России, решается установкой VPN
    e. make
  2. Создадим структуру проекта на основании популярного решения.
  3. Настроим генерацию сервисов и сущностей из 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

  1. Создадим enterprise-application/buf.yaml
  2. Создадим enterprise-application/buf.gen.yaml
  3. Выполним 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;
}

По аналогии с сервисом корзины описываем:

  1. Микросервис продуктов и его сущности.
  2. Микросервис пользователей и его сущности.

Генерируем golang-код:

buf generate

В результате мы должны получить сгенерированный код.

Теперь давайте приступим к реализации бизнес-логики, начиная с микросервиса продуктов, как самого простого.

Внутренняя архитектура микросервиса будет основана на принципах DDD:

  1. Доменный слой — описание сущностей, интерфейсов сервисов и репозиториев.
  2. Инфраструктурный слой — реализация интерфейсов репозиториев и сервисов.
  3. Слой приложения — реализация 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-файла:

Enterprise Application API Swagger

Swagger-файл — это не только наглядный, но и рабочий документ, который позволяет провалиться в конкретный метод, выполнить запрос и получить ответ:

swagger file

Итак, в рамках данной статьи нам удалось

  1. Получить бизнес-требования.
  2. На их основании выработать требования к архитектуре.
  3. Подобрать веб-фреймворк.
  4. Реализовать микросервисы.
  5. Собрать и развернуть микросервисы на локальном стенде.
  6. Подготовить документацию по API.

Код проекта представлен в репозитории.

В следующей статье мы научимся:

  1. Работать с middleware.
  2. Настраивать приложения с помощью конфигурационного файла и переменных окружения.
  3. Автоматически внедрять зависимости.
  4. Мокать и тестировать код.