
Place Order 實作 Repository 部分 - Clean Architecture in Go
處理完整個流程後,我們可以來做最後的「狀態保存」處理,也就是 Repository(倉庫) 的實作。在這個階段,我們會先使用一個 InMemory 的版本,將狀態暫時性保存在記憶體中,等到我們後續有更完善的規劃後,再回來處理。
定義介面
在設計上 Repository 屬於 Adapter Layer 的部分,也就是跟更低階的資料庫轉換,變成提供給 UseCase(使用案例)可以使用的方法,因此我們需要由需求方,也就是 UseCase 描述他想要怎麼儲存訂單。
在 internal/usecase/repository.go
這個檔案,加入以下的內容。
1package usecase
2
3// ...
4
5type OrderRepository interface {
6 Save(ctx context.Context, order *orders.Order) error
7}
我們希望這個 OrderRepository 提供一個 Save
方法,並且可以把一筆訂單傳入到裡面,並且透過 error
回傳告訴我們是否成功。
Golang 很常會使用合成界面的方式(如:
io.ReadWriter
)因此我們也可以根據需求,設計成像是OrderWriter
和OrderReader
的介面
那麼,原本在 PlaceOrder UseCase 中沒有對產生的 Entity(實體)進行狀態保存的處理,就可以在定義後加入到裡面。
1type PlaceOrder struct {
2 orders OrderRepository
3}
4
5func NewPlaceOrder(orders OrderRepository) *PlaceOrder {
6 return &PlaceOrder{
7 orders: orders,
8 }
9}
10
11func (u *PlaceOrder) Execute(ctx context.Context, input *PlaceOrderInput) (*PlaceOrderOutput, error) {
12 order := orders.New(
13 uuid.NewString(),
14 input.Name,
15 )
16 // ...
17
18 if err := u.orders.Save(ctx, order); err != nil {
19 return nil, err
20 }
21
22 // ...
23}
在 UseCase 的修改並不複雜,因為我們在上一階段已經把主要的行為都處理完畢,只需要補上保存狀態的步驟。
Repository 實作
InMemory 的 Repository 實作也不複雜,我們可以直接定義 Golang 的 Struct 用來保存狀態,只需要將 Entity 上的內容複製到 Struct 中即可。
在 internal/repository/in_memory_orders.go
裡面實作如下的內容。
1package repository
2
3// ...
4
5type InMemoryOrderItemSchema struct {
6 Name string
7 Quantity int
8 UnitPrice int
9}
10
11type InMemoryOrderSchema struct {
12 Id string
13 CustomerName string
14 Items []InMemoryOrderItemSchema
15}
16
17type InMemoryOrderRepository struct {
18 orders map[string]InMemoryOrderSchema
19}
20
21func NewInMemoryOrderRepository() *InMemoryOrderRepository {
22 return &InMemoryOrderRepository{
23 orders: map[string]InMemoryOrderSchema{},
24 }
25}
26
27func (r *InMemoryOrderRepository) Save(ctx context.Context, order *orders.Order) error {
28 items := []InMemoryOrderItemSchema{}
29
30 for _, item := range order.Items() {
31 items = append(items, InMemoryOrderItemSchema{
32 Name: item.Name(),
33 Quantity: item.Quantity(),
34 UnitPrice: item.UnitPrice(),
35 })
36 }
37
38 r.orders[order.Id()] = InMemoryOrderSchema{
39 Id: order.Id(),
40 CustomerName: order.CustomerName(),
41 Items: items,
42 }
43
44 return nil
45}
在上面的實作,會發現又再一次的將 Order 的資訊複製到 InMemoryOrderSchema
裡面,因此我們在整個流程中總共做了三次的複製,第一次是 Controller 複製到 UseCase 中,然後再從 UseCase 複製到 Entity 裡面,最後從 Entity 複製到 Repository 裡面。
看起來似乎非常的多餘,然而如果我們仔細區分的話,確實會發現這三次的複製分別指不同的東西。
Controller 之所以要複製到 UseCase 是因為他作為 Adapter 要將 JSON 轉換成 Golang 內的 Struct,並且滿足 UseCase 這個需求方的要求。從 UseCase 複製到 Entity 則是為了將單純的資料(DTO,Data Transfer Object)轉換成一個有意義的物件(Entity)才能做後續的操作。最後 Entity 複製到 Repository 的原因是我們要將 Entity 內的狀態抽離出來,放到資料庫,此時 InMemoryOrderSchema
可以看作資料庫的資料表。
與 MVC 的差異
到這一個階段,我們大致上可以看到跟 MVC 框架的差異,主要在 Model 的地方被分解成 Entity、UseCase、Repository 三大塊,或者說 Entity 加上 Repository 為 Model,而 UseCase 則從 Controller 被抽離出來,這可能會根據框架的偏好有所差異。
我們抽離出 UseCase 和 Repository 這樣的概念跟 MVC 框架相比有怎樣的優點。這就跟 Clean Architecture 希望能夠「抽換」元件的出發點有關係。
舉例來說,當我們的 UseCase 跟 Controller 綁定時,就無法更換 Web 框架,或者從 HTTP 協定替換成其他協定來使用,我們必須把 Controller 的實作一起搬移到新的協定實作上。
同樣的道理,我們要替換資料庫的實作,從 RDBMS 切換到 NoSQL 的時候,以大多數 MVC 框架的設計,會發現通常需要使用另一個新的套件(如:Rails 無法使用內建的 ActiveRecord 而要改用 DDynamoid 這類套件)才能夠實現。
然而,有了 Repository 對狀態的讀取、寫入做了抽象化,有了統一的介面後就不需要思考背後是一段記憶體、RDBMS 還是 NoSQL,行為就被統一我們也更容易地對這些功能做替換。
當然,有著這樣的優點,也會讓系統一定程度的複雜、繁瑣,在簡單、小規模的專案上不一定適合使用。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 連載介紹 - 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