Как создать MVP бэкенда

Показываем, какой MVP бэкенда можно разработать для приложения по отслеживанию процессов в компании. В качестве языка использован Go.

3К открытий3К показов

Меня зовут Антон Малыгин, я пишу мини-серию статей о своих пет-проектах или просто нерабочих проектах, в которых участвовал, с техническими подробностями. В прошлой статье я рассказал, как мы «озвучивали» интернет.

Сегодня расскажу о проекте из того же 2018 года, в котором мне предложили поучаствовать, также, в качестве backend-разработчика.

Идея

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

Суть работы сервиса была следующая: диспетчер/оператор через web-версию создает черновик маршрута для судна, с различным набором событий для учета. Капитан или кто-то из команды, с помощью мобильной версии приложения, подтверждает события из маршрута. Например, по плану должна быть погрузка, капитан подтверждает, потом начинается рейд, капитан отмечает, что рейд начался и т.д. Естественно, все это фиксируется на временных отрезках, т.е. диспетчер видит сколько фактически времени занял каждый этап.

Нужно было сделать MVP проекта, которая включала в себя web-версию, android и backend.

Техническая часть

Я решил писать backend на Go, так как к тому моменту имел небольшой опыт реализации сервиса для отправки Push-уведомлений для iOS, используя Go.

К тому уже имел опыт реализации бэка на MVC на языке Scala, и именно поэтому решил использовать MVC здесь.

После небольших поисков и сравнений решено было использовать web framework iris.

По-моему скромному мнению написан понятно и хорошо, также очень хорошо поддерживается, плюс у него есть модуль для MVC, ну и, конечно, большое количество звездочек на гитхаб =)

Вот так происходит инициализация сервиса:

			func main() {
  app := NewAppDB()
  app.Run(
    iris.Addr(":8080"),
  )
}

func NewAppDB() *iris.Application {
  app := iris.New()
  app.Logger().SetLevel("debug")

  config := PsqlInfo()
  fmt.Printf("%s", config)
  database, err := db.InitWithOptions(config, true)
  if err != nil {
    fmt.Printf("%s", err.Error())
    panic(err)
  }

  service := services.NewEntityService(database)
  userService := services.NewUserService(database)
  taskService := services.NewTaskService(database)
  planService := services.NewPlanService(database)

  task := mvc.New(app.Party("/api/v1/tasks"))
  task.Register(taskService, userService)
  task.Handle(new(controllers.TaskController))

  plan := mvc.New(app.Party("/api/v1/plans"))
  plan.Register(planService, userService)
  plan.Handle(new(controllers.PlanController))

  entity := mvc.New(app.Party("/api/v1/entities"))
  entity.Register(service, userService)
  entity.Handle(new(controllers.EntityController))

  login := mvc.New(app.Party("/api/v1/login"))
  login.Register(userService)
  login.Handle(new(controllers.AuthController))

  users := mvc.New(app.Party("/api/v1/users"))
  users.Register(userService)
  users.Handle(new(controllers.UserController))

  return app
}
		

Web-приложение случает 8080 порт, на который все запросы перенаправляет Nginx.

Сервис работает по принципу CRUD, т.е. по сути является прослойкой между клиентом и базой данных, выполняя простые операции.

Все приложение состоит из 3 основных слоев:

  1. Контроллеры
  2. Модели данных
  3. Сервисы

Рассмотрим простой контроллер аутентификации:

			type AuthController struct {
  Ctx iris.Context
  Service services.UserService
}

func (c *AuthController) Post() interface{} {
  var request AuthRequest
  err := c.Ctx.ReadJSON(&request)
  if err != nil {
    return iris.StatusBadRequest
  }

  user, err := c.Service.Auth(request.Login, request.Password)
  if err != nil {
    return iris.StatusUnauthorized
  }
  return UserResult{Id:user.Id, Token:user.Token}
}

type AuthRequest struct {
  Login string `json:"login"`
  Password string `json:"password"`
}

type UserResult struct {
  Id int64 `json:"id"`
  Token string `json:"token"`
}
		

Контроллер получает POST запрос с логином и паролем, парсит json в AuthRequest, далее пытается получить User на основе пары логин/пароль из UserService. В случае успеха, возвращается структура UserResult c токеном и ID пользователя.

Преобразование в json и обратно происходит автоматически, с помощью метки «json» у каждого поля структуры.

Сервисы реализуют бизнес-логику, работу с хранилищем и т.д. На примере EntityService:

			type EntityService interface {
  Get(companyId int64) (*datamodels.EntitiesResult, error)
  GetEntities(entityType EntityType, companyId int64) (*[]datamodels.Entity, error)
  GetVessels(companyId int64) (*[]datamodels.VesselEntity, error)
  GetHistory(historyId int64, companyId int64) (*datamodels.EntitiesHistoryResult, error)
  GetBarges(companyId int64) (*[]datamodels.BargeEntity, error)
}

func NewEntityService(database *db2.Database) EntityService {
  return &EntityServiceImpl{Database: database}
}

type EntityServiceImpl struct {
  Database *db2.Database
}

func (es *EntityServiceImpl) GetVessels(companyId int64) (*[]datamodels.VesselEntity, error) {
  vessels, err := db2.GetVessels(es.Database.DB, companyId)
  if err != nil {
    return nil, err
  }
  return mappingVesselsToVesselEntity(vessels), nil
}
		

Интерфейс EntityService отвечает за работу с разными объектами, например, баржи, суда и т.д.

Реализация GetVessels возвращает все суда для companyId.

Для работы с БД используется расширение стандартного пакета database/sql:

			type EntityObject struct {
  Id int64
  Name string
  CompanyId int64 `db:"company_id"`
}

type Vessel struct {
  EntityObject
  InRent bool `db:"in_rent"`
}

func GetVessels(db *sqlx.DB, companyId int64) (*[]Vessel, error) {
  var vessels []Vessel
  err := GetObjects(db, Vessels, &vessels, companyId)
  return &vessels, err
}

func GetObjects(db *sqlx.DB, tableName EntityTableName, result interface{}, companyId int64) error {
  err := db.Select(result, "SELECT * FROM " + string(tableName) + " WHERE company_id = $1", companyId)
  return err
}
		

Вместо наследования в Go используется композиция, например, здесь, есть объект EntityObject, с общим набором полей и специфичный Vessel, добавляющий поле InRent.

Заключение

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

Но я выполнил программу минимум для себя — получил интересный и полезный опыт разработки на Go, поэтому потратил время не напрасно.

В следующей статье расскажу про, можно сказать, успешный пет-проект, который работает по сей день, и на котором даже удалось заработать.

Следите за новыми постами
Следите за новыми постами по любимым темам
3К открытий3К показов