蒼時弦也
蒼時弦也
資深軟體工程師
發表於

在 Lookup Order 實作 Token 機制 - Clean Architecture in Go

這篇文章是 Clean Architecture in Go 系列的一部分,你可以透過 Leanpub 提前閱讀內容。

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

Lookup Order

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

1// internal/usecase/repository.go
2
3// ...
4
5type TokenRepository interface {
6	Find(ctx context.Context, token string) (*tokens.Token, error)
7	Save(ctx context.Context, token *tokens.Token) error
8}

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

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

 1// internal/usecase/lookup_order.go
 2
 3// ...
 4
 5type LookupOrder struct {
 6	orders OrderRepository
 7	tokens TokenRepository
 8}
 9
10func NewLookupOrder(orders OrderRepository, tokens TokenRepository) *LookupOrder {
11	return &LookupOrder{
12		orders: orders,
13		tokens: tokens,
14	}
15}
16
17func (u *LookupOrder) Execute(ctx context.Context, input *LookupOrderInput) (*LookupOrderOutput, error) {
18	order, err := u.orders.Find(ctx, input.Id)
19	if err != nil {
20		return nil, err
21	}
22
23	customerName := order.CustomerName()
24	if nameToken, err := u.tokens.Find(ctx, order.CustomerName()); err == nil {
25		customerName = string(nameToken.Data())
26	}
27
28	out := &LookupOrderOutput{
29		Id:    order.Id(),
30		Name:  customerName,
31		Items: []LookupOrderItem{},
32	}
33
34	for _, item := range order.Items() {
35		out.Items = append(out.Items, LookupOrderItem{
36			Name:      item.Name(),
37			Quantity:  item.Quantity(),
38			UnitPrice: item.UnitPrice(),
39		})
40	}
41
42	return out, nil
43}

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

TokenRepository

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

 1// internal/repository/in_memory_tokens.go
 2
 3// ...
 4
 5func (r *InMemoryTokenRepository) Find(ctx context.Context, tokenStr string) (*tokens.Token, error) {
 6	segments := strings.SplitN(tokenStr, ":", 2)
 7	if len(segments) != 2 {
 8		return nil, tokens.ErrTokenNotFound
 9	}
10
11	id := segments[1]
12	token, ok := r.tokens[id]
13	if !ok {
14		return nil, tokens.ErrTokenNotFound
15	}
16
17	return tokens.New(
18		token.Id,
19		tokens.WithVersion(token.Version),
20		tokens.WithData(token.Data),
21	), nil
22}

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

Token Entity

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

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

 1// internal/entity/tokens/token.go
 2
 3// ...
 4
 5type TokenOption func(*Token)
 6
 7type Token struct {
 8	id      string
 9	data    []byte
10	version string
11}
12
13func New(id string, opts ...TokenOption) *Token {
14	token := &Token{
15		id:      id,
16		version: CurrentVersion,
17	}
18
19	for _, opt := range opts {
20		opt(token)
21	}
22
23	return token
24}
25
26// ...
27
28func WithVersion(version string) func(*Token) {
29	return func(t *Token) {
30		t.version = version
31	}
32}
33
34func WithData(data []byte) func(*Token) {
35	return func(t *Token) {
36		t.data = data
37	}
38}

到此為止,我們在幾乎不影響現有系統的狀態下,重構了整個系統流程,並且加入新的機制。Clean Architecture 透過應用 SOLID 來讓我們在可維護性上得到一個不錯的平衡,也讓我們能夠了解到「清楚區分職責」對修改系統的好處。

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