
Lookup Order 功能 - Clean Architecture in Go
基於 Place Order 的實作,我們的專案已經有一個不錯的雛形可以在相當容易擴充的狀況下繼續進行。然而,如果只能建立訂單仍不足以驗證功能完善,因此我們接下來要加入 Lookup Order 來確認是否順利的將資料儲存到記憶體中。
擴展 API
原本我們利用 oapi-codegen
產生了 POST /orders
這個端點(Endpoint),接下來我們需要更新一下文件,加入 GET /orders/{orderId}
這個新的端點用於 Lookup Order 的實作。
1# ...
2paths:
3 /orders/{orderId}:
4 get:
5 summary: Lookup an order
6 operationId: lookupOrder
7 tags:
8 - order
9 parameters:
10 - in: path
11 name: orderId
12 required: true
13 description: The UUID of order
14 schema:
15 type: string
16 example: 2ef749d9-b25e-49df-8ff3-54f3873fffb8
17 responses:
18 200:
19 description: Order found
20 content:
21 application/json:
22 schema:
23 $ref: '#/components/schemas/LookupOrderResponse'
24# ...
25components:
26 schemas:
27# ....
28 LookupOrderResponse:
29 type: object
30 required:
31 - id
32 - name
33 - items
34 properties:
35 id:
36 type: string
37 example: 2ef749d9-b25e-49df-8ff3-54f3873fffb8
38 name:
39 type: string
40 example: Aotoki
41 items:
42 type: array
43 minimum: 1
44 items:
45 $ref: '#/components/schemas/OrderItem'
46# ..
基本上跟我們在處理 Place Order 的方式相同,然而 LookupOrderResponse
在這裡選擇跟 PlaceOrderResponse
重複的原因,是考量到這兩個行為的回傳並不一定會在未來相同,這跟實作時,不要太快嘗試消除重複的想法是類似的。
接下來,在 internal/api/rest/lookup_order.go
實作新產生的方法,我們就可以接續到 UseCase 的處理。
1package rest
2
3// ...
4
5func (api *Api) LookupOrder(w http.ResponseWriter, r *http.Request, orderId string) {
6 out, err := api.LookupOrderUsecase.Execute(r.Context(), &usecase.LookupOrderInput{
7 Id: orderId,
8 })
9
10 if err != nil {
11 http.Error(w, err.Error(), http.StatusBadRequest)
12 return
13 }
14
15 res := buildLookupOrderResponse(out)
16 if err := json.NewEncoder(w).Encode(res); err != nil {
17 http.Error(w, err.Error(), http.StatusInternalServerError)
18 return
19 }
20}
21
22func buildLookupOrderResponse(out *usecase.LookupOrderOutput) *LookupOrderResponse {
23 outItems := make([]OrderItem, 0, len(out.Items))
24 for _, item := range out.Items {
25 outItems = append(outItems, OrderItem{
26 Name: item.Name,
27 Quantity: float32(item.Quantity),
28 UnitPrice: float32(item.UnitPrice),
29 })
30 }
31
32 return &LookupOrderResponse{
33 Id: out.Id,
34 Name: out.Name,
35 Items: outItems,
36 }
37}
實作 UseCase
UseCase 處理上跟 Place Order 大致上相同。因為原本的 Repository 介面並沒有描述如何讀取資料,因此我們需要先擴充 OrderRepository
的方法。
1type OrderRepository interface {
2 Find(ctx context.Context, id string) (*orders.Order, error)
3 Save(ctx context.Context, order *orders.Order) error
4}
基於 Golang 在 io.Reader
的設計,還能透過組合多個介面提供像是 io.ReadWriter
等不同情境,在規劃介面時也需要仔細思考是否附加太多方法,這可能會造成測試上的不便。
有了 Find()
方法後,我們就可以實作一個取出特定 Order
的 UseCase 來滿足這個需求。
1package usecase
2
3import "context"
4
5type LookupOrderItem struct {
6 Name string
7 Quantity int
8 UnitPrice int
9}
10
11type LookupOrderInput struct {
12 Id string
13}
14
15type LookupOrderOutput struct {
16 Id string
17 Name string
18 Items []LookupOrderItem
19}
20
21type LookupOrder struct {
22 orders OrderRepository
23}
24
25func NewLookupOrder(orders OrderRepository) *LookupOrder {
26 return &LookupOrder{
27 orders: orders,
28 }
29}
30
31func (u *LookupOrder) Execute(ctx context.Context, input *LookupOrderInput) (*LookupOrderOutput, error) {
32 order, err := u.orders.Find(ctx, input.Id)
33 if err != nil {
34 return nil, err
35 }
36
37 out := &LookupOrderOutput{
38 Id: order.Id(),
39 Name: order.CustomerName(),
40 Items: []LookupOrderItem{},
41 }
42
43 for _, item := range order.Items() {
44 out.Items = append(out.Items, LookupOrderItem{
45 Name: item.Name(),
46 Quantity: item.Quantity(),
47 UnitPrice: item.UnitPrice(),
48 })
49 }
50
51 return out, nil
52}
接下來,我們繼續把 Repository 實作到裡面,就可以讓 Lookup Order 功能正常運作。
實作 Repository
Repository 的部分基本上沒有太大需要修改,然而我們會遇到「找不到」的情境,因此需要先在 internal/entity/orders/order.go
中加入新的錯誤類型。
1// ...
2var (
3 ErrItemNameMustBeUnique = errors.New("item name must be unique")
4 ErrOrderNotFound = errors.New("order not found")
5)
6// ...
在這裡選擇在 Entity 中定義錯誤,因為「訂單不存在」如果在 Repository 中定義的話,UseCase 就必須依賴於 Repository 上。同時,這個錯誤的知識存在於訂單相關的實體,也會比在 Repository 中合理不少。
撰寫本系列時,仍然沒有找到非常優秀的設計方式,以 Clean Architecture 的思考方式來看,在 Entity 或 UseCase 會合理不少。
在定義好錯誤後,就可以更新 internal/repository/in_memory_orders.go
來增加 Find()
的實作。
1package repository
2
3// ...
4
5type InMemoryOrderRepository struct {
6 orders map[string]InMemoryOrderSchema
7}
8
9// ...
10
11func (r *InMemoryOrderRepository) Find(ctx context.Context, id string) (*orders.Order, error) {
12 orderSchema, ok := r.orders[id]
13 if !ok {
14 return nil, orders.ErrOrderNotFound
15 }
16
17 order := orders.New(id, orderSchema.CustomerName)
18 for _, itemSchema := range orderSchema.Items {
19 err := order.AddItem(itemSchema.Name, itemSchema.Quantity, itemSchema.UnitPrice)
20 if err != nil {
21 return nil, err
22 }
23 }
24
25 return order, nil
26}
到此為止,我們就有一個非常常見的 API 雛形,在 MVC 框架的架構下也不難做出這樣的實作。因此,下一篇開始我們要開始逐步讓這個功能稍微複雜一些,來了解 Clean Architecture 對於較大的系統能提供的彈性以及擴充性。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 連載介紹 - Clean Architecture in Go
- 目標設定 - Clean Architecture in Go
- wire 的依賴注入 - Clean Architecture in Go
- 案例說明 - Clean Architecture in Go
- 操作介面設計 - Clean Architecture in Go
- Place Order 實作 Controller 部分 - Clean Architecture in Go
- Place Order 實作 Entity 部分 - Clean Architecture in Go
- Place Order 實作 Repository 部分 - Clean Architecture in Go
- Lookup Order 功能 - Clean Architecture in Go