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

操作介面設計 - Clean Architecture in Go

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

假設要從零開始開發,相對好的方式是以黑箱的角度思考。也就是我們以「使用者看到的樣子」開始建構系統,並且在過程中逐步的「提煉」將 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 中的範例。

這一整套流程只有在初期會花費比較多的時間,後續進行擴充的速度就會加快不少,是很直得投入一些資源完善的機制。