論 Use Case 和 Domain Service 的差異
這個問題我個人目前還沒有一個邏輯足夠完整的答案,然而最近思考後認為蠻適合拋出來討論。
會回過頭來思考這個問題,也是因為對 Clean Architecture 的使用足夠熟練,要開始準備來深入了解經常一起應用的 Domain-Driven Design,那就一定會遇到這個問題。
MVC 到 Clean Architecture
在進入到 Domain-Driven Design 前,我們從大多數框架都會採用的 MVC 架構發展到 Clean Architecture 開始討論,會相對的容易理解脈絡。
首先,如何實作一個軟體,可以看作是各種類型的條件限制下造成的結果,因此從架構層到設計,都是我們套用各種限制的影響(如:商業需求、系統需求、設計需求)因此,第一步就是將最常使用的 MVC 框架基礎,加入 Clean Architecture 的限制來進行設計。
我們可以先來觀察 MVC 框架,通常在結構上非常單純,基本上就是 Controller 耦合 Model 和 View 來完成輸入到輸出一系列的處理,以單體式(Monolithic)的架構來看,可以算是相當容易開發跟上手的結構。
然而,當系統的功能逐漸增加後,維護上就不那麼容易。因為大部分的功能都在 Controller 上,並且直接耦合在 Model 和 View 而不容易抽換或者調整,此時就迎來如何維護的問題,那麼 Clean Architecture 就是個不錯的方式。
Clean Architecture 透過 SOLID 這類原則,對系統中的分級,讓我們更好區分每種物件所負責的範圍,大多數情況依照作者的案例區分為 Framework、Adapter、Application Business Rule(Use Case)、Enterprise Business Rule(Entity)四個層級就非常足夠。
此時,我會將 MVC 框架中提供的 Model、View、Controller 都視為 Adapter 層級,也就是銜接底層(如:框架、HTTP 伺服器)和商業邏輯(Use Case 和 Entity)的角色。
關於這樣區分的因,可以參考我在 2024 年 COSCUP 的演講 Clean Architecture in Rails 所提及的判斷基準
簡單來說,為了對應逐漸變複雜的情境,我們將 Model、View、Controller 這三個物件的職責做切割,進而出現了 Model 分離出 Entity 和 Presenter、Controller 分離出 Use Case 的情況,在 Rails 的文章,就會看到像是 Service Object 這類概念的出現。
這個做法我們可以從「電視遙控器」來看到合理醒,作為 Controller 的遙控器,只有將按鈕轉換成紅外線訊號的功能(Adapter)實際上進行處理的是電視本身設計的機制(Use Case)
Clean Architecture 到 Domain-Driven Design
原版的 Domain-Driven Design 是以 Layered Architecture 的方式來區分系統的架構,這個架構基本上和 MVC 架構是相對接近(或者說 MVC 有使用相關的概念)然而在現代系統比較複雜的狀況下,大多會把 Domain-Driven Design 和 Clean Architecture 放在一起討論。
從上一個段落我們可以觀察到,MVC 加入 Clean Architecture 的限制後,為了讓物件的職責和層級明確,可以透過從 Controller 分割出 Use Caes 的這種方式來處理。
那麼在 Domain-Driven Design 中的 Domain Model 範圍中,有一半以上跟 Clean Architecture 所提到的物件是差不多交疊的,我們是否可以直接代換呢?
以 Entity 和衍伸的 Aggregate Root 來看,這樣的解讀並無太大問題。而 Repository 劃分為 Adapter 的方式,也可以歸類成呼叫外部服務的抽象概念。至於 Domain Service 和 Use Case 的對應看起來也相當合理,然而我們又有另一個限制條件在這裡出現了一個可能矛盾的狀況。
Domain-Driven Design 的角度來看,Domain Service 不該知道其他 Domain 的物件,這樣可以極大的減少耦合問題,也有助於在未來轉換成 Microservice 等架構。
但是,我們從 MVC 到 Clean Architecture 的過程中,我們是沒有考慮 Domain 問題的,因此會出現像這樣的情況。
1class BuildNotificationUseCase
2 # ...
3
4 def execute(user_id:, channel_id:, event:)
5 user = users.find(user_id) # Users::Entities::User
6 channel = channels.find(channel_id) # Notifications::Entities::Channel
7
8 channel.add_notify(user: user, event: event)
9 channels.save(channel)
10 end
11end
上述的案例中,我們的 Use Case 同時使用了 Users
和 Notifications
兩個命名空間(或者視為 Domain)的物件,那麼就違背了 Domain Service 的預期,也就是說事情沒有表面上那麼單純。
Golang 這類語言有避免循環引用(Circular Dependency)的設計,因此在
clear_notify(user: user)
這類例子不會直接傳入 User Entity 更可能是一個 Struct 作為 DTO(Data Transfer Object)來處理
假設要讓 Use Case 與 Domain Service 是相同的條件,我們就需要讓不同領域之間的調度由更前一個階段的 Controller 來負責,此時我們還需要考慮作為 Adapter 呼叫多個 Use Case 是否恰當的問題。
Bounded Context
我認為比較有可能處理這個問題的方法,可能釐清 Bounded Context 的界線在哪裡。舉例來說,我在一些 Domain-Driven Design 的專案樣板中,看過各種不同的目錄結構,我比較常看到把 Use Case、Repository 切分出來的做法。
以我目前常用的結構,通常像是這樣:
app/entities
app/entities/users
app/entities/notifications
app/usecases
app/repositories
因為 Clean Architecture 的思考方式,Repository 是實作 Use Case 的介面,通常會回傳某個 Domain 的 Entity 回去供 Use Case 使用。
以這個方式作為前提思考,我很可能會把 UserRepository
理所當然視為 User Domain 的物件,然而 Notification Domain 下其實也有一個被通知對象 User Entity 才對,此時該如何命名這個 Repository 呢?
假設要處理這樣的問題,目錄結構是否會轉變成像是這樣。
app/domains/user
app/domains/user/entity
app/domains/user/service
app/domains/user/repository
app/domains/notification
app/domains/notification/entity
app/domains/notification/service
app/domains/notification/repository
此時每個 Domain 下都會有滿足所有行為的 Entity 以及只針對該 Domain 的使用 Use Case(這邊定義為 Domain Service)還有做為 Adapter 的 Repository,那麼 Notification Domain 下就可以有 UserRepository
並且產生該 Domain 之下的 User Entity 出來。
同時 Domain Service 等於 Use Case 的問題可能就可以被滿足,因為在上述的案例中,這個 User 並不是 User Domain 的 User 而是 Notification Domain 下面的「被通知對象」
印象中這個區別在一些討論 Domain-Driven Design 的文章也會被提及,然而在實務上還是蠻高的機率會忽略這樣的細節
此時我們可以理解為,一個 API 呼叫會由某個 Controller 作為 Adapter 將外部的格式(JSON、XML、Protobuf)轉換成內部的格式(如:DTO)交給 Domain Service 來處理,再將回傳的結果轉換後回傳。
然而,這是建立在前後端分離的概念上進行的思考。假設要從後端輸出一個完整的畫面,那麼 Controller 所需的資訊就不只一個 Domain Service 呼叫可以提供,也會有跨越不同 Domain 的需求,此時該由 Controller 來協調,還是 Use Case(這邊視為和 Domain Service 是不同的東西)就可能需要再做討論。
也許這就是 Clean Architecture 說「不一定只有四層」的情境,假設對不同 Domain 的調度是一個經常出現的動作,那麼就可能會有一個新的 Layer 出現在架構中。
這篇文章主要是紀錄思考跟推理的過程,雖然不是第一次思考,但是每次都有一些新的發現。希望不久後自己能找到一個組夠完整的推導,並且足以在正式的專案中應用。