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

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

Tags
Kubernetes
ithome 2020 ironman
Date
Sep 27, 2020
本文章同時發佈於:
文章為自己的經驗與夥伴整理的內容,設計沒有標準答案,如有可以改進的地方,請告訴我,我會盡我所能的修改,謝謝大家~

大家好,還記得在 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
notion image
並且安裝Bloomrpc來測試,他類似於 gRPC 界的 Postman。(當然你也可以寫 gRPC client 來測試,不過這邊以有介面的 tool 來測試)
安裝好後,可以在左上方的+按鈕直接 import protobuf 的 schema,
notion image
修改好 gRPC Server 的 URL 成localhost:6000後,點選綠色test按鈕,即可看到 gRPC Server 已經處理完你的請求,並且回傳 response。
notion image
W ow,可以使用 gRPC Server 了,接下來將介紹實作,
notion image
真的有那麼少?!看看就知道 XD。

實作 gRPC schema

syntax = "proto3"; package digimon; service Digimon { rpc Create (CreateRequest) returns (CreateResponse) {} rpc Query (QueryRequest) returns (QueryResponse) {} rpc Foster (FosterRequest) returns (FosterResponse) {} } // Requestsmessage CreateRequest { string name = 1; } message QueryRequest { string id = 1; } message FosterRequest { message Food { string name = 1; } string id = 1; Food food = 2; } // Responsesmessage 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,
notion image
實作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:
notion image
(滿滿的重構時間)
謝謝你的閱讀~

參考