Eric Evans的《領域驅動設計》問世已經14年之久,到今天幾乎所有業務團隊都或多或少有涉及DDD。然而如果較真會發現,認真遵循DDD設計原則的團隊仍是少數,在多數團隊的現都是:領域模型=資料庫關系。DDD崇尚的是oo式表達,也就是常說的充血模型,對以關系型資料庫實體關系為中心的關系模型甚至是可以用鄙夷來形容。
資料庫關系模型
以資料庫關系指導程式設計實踐,是關系對程式的外延入侵,是預假設關系經存在再按圖索骥将執行邏輯映射到關系,最終收口是落在資料庫而非程式本身。程式本身成了一條條執行通道,每一條通道服務于特定場景的關系,後果必然是過程思維和面條代碼。
假設有業務場景--向購物車添加商品,以關系為中心,代碼組織如下:
public class CartLine{
@Getter @Setter
private String skuCode;
@Getter @Setter
private int buyNo;//數量
}
public class CartServiceImpl implements CartService{
public void addLine(String skuCode, int buyNo, ......){
CartLine line= getLine(skuCode, .....)
if(line!= null){
line.setBuyNo(buyNo);
update(line);
}else{
new CartLine
insert
}
}
}
絕大多數人都應該會有類似代碼的編寫經曆,最常見在經典三分層架構中的Service層,它本質上就是一個類存儲過程,對其執行過程做翻譯:
CartLine line= select * from cart_line where sku_code= #{skuCode}
if(line!= null){
update cart_line set buy_no= #{buyNo} where sku_code= #{skuCode}
}else{
insert cart_line values(xxxx)
}
業務處理就是在有序執行一條條sql,老外給它取了個好聽的名字叫事務腳本。事務腳本是非常典型的過程式表述,類似是串聯sql完成一段完整的業務,可以用求和數學公式“事務腳本=∑fi,fi代指一條sql”來定義
架構模式上管這種代碼叫貧血模型,即無行為,表達力貧瘠。Martin Fowler在《企業應用架構模式》中
定義它是反模式,簡單系統使用它開發沒問題,而對于複雜業務,業務邏輯、各種狀态散布在大量的函數中,維護擴充的成本會變得很高。
資料庫中心的設計是”修改一處,全量回歸“的悲劇源頭,也是代碼寫久後枯燥、無聊、覺得都是重複勞動的源頭。過程式代碼并不需要精心設計群組織,自然寫代碼也就成了無意義的翻譯器。
oo模型
假設記憶體無限大且永不當機,即已經沒有持久化必要,換句話說完全可以不使用資料庫,此時應該如何編寫代碼?
- 是使用與現實世界的活動實體做連接配接、特征和行為封裝在一起,職責明确、邏輯合理分布的有狀态對象,再按場景将合适類聯系起來?
- 還是使用僅映射活動實體特征的pojo,按場景忠實反應發生過程依次get/set操作pojo屬性?
Jdk以及各類優秀中間件都是以記憶體操作為主,可以參考它們的選擇:即便是主推pojo規範的ejb都沒有選擇貧血模型,反而是極度充血-- 有行為,有聯系、表達力強,容易組織。
資料持久化應隻被當成是程式的暫停而非結束,對暫停而言,下一次再執行時需要忠實還原對象的上次執行後狀态,而對結束則下一次是一個新的開始。即
load; do;
和
new; setter/getter; persist
的差別。如果從這個角度出發,對象就變得近似是常駐在記憶體。
在Vaughn Vernon的《實作領域驅動設計》中關于“六邊形架構”如何在領域實踐應用中對資料庫和DDD的關系有很清晰的闡述很:
資料庫僅僅隻是Domain Model的右向适配(被驅動者)。
資料庫隻是持久化手段,是一種基礎設施,不該作為指導程式運作的模型。寫代碼時要時刻保持一種警惕,如果把關系型資料庫替換成json、普通文本或者無schema的nosql資料庫,要如何保證邏輯層的無感?
正确的方法是以對象和對象聯系而非資料庫表關系作為指導程式運作的基礎。一次完整的業務操作由各實體對象行為協同完成,結果最終會反映在記憶體中各對象執行個體的内在屬性上。這樣無論怎麼修改持久化方案,都隻需要改變與特定持久化方案的适配政策。
某逆向交替系統,其逆向狀态是記錄在正向交易上的,
| order_id | order_status |pay_fee|item_id|refund_amount |refund_status|attributes|...... |
refund_amount和refund_status分别代表逆向退款金額以及退款狀态。很顯然,這樣的表設計會導緻兩個問題:1) 無法多次逆向,前一次狀态在下一次發起後被覆寫,即逆向無法追溯;2)逆向需要更新交易訂單屬性,方式是調用交易接口更新,是以某些情況下可能會有樂觀鎖問題-- 逆向更新了鎖版本導緻交易再去更新失敗。
在小規模試跑階段,業務上會嚴格限制一筆訂單一次逆向,同時對于樂觀鎖問題采用多次重試機制,是以問題并不明顯。逐漸的業務開始起來,首先業務量大之後重試導緻的性能問題凸顯-- 在加事務的情況下,資料庫連接配接是一直持有直到送出或復原,多次重試相當于增加了幾倍持有連接配接時間,是以會明顯明顯降低資料庫吞吐;其次,對于不能多次逆向合作夥伴也開始有反彈。是以交易和逆向的表拆分變得勢在必行。
因為逆向在設計時使用的充血+六邊形架構,實際上遷移并沒有很大工作量,隻是重建立了表,然後對資料庫适配進行修改,将輸出表由A指向B。
而如果使用貧血模型,對表結構做了較大變更的情況下,邏輯代碼一定會需要修改。可以用事務腳本的公式做個簡單的逆推導:
表變化=sql變化=事務腳本(邏輯執行)變化。