蒼時弦也
蒼時弦也
資深軟體工程師
發表於

gRPC Server 實作 - Clean Architecture in Go

這篇文章是 Clean Architecture in Go 系列的一部分,你可以透過 Leanpub 提前閱讀內容。

在上一階段我們完成了可以運行的 gRPC Server 接下來就可以將原本 RESTful API 上的實作移植到 gRPC 上,讓我們可以使用 gRPC 來操作這些功能。

前置準備

因為我們將 RESTful API 常用的 Controller 定位在「處理輸入」的角色,所以 Controller 只有對 JSON 解析相關的處理,實際的商業邏輯都是放在 UseCase 裡面。

基於這樣的理由,我們只需要讓 *grpc.OrderServer*rest.Api 一樣參考這些 UseCase 就可以重現相同的功能。

 1// internal/api/grpc/grpc.go
 2
 3var DefaultSet = wire.NewSet(
 4	wire.Struct(new(OrderServer), "*"),
 5	NewServer,
 6)
 7
 8var _ orderspb.OrderServer = &OrderServer{}
 9
10type OrderServer struct {
11	orderspb.OrderServer `wire:"-"`
12	PlaceOrderUsecase    *usecase.PlaceOrder
13	LookupOrderUsecase   *usecase.LookupOrder
14}
15
16// ...

因為 wire 的依賴注入會掃描所有的 Field,我們需要用 wire:"-" 來表示不用對這個 Field 注入,原本的 NewOrderServer 也可以改為 wire.Struct 的方式替換掉。

使用 wire 命令重新生成依賴注入的檔案後,我們就可以進行後續的實作。

Lookup Order

我們先從比較單純的 Lookup Order 來處理,實作的方式跟 RESTful API 基本上相同。將原本 internal/api/grpc/grpc.go 的方法移動到 internal/api/grpc/lookup_order.go 加入以下實作。

 1// ...
 2
 3func (s *OrderServer) LookupOrder(ctx context.Context, req *orderspb.LookupOrderRequest) (*orderspb.LookupOrderResponse, error) {
 4	out, err := s.LookupOrderUsecase.Execute(ctx, &usecase.LookupOrderInput{
 5		Id: req.Id,
 6	})
 7
 8	if err != nil {
 9		return nil, err
10	}
11
12	res := buildLookupOrderResponse(out)
13
14	return res, nil
15}
16
17func buildLookupOrderResponse(out *usecase.LookupOrderOutput) *orderspb.LookupOrderResponse {
18	outItems := make([]*orderspb.OrderItem, 0, len(out.Items))
19	for _, item := range out.Items {
20		outItems = append(outItems, &orderspb.OrderItem{
21			Name:      item.Name,
22			Quantity:  int32(item.Quantity),
23			UnitPrice: int32(item.UnitPrice),
24		})
25	}
26
27	return &orderspb.LookupOrderResponse{
28		Id:    out.Id,
29		Name:  out.Name,
30		Items: outItems,
31	}
32}

除了將 gRPC 生成的資料結構轉換為我們 UseCase 的結構外,和使用 RESTfult API 幾乎沒有差異,透過這樣的手法我們讓這些方法可以很輕易地轉換到不同的協定上,或者在各類情境中使用。

Place Order

基本上和 Lookup Order 相同,我們將方法移動到 internal/api/grpc/place_order.go 中,並且加入以下實作。

 1func (s *OrderServer) PlaceOrder(ctx context.Context, req *orderspb.PlaceOrderRequest) (*orderspb.PlaceOrderResponse, error) {
 2	input := buildPlaceOrderInput(req)
 3
 4	out, err := s.PlaceOrderUsecase.Execute(ctx, input)
 5	if err != nil {
 6		return nil, err
 7	}
 8
 9	res := buildPlaceOrderResponse(out)
10
11	return res, nil
12}
13
14func buildPlaceOrderInput(req *orderspb.PlaceOrderRequest) *usecase.PlaceOrderInput {
15	inItems := make([]usecase.PlaceOrderItem, 0, len(req.Items))
16	for _, item := range req.Items {
17		inItems = append(inItems, usecase.PlaceOrderItem{
18			Name:      item.Name,
19			Quantity:  int(item.Quantity),
20			UnitPrice: int(item.UnitPrice),
21		})
22	}
23
24	return &usecase.PlaceOrderInput{
25		Name:  req.Name,
26		Items: inItems,
27	}
28}
29
30func buildPlaceOrderResponse(out *usecase.PlaceOrderOutput) *orderspb.PlaceOrderResponse {
31	outItems := make([]*orderspb.OrderItem, 0, len(out.Items))
32	for _, item := range out.Items {
33		outItems = append(outItems, &orderspb.OrderItem{
34			Name:      item.Name,
35			Quantity:  int32(item.Quantity),
36			UnitPrice: int32(item.UnitPrice),
37		})
38	}
39
40	return &orderspb.PlaceOrderResponse{
41		Id:    out.Id,
42		Name:  out.Name,
43		Items: outItems,
44	}
45}

因為輸入跟輸出的內容比較多,因此需要花比較多力氣在處理上除此之外沒什麼不同。

最後,我們可以啟動 gRPC Server 使用 grpcurl 命令來測試看看。

1$ grpcurl -plaintext localhost:8080 orders.Order.PlaceOrder
2{
3  "id": "e1454ea1-f31c-415a-9df0-2f89dd8ef85a"
4}

好像哪裡有點奇怪,跟 RESTful API 的行為似乎有點不同,我們接下來會來探討一下這個問題。