相信很多人都有聽過 MVC 這類型的架構模式 (Architecture Pattern),這類型的模式在初期可以幫助我們分離 UI 與資料處理的邏輯,讓我們將兩者的關注點分離。只是 MVC 的 Model
的職責過多,並不利於複雜業務邏輯的軟體的需求,此外,Model
也常讓人習慣直接套用 ORM 框架,導致業務邏輯被 ORM 綁架的情形出現,這對於未來要擴展業務邏輯非常不利。
對於一些擁有複雜業務邏輯的軟體,雖然專門解決領域問題的部分只佔了一小部分,但卻擁有超乎比例的重要性。所以我們要把「領域物件」與「其他技術細節的的物件」分離開來,並把「領域物件」視作核心。
接下來就讓我們介紹一種常見的架構風格 (Architecture Style): 分層式架構 (Layered Architecture),並介紹他的變異體 Clean Architecture。
在傳統的分層式架構中,我們會先把領域模型與業務邏輯分離出來,並減少對於其他層的依賴。同時每一層都應該要有良好的內聚性,並指依賴於比自己還要低的層。
常見的分類會分成: User Interface (使用者介面層)、Application (應用層)、Domain (領域層)、Infrastructure (基礎設施層)。如下圖:
舉一個實際案例,我想要匯 500 元給用戶 A,那麼我會經歷以下的過程:
Account
class) ,然後進行轉帳 (My_Account.TransferTo(A_Account, 500)
)My_Account.TransferTo(A_Account, 500)
的驗證(雙方帳戶狀態是否為開啟、餘額是否足夠)以及計算 (我扣 500,A 得 500 )。My_Account.TransferTo(A_Account, 500)
成功的通知,將新的 Account 狀態存入 DB,然後送交 (commit) Transaction。層 | 職責 |
---|---|
User Interface (Presentation) | 負責向使用者顯示資訊和解釋使用者的指令。使用者可能是人也有可能是另一個系統。 |
Application | 定義軟體要完成的任務(使用案例),並指揮 Domain 來實現業務邏輯的計算。 |
Domain | 負責保管業務概念、業務狀態以及業務規則。本層式軟體的核心。 |
Infrastructure | 為上面個層提供技術能力:為 Application 傳遞訊息、為 Domain 提供持久化機制、為 User Interface 處理畫面等等。 |
以上的分層策略有效的幫助我們做到關注點分離 (separation of concerns)。當我們把軟體設計中的每一個部分獨立出來個別關注,程式就有更好的能力處理複雜的任務。
此外,他還有額外的好處:
而在引入 DDD 時,其實真正在乎的只有 Domain 層。對於其他的層要怎麼分其實並不嚴格要求。但是,這時候有一個問題來了,那就是依賴性問題。
Domain 層既然是軟體的核心,那就要保持它的高層地位。
以目前的架構來看,我們可以看到 Application 與 Domain 層都會依賴於 Infrastructure 層,這樣導致底層機制一變動如更改 DB 或 ORM,那依賴他的那幾層都要全部打掉重練。但我們有提過,使用者並不在乎你是用 Postgres 還是 MongoDB,他只在乎軟體能夠完成任務。因此,我們不應該冒著軟體業務邏輯出錯的風險,讓 Domain 與 Application 依賴於 Infrastructure 層。
這邊我們就來介紹利用依賴反向原則 (Dependency Inversion Principle, 簡稱 DIP) 來扭轉這個劣勢。
註:MVC 並不是 Layered Architecture 的一種,因為 Layered Architecture 的關係是單向的,MVC 元間的互動是雙向的。但兩者並非相斥,
MVC 常用於 Layered Architecture 的 Presentation Layer 中。
依賴反向原則 (DIP) 的精神:
高層模組不該依賴於低層模組,兩者都應該依賴於抽象介面。抽象介面不應該依賴於細節,細節應該依賴於抽象。
在這邊,Infrastructure 與 Domain 層的主要模組都算是細節,而當我們在 Domain 層定義了一個 Interface 規格給 Infrastructure 遵守,那就達到了反向依賴的作用。如下圖:
注意!抽象出來的介面一定要放在高層,不然就沒有達到層之間的依賴反轉。
實際上程式碼會像是這樣:
Domain 層:
// domain/model/Account.ts
class Account {
constructor() {}
}
interface AccountRepository {
// 取得資料
getById(id: string): Account;
// 新增
add(acc: Account): void;
// 更新
save(acc: Account): void;
}
Infrastructure 層:
// infrastructure/repository/PostgresAccountRepository.ts
class PostgresAccountRepository implements AccountRepository {
private db: Pool;
constructor(db) {
this.db = db;
}
async getById(id: string): Account {
const acc = await this.db.query('SELECT * FROM account WHERE id = $1', id);
return acc;
}
async add(acc: Account) {
await this.db.query('INSERT INTO account (....)', acc);
}
async save(acc: Account) {
await this.db.query('INSERT INTO account (....)', acc);
}
}
當依賴關係反轉之後,接著讓我們來介紹如何導入 Clean Architecture。
感謝分享~
請問最後程式碼範例中,interface AccountRepository 是不是違反了 DIP,因為依賴到細節(Account)
並沒有違反 DIP 唷。你可以看 interface AccountRepository 上面幾行程式碼,Account 是 Aggregate Root,也放在 Domain Layer 中,與 interface 屬於同一層。