
輸入檢查 - Clean Architecture in Go
在前面的案例中,我們並沒有考量到「輸入檢查」這件事情,因此在 gRPC 的案例中,我們發現即使沒有填寫 Name
也可以順利建立訂單,這並不符合我們的期待。
驗證時機
驗證(Validate)到底該在什麼時機點進行,一直以來都沒有一個容易判斷的方式。有些框架會設計在 Model 上,也就是在持久化時才進行檢查,有些則是會附加在 Controller 上,做為參數輸入時就要確認。
然而,我們在處理的過程中會出現好幾種不同的情境。
第一種是編碼(Marshalling)的處理,像是 Golang 將 JSON 反序列化(Deserialize)為一個 Struct 時,如果跟結構的型別無法對應,那麼會發生錯誤。
第二種則是輸入數值的處理,像是 Name
這個必填欄位沒有填寫,價格不能為 0
這類情境,也是我們比較常在 Web 框架上會看到的類型。
第三種則是行為上的處理,我們在 Order
的 AddItem()
方法,設計了一個檢查商品名稱不能重複的確認,這種檢查通常會跟我們的狀態有關,只有 Entity 本身才能判斷。
也就是說,以 Clean Architecture 的設計來看,我們的驗證處理分別在 Adapter 的 Controller、Repository 會發生關於「編碼」的檢查,到了 UseCase 則有資訊是否滿足的檢查,以及 Entity 的狀態是否正確的檢查。
那麼,訂單沒有填入姓名、商品的單價和數量不對,應該是第二種類型。
Validator Package
要在 UseCase 加入驗證,我們應該要依照 UseCase 不會知道細節的原則,因此在 UseCase 定義一個 Validator
介面,描述驗證的需求。
1// internal/usecase/usecase.go
2
3type Validator interface {
4 Validate(ctx context.Context, input any) error
5}
修改 Place Order UseCase 加入驗證的步驟,這裡我們選擇使用 validator 這個套件,利用 Golang 的 Struct Tags 就不會有直接的依賴關係。
1// ...
2type PlaceOrderItem struct {
3 Name string `validate:"required"`
4 Quantity int `validate:"required,gte=1"`
5 UnitPrice int `validate:"required,gte=1"`
6}
7
8type PlaceOrderInput struct {
9 Name string `validate:"required"`
10 Items []PlaceOrderItem `validate:"required,gt=0,dive,required"`
11}
12
13// ...
14
15type PlaceOrder struct {
16 orders OrderRepository
17 tokens TokenRepository
18 validator Validator
19}
20
21func NewPlaceOrder(orders OrderRepository, tokens TokenRepository, validator Validator) *PlaceOrder {
22 return &PlaceOrder{
23 orders: orders,
24 tokens: tokens,
25 validator: validator,
26 }
27}
28
29func (u *PlaceOrder) Execute(ctx context.Context, input *PlaceOrderInput) (*PlaceOrderOutput, error) {
30 if err := u.validator.Validate(ctx, input); err != nil {
31 return nil, err
32 }
33
34 // ...
35}
像這樣,我們就可以運行這個 UseCase 之前確定必填的欄位是否都有填寫,以及數量、單價不能為 0
。
使用 Golang 的 Struct Tags 類似於 Java 的 Annotation 都是透過語言的反射(Reflection)機制來達成的效果,因為是 Metadata 的形式,也就不會直接依賴於某個套件。
因為不同套件使用的 Struct Tags 不同,因此還是一定程度受到影響,然而當我們要抽換時只要使用相同方式就能夠做替換。
最後,我們只需要在 internal/validator
加入簡單的封裝即可。
1// internal/validator/validator.go
2
3// ...
4
5type Validator struct {
6 validate *validator.Validate
7}
8
9func New() *Validator {
10 return &Validator{
11 validate: validator.New(),
12 }
13}
14
15func (v *Validator) Validate(ctx context.Context, i any) error {
16 return v.validate.StructCtx(ctx, i)
17}
之後只需要利用像是 wire
這類工具,針對有 Validator
介面需求的 UseCase 注入這個物件,就可以快速地提供驗證機制。
難以統整
雖然我們將驗證的發生時機點設定在 UseCase 上,但是還是有很多不同的情境。以 OpenAPI 生態系來說,我們是可以在 HTTP 請求階段就做處理。
以 kin-openapi 這個套件來說,他提供了一個 ValidateRequest
的機制,可以檢查發過來的 HTTP 請求和 API 文件是否一致,包含 HTTP Status Code 和支援的內容類型(Content-Type)都可以處理。
這個做法可以很好的改善開發過程中對 API 處理的失誤,但裡面很大一部分的處理都跟 UseCase 的驗證重疊,假設我們使用了這個機制進行驗證,那麼 UseCase 的驗證基本上是不會觸發的。但是,在 gRPC 的角度來看 Protobuf 定位是跟 JSON 差不多的角色,因此 Protobuf 本身無法做任何額外的驗證方式標記。
這就造成了我們可能在使用 OpenAPI 方式設計 RESTful API 時沒有注意到驗證問題,當支援了 gRPC 協定時,就因為各種輸入驗證的缺少而造成問題。
除此之外,OpenAPI 驗證的訊息跟我們自己封裝的 Validator 也會有差異,這也會造成後續在測試時因為不一致而導致的混亂,這些都會是我們未來會需要去處理和改善的問題。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 連載介紹 - 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
- Tokenization 機制設計 - Clean Architecture in Go
- 在 Place Order 實作 Token 機制 - Clean Architecture in Go
- 在 Lookup Order 實作 Token 機制 - Clean Architecture in Go
- Token 內容加密 - Clean Architecture in Go
- gRPC Server 準備 - Clean Architecture in Go
- gRPC Server 實作 - Clean Architecture in Go
- 輸入檢查 - Clean Architecture in Go