---
title: "操作介面設計 - Clean Architecture in Go"
date: 2025-01-31T00:00:00+08:00
publishDate: 2025-01-31T00:00:00+08:00
lastmod: 2024-10-07T20:05:43+08:00
tags: ["Golang","Clean Architecture","架構","經驗"]
series: "clean-architecture-in-go"
toc: true
permalink: "https://blog.aotoki.me/posts/2025/01/31/clean-architecture-in-go-operation-interface/"
language: "zh-tw"
---


假設要從零開始開發，相對好的方式是以黑箱的角度思考。也就是我們以「使用者看到的樣子」開始建構系統，並且在過程中逐步的「提煉」將 UseCase、Entity 從系統中提取出來。

這種做法在使用 BDD（Behavior-Driven Development）或者 TDD（Test-Driven Development）也能幫助我們設計出一些初期的測試案例。

<!--more-->

## 撰寫文件{#write-document}

我們要撰寫的是以 OpenAPI 3.0 規範為基礎的文件，在規劃文件之前可能已經經過了產品規格相關的討論，也理解到一些使用者案例，因此我們大致上知道會存在 Place Order（下單）和 Lookup Order（查詢）兩個行為。

在這邊我們先以接下來會實作的 Place Order 為例子，撰寫一份 `openapi.yaml`

```yaml
openapi: 3.0.0
info:
  title: Clean Architecture in Go
  version: 1.0.0
tags:
  - name: order
    description: The orders in the service
paths:
  /orders:
    post:
      summary: Place a new order
      operationId: placeOrder
      tags:
        - order
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PlaceOrderRequest'
      responses:
        200:
          description: Place order successful
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlaceOrderResponse'

components:
  schemas:
    PlaceOrderRequest:
      type: object
      required:
        - name
        - items
      properties:
        name:
          type: string
          example: Aotoki
        items:
          type: array
          minimum: 1
          items:
            $ref: '#/components/schemas/OrderItem'
    PlaceOrderResponse:
      type: object
      required:
        - id
        - name
        - items
      properties:
        id:
          type: string
          example: 2ef749d9-b25e-49df-8ff3-54f3873fffb8
        name:
          type: string
          example: Aotoki
        items:
          type: array
          minimum: 1
          items:
            $ref: '#/components/schemas/OrderItem'
    OrderItem:
      type: object
      required:
        - name
        - quantity
        - unit_price
      properties:
        name:
          type: string
          example: Apple
        quantity:
          type: number
          minimum: 1
          example: 1
        unit_price:
          type: number
          minimum: 1
          example: 10
```

這份文件有一定的長度，同時我們會明確的定義 Schema（結構）這對於後續的實作會有相當大的幫助，同時也可以幫我們釐清資料的樣子。

## 伺服器定義{#codegen}

之所以要先撰寫 API 文件，是因為在 Clean Architecture 中 Controller 是一種 Adapter（轉接器）的角色，因此實際上需要我們處理的只有「找到對應的使用情境」這一個角色。

在 MVC 框架的情境中，我們會將業務邏輯（Application Business Rule）實作在 Controller 裡面，然而這樣就會跟框架耦合在一起，然而只要將這部分實作移動到 UseCase 上，我們需要做的事情就相對單純很多。

這系列會使用 [oapi-codegen](https://github.com/oapi-codegen/oapi-codegen) 這個 Golang 套件來進行生成，詳細的操作與設定可以參考 GitHub 上的說明，接下來我們可以看一下 `internal/api/rest/rest.go` 中的實作。

```go
//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml openapi.yaml
package rest

import (
	"net/http"

	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
	"github.com/google/wire"

	nethttpmiddleware "github.com/oapi-codegen/nethttp-middleware"
)

var DefaultSet = wire.NewSet(
	chi.NewRouter,
	wire.Struct(new(Api), "*"),
	NewServer,
)

var _ ServerInterface = &Api{}

type Api struct {
}

var _ http.Handler = &Server{}

type Server struct {
	router *chi.Mux
}

func NewServer(router *chi.Mux, api *Api) (*Server, error) {
	apiDoc, err := GetSwagger()
	if err != nil {
		return nil, err
	}

	router.Use(nethttpmiddleware.OapiRequestValidator(apiDoc))
	router.Use(middleware.Logger)
	router.Use(middleware.Recoverer)
	HandlerFromMux(api, router)

	return &Server{router: router}, nil
}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	s.router.ServeHTTP(w, r)
}
```

我們標記了要使用 `go:generate` 來根據 `openapi.yaml` 作為基準，並且會產生出 `ServerInterface` 這個介面，接下來只需要對我們使用的 HTTP 框架（如：`chi`）呼叫 `HandlerFromMux` 進行註冊即可。

同時 `place_order.go` 這個檔案中，我們實作了 `ServerInterface` 的介面。

```go
package rest

import "net/http"

func (s *Api) PlaceOrder(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusOK)
}
```

透過這樣的流程，我們幾乎不需要再手動處理請求 Route（路由）登記，接下來只需要專注在 UseCase 的實作上。

> 為了後續依賴注入處理方便，現階段的設計還會再做調整。
## 測試實作{#testing}

上述的處理後，我們可以加入測試來進行驗證。因為 API 的定義已經完成，因此可以很簡單的用 Cucumber 這類工具撰寫如下的測試。

```gherkin
Feature: Order
  Scenario: I can place an order
    When make a POST request to "/orders"
    """
    {
      "name": "Aotoki",
      "items": [
        {
          "name": "Apple",
          "quantity": 2,
          "unit_price": 10
        },
        {
          "name": "Banana",
          "quantity": 3,
          "unit_price": 5
        }
      ]
    }
    """
    Then the response status code should be 200
```

關於 Golang 的 Cucumber 測試框架 [godog](https://github.com/cucumber/godog) 的設定在這系列中不會詳細描述，可以參考 [clean-architecture-in-go-2025](https://github.com/elct9620/clean-architecture-in-go-2025) 中的範例。

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

