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

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

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

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

Token Entity

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

  • 代號
  • 版本
  • 資料

因此可以像這樣實作

 1// internal/entity/tokens/token.go
 2package tokens
 3
 4// ...
 5
 6const (
 7	CurrentVersion = "v1"
 8)
 9
10type Token struct {
11	id      string
12	data    []byte
13	version string
14}
15
16func New(id string) *Token {
17	return &Token{
18		id:      id,
19		version: CurrentVersion,
20	}
21}
22
23func (t *Token) Id() string {
24	return t.id
25}
26
27func (t *Token) Data() []byte {
28	return t.data
29}
30
31func (t *Token) Version() string {
32	return t.version
33}
34
35func (t *Token) SetData(data []byte) {
36	t.data = data
37}
38
39func (t Token) String() string {
40	return t.version + ":" + t.id
41}

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

Place Order

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

 1// internal/usecase/place_order.go
 2
 3type PlaceOrder struct {
 4	orders OrderRepository
 5	tokens TokenRepository
 6}
 7
 8func NewPlaceOrder(orders OrderRepository, tokens TokenRepository) *PlaceOrder {
 9	return &PlaceOrder{
10		orders: orders,
11		tokens: tokens,
12	}
13}
14
15func (u *PlaceOrder) Execute(ctx context.Context, input *PlaceOrderInput) (*PlaceOrderOutput, error) {
16	nameToken := tokens.New(uuid.NewString())
17	nameToken.SetData([]byte(input.Name))
18
19	if err := u.tokens.Save(ctx, nameToken); err != nil {
20		return nil, err
21	}
22
23	order := orders.New(
24		uuid.NewString(),
25		nameToken.String(),
26	)
27
28	for _, item := range input.Items {
29		err := order.AddItem(item.Name, item.Quantity, item.UnitPrice)
30		if err != nil {
31			return nil, err
32		}
33	}
34
35	if err := u.orders.Save(ctx, order); err != nil {
36		return nil, err
37	}
38
39	out := &PlaceOrderOutput{
40		Id:    order.Id(),
41		Name:  string(nameToken.Data()),
42		Items: []PlaceOrderItem{},
43	}
44
45	for _, item := range order.Items() {
46		out.Items = append(out.Items, PlaceOrderItem{
47			Name:      item.Name(),
48			Quantity:  item.Quantity(),
49			UnitPrice: item.UnitPrice(),
50		})
51	}
52
53	return out, nil
54}

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

TokenRepository

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

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

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

 1// internal/repository/in_memory_tokens.go
 2
 3// ...
 4
 5type InMemoryTokenSchema struct {
 6	Id      string
 7	Data    []byte
 8	Version string
 9}
10
11type InMemoryTokenRepository struct {
12	tokens map[string]InMemoryTokenSchema
13}
14
15func NewInMemoryTokenRepository() *InMemoryTokenRepository {
16	return &InMemoryTokenRepository{
17		tokens: map[string]InMemoryTokenSchema{},
18	}
19}
20
21func (r *InMemoryTokenRepository) Save(ctx context.Context, token *tokens.Token) error {
22	r.tokens[token.Id()] = InMemoryTokenSchema{
23		Id:      token.Id(),
24		Data:    token.Data(),
25		Version: token.Version(),
26	}
27
28	return nil
29}

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