基於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 int TeamId { 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 string Name { get; private set; }
public string Email { get; private set; }
public Member( string name, string email)
{
this.Name = name;
this.Email = email;
}
public void SetName( string name)
{
Assert.IsNotNullOrEmpty(name);
Assert.LengthLessThen(name, 255);
this.Name = name;
}
}
上面的例子中,有一個聚合根,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聚合根了。這里實際上就是我說的上面的第三個要素發生了結構性變化,導致我們無法通過事件溯源重建對象
看到這里,大家再回過頭去看一下我最上面對問題的闡述可能更好理解一點吧!