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


處理完在 Place Order 時將顧客名字轉換成代號（Token）進行儲存的處理後，我們也需要讓讀取出來的資料能將資訊轉換回原本的樣子，假設系統已經運行一段時間，我們還需要區分是否是尚未進行代號處理的版本，還是已經被處理過。

<!--more-->

## Lookup Order

 因為我們已經有 Token Entity 的實作，接下來只需要定義 `TokenRepository` 如何對一個代號查詢，並且將資料呈現出來的邏輯在我們的 Use Case 即可。

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

// ...

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

在這邊我們會使用 `token` 而不是 `id` 的原因，是因為我們在查詢的時候是採用 `v1:ca244c41-596e-4540-a9e3-afe270b62537` 這樣的代號格式，而不是 Token 的 ID 本身。

完成介面定義後，我們就可以把 Lookup Order 的實作調整成新的版本。

```go
// internal/usecase/lookup_order.go

// ...

type LookupOrder struct {
	orders OrderRepository
	tokens TokenRepository
}

func NewLookupOrder(orders OrderRepository, tokens TokenRepository) *LookupOrder {
	return &LookupOrder{
		orders: orders,
		tokens: tokens,
	}
}

func (u *LookupOrder) Execute(ctx context.Context, input *LookupOrderInput) (*LookupOrderOutput, error) {
	order, err := u.orders.Find(ctx, input.Id)
	if err != nil {
		return nil, err
	}

	customerName := order.CustomerName()
	if nameToken, err := u.tokens.Find(ctx, order.CustomerName()); err == nil {
		customerName = string(nameToken.Data())
	}

	out := &LookupOrderOutput{
		Id:    order.Id(),
		Name:  customerName,
		Items: []LookupOrderItem{},
	}

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

	return out, nil
}
```

在這裡我們選擇將 `nameToken` 查詢時的錯誤無視，假設沒辦法找到任何代號，我們會直接使用原本的 `order.CustomerName()` 來呈現，這樣舊版本的資料就能繼續的相容在新版本中。

## TokenRepository

接下來我們要將原本沒有實作 `Find()` 方法的 `TokenRepository` 增加對應的實作，我們需要將 `[Version]:[ID]` 中以 `:` 切開，拿到 `id` 進行查詢，至於 `version` 的用途會在下一個階段使用到，我們可以先暫時當作一個區分數值的參考。

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

// ...

func (r *InMemoryTokenRepository) Find(ctx context.Context, tokenStr string) (*tokens.Token, error) {
	segments := strings.SplitN(tokenStr, ":", 2)
	if len(segments) != 2 {
		return nil, tokens.ErrTokenNotFound
	}

	id := segments[1]
	token, ok := r.tokens[id]
	if !ok {
		return nil, tokens.ErrTokenNotFound
	}

	return tokens.New(
		token.Id,
		tokens.WithVersion(token.Version),
		tokens.WithData(token.Data),
	), nil
}
```

查詢的方式也不複雜，我們先使用 `strings.SplitN()` 分離 `version` 和 `id`，再把 `id` 拿去查詢，就可以得到我們在 Place Order 操作中保存的代號。

## Token Entity

接下來我們只需要傳回一個新的 `Token` 即可，為了方便還原狀態增加了可以在初始化時帶入額外資訊的實作。

> 從物件封裝的角度來看，這個也允許使用者提供任意數值破壞了物件本身對操作的檢查，因此在 Golang 提供這個操作方式時需要仔細評估是否適用。

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

// ...

type TokenOption func(*Token)

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

func New(id string, opts ...TokenOption) *Token {
	token := &Token{
		id:      id,
		version: CurrentVersion,
	}

	for _, opt := range opts {
		opt(token)
	}

	return token
}

// ...

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

func WithData(data []byte) func(*Token) {
	return func(t *Token) {
		t.data = data
	}
}
```

到此為止，我們在幾乎不影響現有系統的狀態下，重構了整個系統流程，並且加入新的機制。Clean Architecture  透過應用 [SOLID](https://zh.wikipedia.org/zh-tw/SOLID_(%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E8%AE%BE%E8%AE%A1)) 來讓我們在可維護性上得到一個不錯的平衡，也讓我們能夠了解到「清楚區分職責」對修改系統的好處。

然而，在 Tokenization 機制的實現中，我們並沒有討論到「加密和解密」的處理，我們應該將這個處理放在哪裡呢？接下來讓我們一起來探討這個問題。

