從面向對象設計到領域驅動設計

本文通過一個簡單的案例闡述什麼是正確的面向對象設計,而通常所講的面向對象設計,其實只是營造了一個理想的對象世界。在面對現實問題時,領域驅動設計提出瞭解決方案,通過在戰略模式中運用限界上下文,在戰術模式中運用聚合,有效地控制了業務複雜度,讓設計變得合情合理。

01 最初的版本

下圖列出超市收銀員的第 1 個版本:

這一設計違背了面向對象設計原則:

迪米特法則認爲兩個對象若要產生調用關係,需要滿足:

可認爲這樣的兩個對象是一對好朋友,而朋友的朋友呢,則認爲是陌生人。迪米特法則認爲:“不要和陌生人通話”,所以陌生人之間不允許出現調用關係。如代碼所示,Customer 作爲 Cashier 中 charge() 方法的參數,二者爲朋友;Wallet 作爲 Customer 的屬性,二者爲朋友;——但朋友的朋友並非朋友,所以 Cashier 不能繞開 Customer 直接訪問 Wallet,如下圖所示:

同理,根據信息專家模式,擁有信息的對象是操作該信息的專家,既然 Customer 擁有 Wallet,就該將操作 Wallet 的方法分給它。

02 重構的版本

通過提取方法和轉移方法,對 Cashier 和 Customer 開展重構,可得到如下圖所示的第 2 個版本:

注意,不僅位於 Cashier 的大部分職責通過提取爲 pay() 方法轉移到 Customer,原實現中對錢包中的錢的判斷,即條件表達式:

myWallet.getTotalMoney() >= payment

也被提取爲 isEnough() 方法,轉移到了 Wallet,真正實現了職責的合理分配。

重構後的版本滿足了面向對象設計原則。Wallet 是 Customer 的隱私,重構後的版本保護了 Customer 的隱私;對 Wallet 的操作並非 Cashier 希望關注的細節,設計時,應該讓調用者瞭解的知識越少越好,設計的接口也更友好。

03 對象世界的問題

版本 2 的代碼看起來很美好,只可惜,它創造的不過是一個理想的對象世界。

計算機並非永遠都在運行,也不會擁有無限的內存空間,因而我們不能只考慮對象在內存空間中的引用與協作。我將其稱之爲領域對象的哲學三問題

由此引出領域對象的現實問題:

04 引入領域驅動戰術模式

領域驅動設計通過引入實體、值對象、聚合、資源庫(Repository)與工廠、領域服務和領域事件七種角色解決以上所說的現實問題。

領域對象通過聚合作爲邊界形成一個個獨立的整體領域概念,並交由資源庫和工廠管理其生命週期。聚合內的實體與值對象有其不同身份,前者通過定義唯一標識支持生命週期管理時對實體的識別與跟蹤。當各個聚合被加載到內存後,聚合之間只能通過根實體建立關係,也可以通過領域事件通知彼此狀態的變更。如下圖所示:

因爲計算機能力所限,我們無法營造一個理想的大而全的對象世界,領域驅動設計則提供一種設計上的妥協,以聚合作爲設計約束,將整個對象設計劃分爲一個個小而美的村落,每個村落就像一個小的桃花源,實體與值對象之間能做到 “黃髮垂髫並怡然自樂”,不受外界環境的干擾。

以超市收銀員爲例,受到現實世界的影響,Cashier 與 Customer 等對象需要持久化到數據庫。遵循領域驅動設計模式,可將 Cashier 與 Customer 定義爲兩個不同的聚合,而 Wallet 作爲值對象放入 Customer 聚合內部。兩個聚合都有屬於自己的資源庫,並通過領域服務實現聚合、資源庫之間的協作。如下圖所示:

領域服務 CashierService 對外擔任收銀的職責,實際上,它只是一個控制者或者協調者,通過資源庫加載各自的聚合到內存後,彼此即可友好協作了:

public class CashierService {
    private CashierRepository cashierRepo;
    private CustomerRepository custRepo;
    public void charge(String cashierId, String custId, float payment) {
        Cashier cashier = cashierRepo.cashierOf(cashierId);
        Customer customer = custRepo.customerOf(custId);
        cashier.charge(customer, payment);
    }
}

除了引入領域服務和資源庫,領域對象的定義與協作與第 2 個版本的代碼完全一致。不同之處在於,現有設計清晰地明確了 Wallet 屬於 Customer 聚合的一部分,根據領域驅動設計的要求,並不允許聚合外部的對象直接訪問聚合的非根元素,如此則有效避免了第 1 個版本出現的問題。

這就是引入領域驅動戰術設計模式之後的第 3 版。領域邏輯形成一種三權分立的態勢:

**聚合的邊界構成聚合內和聚合外兩個不同的世界。**聚合內維持了理想的對象世界,聚合內的實體與值對象都在內存中,可以遵循面向對象設計原則,自如地實現行爲之間的協作。

05 引入領域驅動戰略模式

理想很豐滿,現實很骨感。一旦我們把超市收銀員這一系統的規模擴大,就需要從架構層面考慮更大的獨立邊界。這是控制規模複雜度最有效的手段,即分而治之。

領域驅動設計提出的分而治之策略就是劃分限界上下文。如果我們將 Cashier 和 Customer 劃分到兩個不同的限界上下文,它們彼此之間就需要完全隔離,否則就會造成大泥球似的單體系統

爲了清晰地界定各自邊界,在降低耦合的同時完成彼此的協作,一種有效手段是爲限界上下文引入菱形對稱架構模式。如此一來,下游限界上下文與上游限界上下文之間的協作,就只能通過下游的南向網關向上遊的北向網關發起調用,從而得到最終版即第 4 版的超市收銀員:

相較第 3 版而言,第 4 版變得更加複雜,除了已有的對象之外,還引入了北向網關的遠程服務和本地服務,以及南向網關的端口和適配器。

兩個限界上下文之間的協作有着嚴格的規定,CashierService 不能跨過限界上下文直接訪問 CustomerRepository,而是通過自身南向網關的端口 CustomerClient 發起對上游的調用。

雖然結構變得更加複雜,但我們看待問題不能脫離現實。在做出這一設計決策之前,我們給出一個前提,就是超市收銀員系統的規模擴大了,不僅需求變得越來越複雜,導致領域模型更加龐大,負責開發它的團隊規模也擴大了,需要多個團隊互相協作、並行開發。倘若我們只是從限界上下文的層次看待上圖,每個限界上下文的內部依舊簡單。

領域驅動設計的聚合與限界上下文都是通過引入邊界爲設計添加了約束。這些約束看起來讓設計者變得束手束腳,卻能夠以一種森嚴的紀律防止架構和代碼的腐化,確保系統的架構保持清晰,並具有一致的面貌,即便隨着需求的日益增長,依然能夠保持對架構的控制力。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/b4UX2EE4Mv5ZzO0TnTys_A