
操作介面設計 - Clean Architecture in Go
假設要從零開始開發,相對好的方式是以黑箱的角度思考。也就是我們以「使用者看到的樣子」開始建構系統,並且在過程中逐步的「提煉」將 UseCase、Entity 從系統中提取出來。
這種做法在使用 BDD(Behavior-Driven Development)或者 TDD(Test-Driven Development)也能幫助我們設計出一些初期的測試案例。
撰寫文件
我們要撰寫的是以 OpenAPI 3.0 規範為基礎的文件,在規劃文件之前可能已經經過了產品規格相關的討論,也理解到一些使用者案例,因此我們大致上知道會存在 Place Order(下單)和 Lookup Order(查詢)兩個行為。
在這邊我們先以接下來會實作的 Place Order 為例子,撰寫一份 openapi.yaml
1openapi: 3.0.0
2info:
3 title: Clean Architecture in Go
4 version: 1.0.0
5tags:
6 - name: order
7 description: The orders in the service
8paths:
9 /orders:
10 post:
11 summary: Place a new order
12 operationId: placeOrder
13 tags:
14 - order
15 requestBody:
16 content:
17 application/json:
18 schema:
19 $ref: '#/components/schemas/PlaceOrderRequest'
20 responses:
21 200:
22 description: Place order successful
23 content:
24 application/json:
25 schema:
26 $ref: '#/components/schemas/PlaceOrderResponse'
27
28components:
29 schemas:
30 PlaceOrderRequest:
31 type: object
32 required:
33 - name
34 - items
35 properties:
36 name:
37 type: string
38 example: Aotoki
39 items:
40 type: array
41 minimum: 1
42 items:
43 $ref: '#/components/schemas/OrderItem'
44 PlaceOrderResponse:
45 type: object
46 required:
47 - id
48 - name
49 - items
50 properties:
51 id:
52 type: string
53 example: 2ef749d9-b25e-49df-8ff3-54f3873fffb8
54 name:
55 type: string
56 example: Aotoki
57 items:
58 type: array
59 minimum: 1
60 items:
61 $ref: '#/components/schemas/OrderItem'
62 OrderItem:
63 type: object
64 required:
65 - name
66 - quantity
67 - unit_price
68 properties:
69 name:
70 type: string
71 example: Apple
72 quantity:
73 type: number
74 minimum: 1
75 example: 1
76 unit_price:
77 type: number
78 minimum: 1
79 example: 10
這份文件有一定的長度,同時我們會明確的定義 Schema(結構)這對於後續的實作會有相當大的幫助,同時也可以幫我們釐清資料的樣子。
伺服器定義
之所以要先撰寫 API 文件,是因為在 Clean Architecture 中 Controller 是一種 Adapter(轉接器)的角色,因此實際上需要我們處理的只有「找到對應的使用情境」這一個角色。
在 MVC 框架的情境中,我們會將業務邏輯(Application Business Rule)實作在 Controller 裡面,然而這樣就會跟框架耦合在一起,然而只要將這部分實作移動到 UseCase 上,我們需要做的事情就相對單純很多。
這系列會使用 oapi-codegen 這個 Golang 套件來進行生成,詳細的操作與設定可以參考 GitHub 上的說明,接下來我們可以看一下 internal/api/rest/rest.go
中的實作。
1//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml openapi.yaml
2package rest
3
4import (
5 "net/http"
6
7 "github.com/go-chi/chi/v5"
8 "github.com/go-chi/chi/v5/middleware"
9 "github.com/google/wire"
10
11 nethttpmiddleware "github.com/oapi-codegen/nethttp-middleware"
12)
13
14var DefaultSet = wire.NewSet(
15 chi.NewRouter,
16 wire.Struct(new(Api), "*"),
17 NewServer,
18)
19
20var _ ServerInterface = &Api{}
21
22type Api struct {
23}
24
25var _ http.Handler = &Server{}
26
27type Server struct {
28 router *chi.Mux
29}
30
31func NewServer(router *chi.Mux, api *Api) (*Server, error) {
32 apiDoc, err := GetSwagger()
33 if err != nil {
34 return nil, err
35 }
36
37 router.Use(nethttpmiddleware.OapiRequestValidator(apiDoc))
38 router.Use(middleware.Logger)
39 router.Use(middleware.Recoverer)
40 HandlerFromMux(api, router)
41
42 return &Server{router: router}, nil
43}
44
45func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
46 s.router.ServeHTTP(w, r)
47}
我們標記了要使用 go:generate
來根據 openapi.yaml
作為基準,並且會產生出 ServerInterface
這個介面,接下來只需要對我們使用的 HTTP 框架(如:chi
)呼叫 HandlerFromMux
進行註冊即可。
同時 place_order.go
這個檔案中,我們實作了 ServerInterface
的介面。
1package rest
2
3import "net/http"
4
5func (s *Api) PlaceOrder(w http.ResponseWriter, r *http.Request) {
6 w.WriteHeader(http.StatusOK)
7}
透過這樣的流程,我們幾乎不需要再手動處理請求 Route(路由)登記,接下來只需要專注在 UseCase 的實作上。
為了後續依賴注入處理方便,現階段的設計還會再做調整。
測試實作
上述的處理後,我們可以加入測試來進行驗證。因為 API 的定義已經完成,因此可以很簡單的用 Cucumber 這類工具撰寫如下的測試。
1Feature: Order
2 Scenario: I can place an order
3 When make a POST request to "/orders"
4 """
5 {
6 "name": "Aotoki",
7 "items": [
8 {
9 "name": "Apple",
10 "quantity": 2,
11 "unit_price": 10
12 },
13 {
14 "name": "Banana",
15 "quantity": 3,
16 "unit_price": 5
17 }
18 ]
19 }
20 """
21 Then the response status code should be 200
關於 Golang 的 Cucumber 測試框架 godog 的設定在這系列中不會詳細描述,可以參考 clean-architecture-in-go-2025 中的範例。
這一整套流程只有在初期會花費比較多的時間,後續進行擴充的速度就會加快不少,是很直得投入一些資源完善的機制。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 連載介紹 - 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