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


Controller 和 UseCase 都有對應的實作後，我們還需要定義在這個系統中負責管理狀態的 Entity（實體）才算是有一個基礎的系統雛形。在這個例子中，我們會需要 Order 和 OrderItem 這兩種實體。

<!--more-->

## 定義 Entity {#define-entity}

在 Place Order 的情境中，我們會需要一個訂單來紀錄客戶名稱，以及一個的列表來紀錄使用者購買的項目，同時我們並不希望項目重複。

因此，我們可以做出如下的實作。

```go
// internal/entity/orders/item.go
package orders

type Item struct {
	name      string
	quantity  int
	unitPrice int
}

func (i *Item) Name() string {
	return i.name
}

func (i *Item) Quantity() int {
	return i.quantity
}

func (i *Item) UnitPrice() int {
	return i.unitPrice
}
```

```go
// internal/entity/orders/order.go
package orders

import "errors"

var (
	ErrItemNameMustBeUnique = errors.New("item name must be unique")
)

type Order struct {
	id           string
	customerName string
	items        []*Item
}

func New(id string, customerName string) *Order {
	return &Order{
		id:           id,
		customerName: customerName,
		items:        []*Item{},
	}
}

func (o *Order) Id() string {
	return o.id
}

func (o *Order) CustomerName() string {
	return o.customerName
}

func (o *Order) Items() []*Item {
	return o.items
}

func (o *Order) HasItem(name string) bool {
	for _, item := range o.items {
		if item.name == name {
			return true
		}
	}

	return false
}

func (o *Order) AddItem(name string, quantity int, unitPrice int) error {
	if o.HasItem(name) {
		return ErrItemNameMustBeUnique
	}

	o.items = append(o.items, &Item{
		name:      name,
		quantity:  quantity,
		unitPrice: unitPrice,
	})

	return nil
}
```

和 Controller 跟 UseCase 使用的結構（Struct）不同的地方在於所有欄位（Field）都是私有的，這是因為我們並不預期使用者透過我們封裝的方法以外的方式改變狀態，這樣才能確保每一個變動都有適當的檢查（如：`AddItem` 會做 `HasItem` 的確認）

這也是為什麼我們無法直接使用 ORM 產生的結構來作為 Entity 的理由，因為 ORM 產生的結構都是開放的，很可能在我們預期以外的情況下被修改，最終導致得到我們期望以外的結果。

## 更新 UseCase {#update-usecase}

原本的 UseCase 只是單純地將輸入轉換到輸出，有了 Entity 後就可以實際的使用 Entity 來進行處理，看起來就更接近真實的使用情境。

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

// ...
func (u *PlaceOrder) Execute(ctx context.Context, input *PlaceOrderInput) (*PlaceOrderOutput, error) {
	order := orders.New(
		uuid.NewString(),
		input.Name,
	)

	for _, item := range input.Items {
		err := order.AddItem(item.Name, item.Quantity, item.UnitPrice)
		if err != nil {
			return nil, err
		}
	}

	out := &PlaceOrderOutput{
		Id:    order.Id(),
		Name:  order.CustomerName(),
		Items: []PlaceOrderItem{},
	}

	for _, item := range order.Items() {
		out.Items = append(out.Items, PlaceOrderItem{
			Name:      item.Name(),
			Quantity:  item.Quantity(),
			UnitPrice: item.UnitPrice(),
		})
	}

	return out, nil
}
```

上述的實作看起來似乎只是增加了 `orders.New()` 並且展開輸入放到回傳的內容，似乎沒有太過於特別的地方。

然而，我們在 `order.AddItem()` 時因為會檢查是否有名稱上的重複，因此在 UseCase 發生的錯誤就能以各種形式回傳給使用者，類似下面的測試案例。

```gherkin
Feature: Place order
  Scenario: Cannot place order with same item name
    When make a POST request to "/orders"
    """
    {
      "name": "Aotoki",
      "items": [
        {
          "name": "Apple",
          "quantity": 2,
          "unit_price": 10
        },
        {
          "name": "Apple",
          "quantity": 3,
          "unit_price": 5
        }
      ]
    }
    """
    Then the response status code should be 400
    And the response body should be "item name must be unique"
```

當我們傳入多個品項，但名稱重複的時候，就會在 Entity 的情境中被偵測到，並且告知使用者。這裡比較難以區分的是 Controller 接收輸入時的 Validation（驗證）以及操作中發生的錯誤，像是「不能小於 0」這樣的情境。

在 Controller 中會對此做檢查，是因為小於 0 不是一個合理的範圍。然而在 Entity 中處理時，會發生這樣的錯誤，會是因為 `Decrement(1)` 這樣的方法造成操作超出預期的範圍，即使錯誤訊息相同，仍有不同的意義。

## 封裝{#encapsulation}

Entity 的實作過程是一個不錯的封裝（Encapsulation）案例，我們希望物件可以很好的管理內部狀態，因此不允許直接存取這些欄位。同時透過實作 Getter 的方法，開放出允許存取的欄位以唯讀的方式取得。

若要改變狀態，那麼只能夠過設計好的方法操作，這樣一來使用者就不會用我們預期之外的方法使用，在 UseCase 進行操作的時候，就不容易出現為了方便或者理解錯誤，錯誤的修改物件狀態的情境。

同時，如果要拓展可以操作的行為，也會回到 Entity 上修改，如果對於 Entity 的角色跟定位有理解錯誤，也就更容易在程式碼審查（Code Review）階段被注意到。

