本文章同時發佈於:
文章為自己的經驗與夥伴整理的內容,設計沒有標準答案,如有可以改進的地方,請告訴我,我會盡我所能的修改,謝謝大家~
大家好,還記得在 DAY6~DAY8 的 Clean Architecture 篇,使用了 Restful API 來設計,但是後來的介紹卻都是 gRPC,其實最大的目的是要介紹本篇,將 Restful API Server 換成 gRPC Server,而使用 Clean Architecture 你可以
讓換框架或引擎只需更動 delivery 層,不引響商務邏輯(Business Logic)
如果對原本的 Server 設計不清楚,可以先閱讀以下文章:
- DAY6 - 你的 Backend 可以更有彈性一點 - Clean Architecture 概念篇
- DAY7 - 奔放的 Golang,卻隱藏著有紀律的架構! - Clean Architecture 實作篇
- DAY8 - 讓你的 Backend 萬物皆虛,萬事皆可測 - Clean Architecture 測試篇
怎麼 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,分別是Create
、Query
、Foster
,你可以把他理解為 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,並以DigimonUsecase
的Store()
來儲存,與原本 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,最後也以DigimonUsecase
的Store()
來儲存,所以,
Delivery 層單純就是在實作如何把引擎的資訊交付給 Usecase 層
很實用對吧!雖然當我知道這樣的架構的時候,我內心偏向下圖 XD:
(滿滿的重構時間)
謝謝你的閱讀~