基于ddd+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内部)的代碼重構的壓力,但這我們總能找到相對成本比較輕的解決方案,比如盡量不改原來事件,隻新增事件屬性,不删除事件屬性。即總是采用與原事件相容的修改方式來修改事件,這其實是可以接受的。
大家覺得怎麼樣呢?很希望能多聽聽大家的想法。
--------------------------------------------------------------------------------------------------------
為了能更好的說明問題,我寫了個簡單的小例子。下面有對這個例子的較長的描述,以及基于該例子的問題描述;

//團隊聚合根
public class team : entitybase<int>, iaggregateroot
{
private ilist<member> _members = new list<member>();
public ienumerable<member> members { get { return _members; } }
public void addmember(string name, string email)
{
applyevent(new memberadded(name, email, this.id));
}
public void updatemembername(int memberid, string newname)
applyevent(new membernameupdated(memberid, newname, this.id));
private void onmemberadded(memberadded evnt)
_members.addmember(new member(evnt.name, evnt.email));
private void onmembernameupdated(membernameupdated evnt)
var member = _members.findmemberbyid(evnt.memberid);
member.setname(evnt.newname);
}
//團隊成員新增事件
public class memberadded
public string name { get; private set; }
public string email { get; private set; }
public int teamid { get; private set; }
public memberadded(string name, string email, int teamid)
this.name = name;
this.email = email;
this.teamid = teamid;
//團隊成員名稱修改事件
public class membernameupdated
public int memberid { get; private set; }
public string newname { get; private set; }
public onmembernameupdated(int memberid, string newname, int teamid)
this.memberid = memberid;
this.newname = newname;
this.teamid = teamid;
//團隊成員實體
public class member : entitybase<int>
public member(string name, string email)
public void setname(string name)
assert.isnotnullorempty(name);
assert.lengthlessthen(name, 255);

}
上面的例子中,有一個聚合根,team,表示一個團隊;team内聚了一些團隊成員,member;member是實體;
這裡聚合根,實體,就是ddd中的aggregate root與entity。這裡沒問題吧!另外,上面的例子,我采用了event sourcing的方式來實作模型。
event sourcing的核心思想有兩點:
1)用與某個對象相關的事件來記錄對象的每一次變化,一次變化一個事件,對象的建立是第一個事件,如teamcreated事件表示一個團隊被建立了;
2)對象的重建不需通過orm,而是直接使用之前記錄的事件進行逐個重演最終得到對象最新狀态,這個重演的過程我們稱為事件溯源,英文叫event sourcing;
不知我上述對event sourcing的描述是否和大家的了解一緻?
好了,本文提到的關于“曆史不僅僅由事件組成,還必須由處理該事件的邏輯組成”。這句話的意思是,事件要進行重演,必須與一定的邏輯結合,事件本質上隻是一些資料,
包含了某次變化的相關資訊,它不包含邏輯,是靜态的值對象;那邏輯是什麼呢?主要指兩方面:
1)上面team類裡的onmemberadded和onmembernameupdated這兩個方法,這兩個方法實際上是事件的處理函數,職責是負責更新聚合的相關狀态;
2)這些事件處理函數在更新聚合狀态時實際上是依賴于目前聚合的内部結構的;
是以,事件要能夠順利的按照和曆史的方式完全一緻的重演,依賴于三個要素必須和曆史一緻:
1)事件不變;
2)聚合内部的事件處理邏輯不變,或者即便要變也必須和以前的邏輯相容;
3)事件處理邏輯依賴的聚合的内部結構不變,或者即便要變也必須和以前的結構相容;
而我們現在做到的隻是第一個要素不變,第二和第三個要素我們很可能會進行重構;
當然你可能會說,第二點你也基本不會變,因為你的事件處理邏輯一般都是簡單的屬性指派,即簡單的更改聚合相關屬性的狀态,那行,如果你真這樣做,那确實問題不大;實際上也必須這樣做!
但是第三個要素呢?第三個要素實際上就是我說的模型結構重構,最嚴重的重構情況則是:聚合根降級為實體,或者實體更新為聚合根,簡稱聚合根的更新與降級;
對于這兩種情況,在應用了event sourcing的情況下,那是很可怕的。因為從上面我的代碼中可以看出member起初隻是個實體,它沒有自己的事件,所有的事件都隻和聚合根關聯,即team。
但是我們之後如果想重構,把member更新為聚合根了,這個重構之前在orm時代,那時非常簡單的事情,基本什麼都不必變,但是在event sourcing的模式下,就有大問題。
因為我們沒有與member對應的事件,自然就無法應用事件溯源來重建member聚合根了。這裡實際上就是我說的上面的第三個要素發生了結構性變化,導緻我們無法通過事件溯源重建對象
看到這裡,大家再回過頭去看一下我最上面對問題的闡述可能更好了解一點吧!