zahir-core / grest

An instant, full-featured, and scalable REST APIs framework for Go. Designed to make rapid development easy with clean code and best performance.
MIT License
13 stars 1 forks source link

Refactoring for proper dependency injection #7

Closed jeffry-luqman closed 2 years ago

jeffry-luqman commented 2 years ago

To keep it simple, the current rest api application project structure proposed by GREST is as follows:

├── app                 # config, framework, drivers, etc
│   ├── i18n
│   │   ├── en_US.go
│   │   ├── id_ID.go
│   │   ...
│   ├── app.go
│   ├── auth.go
│   ├── cache.go
│   ├── config.go
│   ├── crypto.go
│   ├── ctx.go
│   ├── db.go
│   ├── fs.go
│   ├── handler.go
│   ├── i18n.go
│   ├── log.go
│   ├── mail.go
│   ├── slack.go
│   ├── telegram.go
│   ├── validator.go
│   ...
├── middleware
│   ├── auth.go
│   ├── ctx.go
│   ├── db.go
│   ├── log.go
│   ...
├── src                 # all domains are here, up to you, you can structure directories based on the scope of your business logic
│   ├── domain1
│   │   ├── model.go                     # Entities / Enterprise business rules
│   │   ├── seeder.go
│   │   ├── use_case.go                  # Application business role, pure business logic
│   │   ├── use_case_test.go
│   │   ...
│   │   ├── rest_api_handler.go
│   │   ├── grpc_handler.go
│   │   ...
│   ├── domain2
│   │   ├── model.go
│   │   ├── rest_api_handler.go
│   │   ├── seeder.go
│   │   ├── use_case.go
│   │   ├── use_case_test.go
│   │   ...
│   ├── auth
│   │   ├── user
│   │   │   ├── role
│   │   │   │   ├── model.go
│   │   │   │   ├── rest_api_handler.go
│   │   │   │   ├── seeder.go
│   │   │   │   ├── use_case.go
│   │   │   │   └── use_case_test.go
│   │   │   ...
│   │   │   ├── model.go
│   │   │   ├── rest_api_handler.go
│   │   │   ├── seeder.go
│   │   │   ├── use_case.go
│   │   │   └── use_case_test.go
│   │   ...
│   │   └── session
│   │       ├── model.go
│   │       ...
│   ...
│   ├── db_migration.go
│   ├── db_seed.go
│   ├── middleware.go
│   ├── route.go
│   └── schedule.go
├── go.mod
├── go.sum
├── main.go
...

So far it's been going pretty well until we realized that dependency injection is still not standardized and each layer is too easy to access to potentially become tightly coupled. To keep each layer have clear boundaries, allowing replacement of parts without affecting the rest of the project and easier to build, test and maintain, it is necessary to standardize how dependencies are injected.

The design I propose is as follows: use_case.go

package user

import (
  "net/url"

  "{module}/app"
)

type UseCase struct {
  // Dependencies (interface or func or struct or whatever as required)
  DB        app.DB
  Cache     app.Cache
  Validator app.Validator
  User      app.User
  Query     url.Values
  ...
}

func (uc *UseCase) GetByID(id string) (User, error) {
  // pure business logic without direct call to any deps
  ...
  return User{}, nil
}

func (uc *UseCase) Get() (UserList, error) {
  // pure business logic without direct call to any deps
  ...
  return UserList{}, nil
}

func (uc *UseCase) Create(param *CreateUserParam) error {
  // pure business logic without direct call to any deps
  ...
  return nil
}

func (uc *UseCase) UpdateByID(id string, param *UpdateUserParam) error {
  // pure business logic without direct call to any deps
  ...
  return nil
}

func (uc *UseCase) PartialyUpdateByID(id string, param *PartialyUpdateUserParam) error {
  // pure business logic without direct call to any deps
  ...
  return nil
}

func (uc *UseCase) DeleteByID(id string, param *PartialyUpdateUserParam) error {
  // pure business logic without direct call to any deps
  ...
  return nil
}

rest_api_handler.go (or grpc_handler, etc)

package user

import (
  "github.com/gofiber/fiber/v2"

  "{module}/app"
)

type RESTfulAPI struct {
  UseCase *UseCase
}

// dependency injection
func (r *RESTfulAPI) useCaseImpl(c *fiber.Ctx) error {
  // inject the dependency here
  return nil
}

// Get user by id
// GET /users/:id
func (r *RESTfulAPI) GetByID(c *fiber.Ctx) error {
  err := r.useCaseImpl(c)
  if err != nil {
    return app.ErrorHandler(c, err)
  }
  res, err := r.UseCase.GetByID(c.Params("id"))
  if err != nil {
    return app.ErrorHandler(c, err)
  }
  return c.JSON(grest.NewJSON(res).ToStructured().Data)
}

// Get list of user
// GET /users
func (r *RESTfulAPI) Get(c *fiber.Ctx) error {
  err := r.useCaseImpl(c)
  if err != nil {
    return app.ErrorHandler(c, err)
  }
  res, err := r.UseCase.Get()
  if err != nil {
    return app.ErrorHandler(c, err)
  }
  return c.JSON(grest.NewJSON(res).ToStructured().Data)
}

// Create user
// POST /users
func (r *RESTfulAPI) Create(c *fiber.Ctx) error {
  err := r.useCaseImpl(c)
  if err != nil {
    return app.ErrorHandler(c, err)
  }
  param := CreateUserParam{}
  err = grest.NewJSON(c.Body()).ToFlat().Unmarshal(&param)
  if err != nil {
    return app.ErrorHandler(c, err)
  }
  err = r.UseCase.Create(&param)
  if err != nil {
    return app.ErrorHandler(c, err)
  }
  res, err := r.UseCase.GetByID(param.ID)
  if err != nil {
    return app.ErrorHandler(c, err)
  }
  return c.Status(http.StatusCreated).JSON(grest.NewJSON(res).ToStructured().Data)
}

// Update user by id
// PUT /users/:id
func (r *RESTfulAPI) UpdateByID(c *fiber.Ctx) error {
  err := r.useCaseImpl(c)
  if err != nil {
    return app.ErrorHandler(c, err)
  }
  param := UpdateUserParam{}
  err = grest.NewJSON(c.Body()).ToFlat().Unmarshal(&param)
  if err != nil {
    return app.ErrorHandler(c, err)
  }
  err = r.UseCase.UpdateByID(c.Params("id"), &param)
  if err != nil {
    return app.ErrorHandler(c, err)
  }
  res, err := r.UseCase.GetByID(param.ID)
  if err != nil {
    return app.ErrorHandler(c, err)
  }
  return c.JSON(grest.NewJSON(res).ToStructured().Data)
}

// Partialy update user by id
// PATCH /users/:id
func (r *RESTfulAPI) PartialyUpdateByID(c *fiber.Ctx) error {
  err := r.useCaseImpl(c)
  if err != nil {
    return app.ErrorHandler(c, err)
  }
  param := PartialyUpdateUserParam{}
  err = grest.NewJSON(c.Body()).ToFlat().Unmarshal(&param)
  if err != nil {
    return app.ErrorHandler(c, err)
  }
  err = r.UseCase.PartialyUpdateByID(c.Params("id"), &param)
  if err != nil {
    return app.ErrorHandler(c, err)
  }
  res, err := r.UseCase.GetByID(param.ID)
  if err != nil {
    return app.ErrorHandler(c, err)
  }
  return c.JSON(grest.NewJSON(res).ToStructured().Data)
}

// Delete user by id
// DELETE /users/:id
func (r *RESTfulAPI) DeleteByID(c *fiber.Ctx) error {
  err := r.useCaseImpl(c)
  if err != nil {
    return app.ErrorHandler(c, err)
  }
  param := DeleteUserParam{}
  err = grest.NewJSON(c.Body()).ToFlat().Unmarshal(&param)
  if err != nil {
    return app.ErrorHandler(c, err)
  }
  err = r.UseCase.DeleteByID(c.Params("id"), &param)
  if err != nil {
    return app.ErrorHandler(c, err)
  }
  return c.JSON(app.GeneralResponse{Code: 200, Message: r.UseCase.Trans(r.UseCase.Lang, "deleted")})
}
jeffry-luqman commented 2 years ago

grest cli issue