Clean Architecture 的力量!無痛從 Restful API 轉換成 gRPC Server

本文章同時發佈於:

文章為自己的經驗與夥伴整理的內容,設計沒有標準答案,如有可以改進的地方,請告訴我,我會盡我所能的修改,謝謝大家~

大家好,還記得在 DAY6~DAY8 的 Clean Architecture 篇,使用了 Restful API 來設計,但是後來的介紹卻都是 gRPC,其實最大的目的是要介紹本篇,將 Restful API Server 換成 gRPC Server,而使用 Clean Architecture 你可以

讓換框架或引擎只需更動 delivery 層,不引響商務邏輯(Business Logic)

如果對原本的 Server 設計不清楚,可以先閱讀以下文章:

怎麼 run 起來?

建議大家直接先把 Server run 起來,這樣會比較有感覺,

請先 clone Github-Example-Code

$ cd DAY13/go-server
$ docker-compose up

並且安裝Bloomrpc來測試,他類似於 gRPC 界的 Postman。(當然你也可以寫 gRPC client 來測試,不過這邊以有介面的 tool 來測試)

安裝好後,可以在左上方的+按鈕直接 import protobuf 的 schema,

修改好 gRPC Server 的 URL 成localhost:6000後,點選綠色test按鈕,即可看到 gRPC Server 已經處理完你的請求,並且回傳 response。

W ow,可以使用 gRPC Server 了,接下來將介紹實作,

真的有那麼少?!看看就知道 XD。

實作 gRPC schema

syntax = "proto3";

package digimon;

service Digimon {
  rpc Create (CreateRequest) returns (CreateResponse) {}
  rpc Query (QueryRequest) returns (QueryResponse) {}
  rpc Foster (FosterRequest) returns (FosterResponse) {}
}

// Requests

message CreateRequest {
  string name = 1;
}

message QueryRequest {
  string id = 1;
}

message FosterRequest {
  message Food {
    string name = 1;
  }

  string id = 1;
  Food food = 2;
}

// Responses

message CreateResponse {
  string id = 1;
  string name = 2;
  string status = 3;
}

message QueryResponse {
  string id = 1;
  string name = 2;
  string status = 3;
}

message FosterResponse {
}

在 Digimon service 裡分別新增 3 個 method,分別是CreateQueryFoster,你可以把他理解為 Restful API 的三個 API,並且分別給 3 個 method 定義好 request 與 response,在 code 中必須符合此 protobuf schema 來實作。

實作 gRPC Server

在 digimon/delivery 資料夾新增一個 grpc 資料夾,他代表了另一種引擎的 deliver,

實作grpc_handler.go

// go-server/digimon/delivery/grpc/grpc_handler.go

// ... 其他程式碼

// DigimonHandler ...
type DigimonHandler struct {
	DigimonUsecase domain.DigimonUsecase
	DietUsecase    domain.DietUsecase
	pb.UnimplementedDigimonServer
}

// NewDigimonHandler ...
func NewDigimonHandler(s *grpcLib.Server, digimonUsecase domain.DigimonUsecase, dietUsecase domain.DietUsecase) {
	handler := &DigimonHandler{
		DigimonUsecase: digimonUsecase,
		DietUsecase:    dietUsecase,
	}

	pb.RegisterDigimonServer(s, handler)
}

// Create ...
func (d *DigimonHandler) Create(ctx context.Context, req *pb.CreateRequest) (*pb.CreateResponse, error) {
	aDigimon := domain.Digimon{
		Name: req.GetName(),
	}
	if err := d.DigimonUsecase.Store(ctx, &aDigimon); err != nil {
		logrus.Error(err)
		return nil, status.Errorf(codes.Internal, "Internal error. Store failed")
	}

	return &pb.CreateResponse{
		Id:     aDigimon.ID,
		Name:   aDigimon.Name,
		Status: aDigimon.Status,
	}, nil
}

// ... 其他程式碼

Create()中,使用req.GetName()獲得 Name,並以DigimonUsecaseStore()來儲存,與原本 Restful API 的 code 做比較,

// go-server/digimon/delivery/http/digimon_handler.go

// ... 其他程式碼

// DigimonHandler ...
type DigimonHandler struct {
	DigimonUsecase domain.DigimonUsecase
	DietUsecase    domain.DietUsecase
}

// NewDigimonHandler ...
func NewDigimonHandler(e *gin.Engine, digimonUsecase domain.DigimonUsecase, dietUsecase domain.DietUsecase) {
	handler := &DigimonHandler{
		DigimonUsecase: digimonUsecase,
		DietUsecase:    dietUsecase,
	}

	e.GET("/api/v1/digimons/:digimonID", handler.GetDigimonByDigimonID)
	e.POST("/api/v1/digimons", handler.PostToCreateDigimon)
	e.POST("/api/v1/digimons/:digimonID/foster", handler.PostToFosterDigimon)
}

// PostToCreateDigimon ...
func (d *DigimonHandler) PostToCreateDigimon(c *gin.Context) {
	var body swagger.DigimonInfoRequest
	if err := c.BindJSON(&body); err != nil {
		logrus.Error(err)
		c.JSON(500, &swagger.ModelError{
			Code:    3000,
			Message: "Internal error. Parsing failed",
		})
		return
	}
	aDigimon := domain.Digimon{
		Name: body.Name,
	}
	if err := d.DigimonUsecase.Store(c, &aDigimon); err != nil {
		logrus.Error(err)
		c.JSON(500, &swagger.ModelError{
			Code:    3000,
			Message: "Internal error. Store failed",
		})
		return
	}

	c.JSON(200, swagger.DigimonInfo{
		Id:     aDigimon.ID,
		Name:   aDigimon.Name,
		Status: aDigimon.Status,
	})
}

// ... 其他程式碼

除了交付參數給 Usecase 的方式不同,其他邏輯完全相同!

對的沒錯,在 Restful API 中需要以swagger.DigimonInfoRequest搭配c.BindJSON()來獲得 JSON Body,最後也以DigimonUsecaseStore()來儲存,所以,

Delivery 層單純就是在實作如何把引擎的資訊交付給 Usecase 層


很實用對吧!雖然當我知道這樣的架構的時候,我內心偏向下圖 XD:

(滿滿的重構時間)

謝謝你的閱讀~

參考

comments powered by Disqus