---
title: "在 Place Order 實作 Token 機制 - Clean Architecture in Go"
date: 2025-03-14T00:00:00+08:00
publishDate: 2025-03-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/03/14/clean-architecture-in-go-add-token-to-place-order/"
language: "zh-tw"
---


我們已經對於 Tokenization 機制的設計有大致的方向，那麼就可以開始來對 Place Order 的流程進行調整，將原本直接把客戶名稱儲存到 Order 改為先向 Token 機制要求產生一個代號，再作為原本的替代儲存進去。

<!--more-->

## Token Entity

Token 的實作並不複雜，我們只需要處理三項資訊。

* 代號
* 版本
* 資料

因此可以像這樣實作

```go
// internal/entity/tokens/token.go
package tokens

// ...

const (
	CurrentVersion = "v1"
)

type Token struct {
	id      string
	data    []byte
	version string
}

func New(id string) *Token {
	return &Token{
		id:      id,
		version: CurrentVersion,
	}
}

func (t *Token) Id() string {
	return t.id
}

func (t *Token) Data() []byte {
	return t.data
}

func (t *Token) Version() string {
	return t.version
}

func (t *Token) SetData(data []byte) {
	t.data = data
}

func (t Token) String() string {
	return t.version + ":" + t.id
}
```

不會太過複雜，主要就是將必要的資訊進行簡單的封裝，有這樣的機制就非常足夠我們在後續的實作中使用。

## Place Order

有了 Token 後，我們就可以更新 Place Order Use Case 的流程，在原本的流程中插入一段 `nameToken` 的處理機制，關於 `TokenRepository` 我們會在後續討論相應的實作。

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

type PlaceOrder struct {
	orders OrderRepository
	tokens TokenRepository
}

func NewPlaceOrder(orders OrderRepository, tokens TokenRepository) *PlaceOrder {
	return &PlaceOrder{
		orders: orders,
		tokens: tokens,
	}
}

func (u *PlaceOrder) Execute(ctx context.Context, input *PlaceOrderInput) (*PlaceOrderOutput, error) {
	nameToken := tokens.New(uuid.NewString())
	nameToken.SetData([]byte(input.Name))

	if err := u.tokens.Save(ctx, nameToken); err != nil {
		return nil, err
	}

	order := orders.New(
		uuid.NewString(),
		nameToken.String(),
	)

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

	if err := u.orders.Save(ctx, order); err != nil {
		return nil, err
	}

	out := &PlaceOrderOutput{
		Id:    order.Id(),
		Name:  string(nameToken.Data()),
		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
}
```

大部分的內容跟我們在設計 Order 的實作是差不多的，只是在這裡我們多出了 `nameToken` 的處理，產生了一個 `Token` 用於替代原本明碼的顧客名稱。

## TokenRepository

處理上也跟 OrderRepository 上類似，我們在 `internal/usecase` 描述需要的介面，再由 `internal/repository` 來實作對應的機制。

```go
// internal/usecase/repository.go

// ...

type TokenRepository interface {
	Save(ctx context.Context, token *tokens.Token) error
}
```

在開發初期，我們可以使用 `InMemoryTokenRepository` 來對應撰寫初期測試所需的版本，因此跟 `InMemoryOrderRepository` 相同快速的實作一個類似的實作。

```go
// internal/repository/in_memory_tokens.go

// ...

type InMemoryTokenSchema struct {
	Id      string
	Data    []byte
	Version string
}

type InMemoryTokenRepository struct {
	tokens map[string]InMemoryTokenSchema
}

func NewInMemoryTokenRepository() *InMemoryTokenRepository {
	return &InMemoryTokenRepository{
		tokens: map[string]InMemoryTokenSchema{},
	}
}

func (r *InMemoryTokenRepository) Save(ctx context.Context, token *tokens.Token) error {
	r.tokens[token.Id()] = InMemoryTokenSchema{
		Id:      token.Id(),
		Data:    token.Data(),
		Version: token.Version(),
	}

	return nil
}
```

因為我們在前期打下的基礎，在要進行擴充的時候並不會耗費太多力氣。受限於文章篇幅，我們使用的案例並不會太過複雜，因此現階段即使將 UseCase 的實作放在 Controller 中也不會有太大的差異，在後續加入像是 gRPC、真實的資料庫整合案例後，就可以逐步看出 Clean Architecture 對這幾大類物件的區分，是怎麼幫助我們更快的擴充系統能力。

