---
title: "Place Order 實作 Controller 部分 - Clean Architecture in Go"
date: 2025-02-07T00:00:00+08:00
publishDate: 2025-02-07T00: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/02/07/clean-architecture-in-go-place-order-controller/"
language: "zh-tw"
---


我們已經透過 [oapi-codegen](https://github.com/oapi-codegen/oapi-codegen) 產生了 Controller 的介面定義，然而還沒有任何實作。在 Clean Architecture 中 Controller 要扮演 Adapter（轉接器）的角色，因此我們需要透過 Controller 把使用者的輸入轉換成 UseCase 可以使用的格式。

<!--more-->

## 定義 UseCase {#define-usecase}

要處理 Controller 的實作，我們需要先確定 Place Order 所需要的資料和回傳為何，因此我們會需要先定義 UseCase 的介面。

```go
// internal/usecase/place_order.go

type PlaceOrderItem struct {
	Name      string
	Quantity  int
	UnitPrice int
}

type PlaceOrderInput struct {
	Name  string
	Items []PlaceOrderItem
}

type PlaceOrderOutput struct {
	Id    string
	Name  string
	Items []PlaceOrderItem
}

type PlaceOrder struct {
}

func NewPlaceOrder() *PlaceOrder {
	return &PlaceOrder{}
}

func (u *PlaceOrder) Execute(ctx context.Context, input *PlaceOrderInput) (*PlaceOrderOutput, error) {
	return &PlaceOrderOutput{
		Id:    uuid.NewString(),
		Name:  input.Name,
		Items: input.Items,
	}, nil
}
```

上述這段程式碼看起來會跟 Controller 幾乎相同，在 Controller 我們可以透過 oapi-codegen 根據 OpenAPI 文件產生，這邊卻是要手動撰寫，看起來似乎是「重工」的情況。

然而，Controller 會因為不同的框架、格式有不一樣的實作，在 UseCase 則會是相同的，假設未來我們需要將 Order Service 切割成一個獨立的 Microservice（維服務）才能直接把 UseCase 搬移，而不需要從 Controller 抽離程式碼。

因為這樣的原因，我們才得以實踐 Clean Architecture 期望處理的情境，能夠輕鬆的替換底層的框架，甚至在其他地方重新實現。

## 實作 Controller {#implement-controller}

我們已經確定 UseCase 該有怎樣的介面後，就可以在 Controller 上把這個介面串接上去，並且將使用者的輸入轉換成適合的格式。

首先，我們會利用 [wire](https://github.com/google/wire) 來幫助我們依賴注入，之前特意設計了 `Api` 這個結構就可以被我們用來描述會被注入的 UseCase 有哪些。

```go
// internal/api/rest/rest.go

var _ ServerInterface = &Api{}

type Api struct {
	PlaceOrderUsecase *usecase.PlaceOrder
}
```

接著完成 Controller 的實作，我們就可以通過以下的測試。

```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
    And the response JSON contains "id" string
    And the response JSON contains "name" with value "Aotoki"
    And the response JSON contains "items[0].name" with value "Apple"
    And the response JSON contains "items[0].quantity" with value 2
    And the response JSON contains "items[0].unit_price" with value 10
    And the response JSON contains "items[1].name" with value "Banana"
    And the response JSON contains "items[1].quantity" with value 3
    And the response JSON contains "items[1].unit_price" with value 5
```

```go
// internal/api/rest/place_order.go

package rest

import (
	"encoding/json"
	"net/http"

	"github.com/elct9620/clean-architecture-in-go-2025/internal/usecase"
)

func (api *Api) PlaceOrder(w http.ResponseWriter, r *http.Request) {
	var req PlaceOrderRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	input := buildPlaceOrderInput(req)

	out, err := api.PlaceOrderUsecase.Execute(r.Context(), input)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	res := buildPlaceOrderResponse(out)
	if err := json.NewEncoder(w).Encode(res); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

func buildPlaceOrderInput(req PlaceOrderRequest) *usecase.PlaceOrderInput {
	inItems := make([]usecase.PlaceOrderItem, 0, len(req.Items))
	for _, item := range req.Items {
		inItems = append(inItems, usecase.PlaceOrderItem{
			Name:      item.Name,
			Quantity:  int(item.Quantity),
			UnitPrice: int(item.UnitPrice),
		})
	}

	return &usecase.PlaceOrderInput{
		Name:  req.Name,
		Items: inItems,
	}
}

func buildPlaceOrderResponse(out *usecase.PlaceOrderOutput) *PlaceOrderResponse {
	outItems := make([]OrderItem, 0, len(out.Items))
	for _, item := range out.Items {
		outItems = append(outItems, OrderItem{
			Name:      item.Name,
			Quantity:  float32(item.Quantity),
			UnitPrice: float32(item.UnitPrice),
		})
	}

	return &PlaceOrderResponse{
		Id:    out.Id,
		Name:  out.Name,
		Items: outItems,
	}
}
```

從經驗上來看，上面的實作似乎是沒必要的消耗，然而這剛好就是 Controller 最主要的功能之一，一個是驗證輸入的資料格式，另一個則是將這些格式轉換成 UseCase 可以使用的資料。

舉例來說，在 JSON 中採用了 `float32` 的型別，然而在我們的設計 `Quantity` 和 `UnitPrice` 都是 `int` 型別，那麼就需要有人處理，只要在 Controller 都處理完善後，後續的流程就不需要再擔心會發生預期外的情況。

## 必要性 {#necessary}

我們在學習後端相關知識的時候，大多會從採用 MVC 的框架開始學起，因此會對於上述這樣的做法感到疑惑，覺得「有必要嗎？」

根據這幾年實踐的經驗來看，假設整個系統非常單純的時候，是可以不這樣做的。畢竟我們很少遇到需要同時支援 RESTful API 和 gRPC 的情境，然而當我們開始有 APIv1 和 APIv2 這樣的差異時，到是一個不錯的時機點進行重構。

舉例來說，假設 APIv1 的時候，我們預期回傳只提供 `id` 欄位。

```json
{
    "id": "a621f661-e620-4926-b0f4-c726096c93b9"
}
```

然而到了 APIv2 為了讓前端可以更容易處理，我們希望將 Name 和 Order Items 也都呈現出來，因此要改為下面的樣子。

```json
{
    "id": "a621f661-e620-4926-b0f4-c726096c93b9",
    "name": "Aotoki",
    "items": [
        {
          "name": "Apple",
          "quantity": 2,
          "unit_price": 10
        },
        {
          "name": "Banana",
          "quantity": 3,
          "unit_price": 5
        }
      ]
    }
```

這個時候如果我們在處理建立訂單的邏輯是跟 Controller 綁定的，那麼就會出現需要複製相似的程式碼到 APIv2 的 Controller 中，然而有 UseCase 的狀況下，只是 `buildPlaceOrderResponse` 這個 Helper 的實作差異。

同樣的情境也可以套用到未來如果我們在 APIv2 想要改由從 API Token 取得下單者名稱，所以要拿掉 `name` 欄位的輸入，實際上背後的處理還是會用到，因此我們只需要在 Controller 階段做一些處理，就可以讓這些機制順利地被實現。

