
在 Lookup Order 實作 Token 機制 - Clean Architecture in Go
處理完在 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()
分離 version
和 id
,再把 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 機制的實現中,我們並沒有討論到「加密和解密」的處理,我們應該將這個處理放在哪裡呢?接下來讓我們一起來探討這個問題。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 連載介紹 - Clean Architecture in Go
- 目標設定 - Clean Architecture in Go
- wire 的依賴注入 - Clean Architecture in Go
- 案例說明 - Clean Architecture in Go
- 操作介面設計 - Clean Architecture in Go
- Place Order 實作 Controller 部分 - Clean Architecture in Go
- Place Order 實作 Entity 部分 - Clean Architecture in Go
- Place Order 實作 Repository 部分 - Clean Architecture in Go
- Lookup Order 功能 - Clean Architecture in Go
- Tokenization 機制設計 - Clean Architecture in Go
- 在 Place Order 實作 Token 機制 - Clean Architecture in Go
- 在 Lookup Order 實作 Token 機制 - Clean Architecture in Go
- Token 內容加密 - Clean Architecture in Go