Event Sourcing + DDD帶來的模型重構問題如何解決?

tangxuehua發表於2012-09-05
基於Event Sourcing模式設計的模型如何處理模型重構?

問題背景:ddd的核心是聚合,一個聚合內包含一些實體,其中一個是根實體,這個大家都有共識;另外,如果將DDD與Event Sourcing結合,那就是一個聚合根會產生一些event;那麼這裡的問題是:如果一個領域物件,一開始是entity,後來升級為聚合根,但是該entity之前根本沒有對應的event,因為它不是聚合根。因此它升級後我們如何透過event sourcing獲取升級後的聚合根最新狀態;同理,相反的例子是聚合根降級為實體,該如何處理。

基於哲學方面的一些思考:
之前ORM時代,資料就是資料,我們直接儲存資料,然後讀取儲存的資料即可,很簡單;
現在Event Sourcing了,資料用事件表示,我們不在儲存資料本身,而是儲存與該資料相關的所有事件,包括資料被建立的事件在內;這種思維是好的,我們希望透過儲存資料的“完整的歷史”來達到任意時刻都能還原資料的目標。但是我們僅僅儲存event就真的儲存了“完整的歷史”了嗎?顯然不是,我認為歷史包含兩部分資訊:1)事件;2)邏輯;目前我們只儲存事件而沒有儲存邏輯;但是我們又要希望透過事件溯源還原“完整的歷史”,怎麼可能?!
但是,我們為了確保能還原資料,所以程式碼重構都小心翼翼,比如確保儘量不改原來的事件,儘量用新事件實現業務變化或新業務功能。另外,對於處理事件的邏輯也儘量確保能相容老的事件。之所以要這麼彆扭是因為我們沒辦法把歷史的事件和歷史的事件處理邏輯一同持久化。實際上我們總是在用老的事件與最新的程式碼邏輯相結合進行重演,這實際上是很危險的事情。

然後碰到我上面提出的尖銳問題,實際上很難有優雅的解決方案了。上面我提出的問題其實很難解決:無論是聚合根升級還是降級,都意味著新物件的事件我們無法獲取或者說根本之前沒有任何與新物件相關的事件,自然就無法再用事件溯源的方式得到該物件了。而實際上這個物件什麼都沒做,只是做了個升級或降級處理而已;

那麼問題出在哪裡呢?我認為是ddd的聚合導致的問題。我們之所以要設計出聚合,主要原因是為了透過聚合的手段確保業務上具有內聚關係具有資料一致性規則(Invariants)的領域物件之間方便的維護其一致性;而事件溯源從概念上來說並不針對整個aggregate,而是針對單個的entity.現在一旦將ddd與event sourcing結合,那勢必會導致模型中一些物件沒有與其相關的event,這就會給我們後期模型重構帶來巨大的問題。

既然問題找到了,那我想解決方案也很容易了。就是如果要用event soucing,就必須拋棄聚合的概念,讓一切物件迴歸平等,所有的entity都相互平等,當然value object還是保持不變,因為其只是一個值而已;然後讓每個entity都能產生事件,這樣就不會有因為某些entity沒有事件而導致重構時遇到巨大問題的情況了。

自此,也許你會說,沒有聚合那不就是貧血模型了嗎?我不這麼認為!聚合的意義有兩個:1)更好的表達業務完整概念,因為有些物件卻是在概念上就是內聚其他一些物件的,比如一輛汽車有四個輪子,汽車內聚輪子;2)為了維護物件之間的Invariants,這個不多解釋了,我想大家都理解;那我認為第一點其實和功能無關,是概念上好理解才這樣做;關於第二點維護物件之間的Invariants,我認為有很多方法,不必必須顯式的定義聚合來實現,我們只要確保所有的entity都能很好的規定其自身哪些屬性必須有,哪些屬性不能變,哪些可以變,哪些可以在什麼範圍內變,等等規則約束。這樣也同樣能實現不變性約束;實際上這種方式和ddd看起來非常接近,但是絕不是貧血模型,因為貧血模型是所有entity的所有屬性當然id除外都有get;set;然後所有邏輯全部在service中以transaction script的方式實現;而我上面說的方式實際上entity該有的職責和業務規則判斷還是放在entity內部做掉,但是和經典ddd相比,經典ddd的大部分規則和一致性邏輯都在聚合根內完成,而我的方式則由各個entity合起來實現相同的規則和一致性約束;

到這裡,其實event sourcing還是面臨小範圍(單個entity內部)的程式碼重構的壓力,但這我們總能找到相對成本比較輕的解決方案,比如儘量不改原來事件,只新增事件屬性,不刪除事件屬性。即總是採用與原事件相容的修改方式來修改事件,這其實是可以接受的。

大家覺得怎麼樣呢?我說了一大堆,也希望能多聽聽大家的想法。

[該貼被tangxuehua於2012-09-05 23:03修改過]

[該貼被tangxuehua於2012-09-05 23:32修改過]

相關文章