標籤:style blog http color 使用 os io 檔案
這篇文章應該算是對前三篇的一個補充,在寫之前說個題外話,有園友評論這是在用三層架構在寫DDD,我的個人理解DDD是一種設計思想,跟具體用什麼架構應該沒有什麼關係,DDD也需要分層,也有三層架構的影子在裡面。三層架構主要是表現層、業務層和資料層,而DDD已經沒有資料層,三層結構裡的模型是貧血的,而DDD卻是充血的。如果你在用三層架構已經有了彙總,實體,值對象的概念,那說明你已經在靠近DDD了,或者你不願相信罷了,當然你可以保留自己的觀點,這裡不作爭論,我也不能作出結論,我個人是覺得這種討論也是有意義的,我也會思考之前所介紹的到底是不是DDD,這個答案留給各位讀者吧。總之歡迎形式各樣的評論。
接下來我就來介紹一下CQRS(命令和查詢職責分離 )風格的架構。在學習CQRS的時候,有很多人說這個太高大上,難以應用。我想說CQRS不是那麼可怕,當然也不是那麼簡單。那麼就開始慢慢來揭開面紗。首先還是先看看經典DDD在Application層是這麼做的,先定義一個介面
public interface IUserService{ UserDTO GetUserInfo(string userId); IEnumerable<UserDTO> GetAllUsers(); void RegisterUser(UserDTO userData); void ModifyContactInfo(UserDTO userData); void ModifyPassword(string userId, string oldPwd, string newPwd);}
通過代碼會發現定義的介面有一點點規律,要麼是有傳回值,要麼就是沒有傳回值的,那麼他們有什麼特點呢?請注意我在寫代碼的時候特意在兩個介面之間加了斷行符號以區分,上面兩個主要為了返回資料,是查詢,下面三個其中一個是建立資料,剩下的是修改資料,是命令。也就是說一個方法要麼是執行某種動作的命令,要麼是返回資料的查詢且查詢不應該會影響資料,不可能兩者同時存在(可能你並不認同,有一種情況是特殊的,就是當實體標識由資料庫來提供的,那麼有時我們需要知道它的標識,但我也建議該方法不應該有傳回值,可以用out,或者是給傳輸對象進行賦值,在有些環境下後一種也並不能解決),也就類似一次向伺服器發送url請求時,要麼是get,要麼是post,不可能即是get,又是post。
當前介面只定義了一個DTO,該DTO的描述可能會過於宏大,只有當我們知道需要調用哪個介面時才會知道此DTO有哪些資料,於是當ui層去對DTO賦值往往也會不知所措,你是不是有針對不同的介面去定義相應的DTO的想法呢?至少我有,那麼這樣的DTO和介面是不是具有相同功能的表達呢?我已經開始會將上頁分成兩個介面了
再來說說查詢,查詢主要是為了ui呈現資料,經典DDD的查詢一般都是通過repository(具體實現很多情況下會選擇orm),然後將domain model轉成dto,這種方式限制很大,對於一些複雜資料就會顯得很難,如當要查看一個user資訊時還要展示他的role資訊,這樣就需要通過repository先查出user,然後再通過user.RoleID再查詢role,最終資料轉換成ui需要的model,應用程式層開發就會有點繁瑣了,不如關聯式資料庫一句sql來的方便。當然,現在的orm(如nh、ef)提供了級聯查詢,這樣就會在user上定義一個role屬性,雖然是方便了很多,但是這樣的回報也僅僅是為了查詢,對於我們跟蹤狀態一點用也沒有,為什嗎?當一個使用者修改角色時,需要role對象嗎?不需要,只需要他的ID,因此在彙總之間的引用應該盡量用引用ID,而不是引用對象,所以彙總之間盡量低耦合,“低耦合高內聚”這個標準也能夠更好進行模組式開發。再有一些匯總查詢,估計repository實現人員快要瘋了,寫應用程式層的人估計更要瘋,呵呵。使用orm帶來的好處是顯而易見的,但是面對查詢,orm並不是那麼完美,儘管現在的orm查詢功能已經很強大。經過以上闡述你可能有了一點想法,讓查詢繞開倉儲。
接下來就開始CQRS吧。不細說查詢了,在上述介面重新定義一個名稱叫做IUserQuerySerice,我已經開始注重新命名了,去掉裡面的命令方法就行了。那麼只要針對ui展示資料用的查詢DTO就行了,他也可以叫ReadModel(唯讀模型),我個人覺得這個叫法更合適一點。那麼實現你用資料庫檢視也行,用sql也好,達到目的就行。還要就是需要定義多少ReadModel,這個仁者見仁,智者見智。
重點是命令處理,為C端設計一個介面
public interface ICommandBus{ void Send(ICommand command);}
就這麼簡單,但是這帶來了需要大量寫Command,即每有一個操作就需要定義一個命令模型,然後還要寫該命令對應的處理器,還是拿之前的使用者註冊的例子來示範代碼吧
public class RegisterUserCommand : ICommand{ public string Name { get; set; } public string Password { get; set; } public string Email { get; set; }}public class RegisterUserHandler : ICommandHandler<RegisterUserCommand>{ private readonly IRegisterUserService _registerUserService; private readonly IUserRepository _userRepository; public void Handle(RegisterUserCommand command) { User user = _registerUserService.RegisterNewUser(command.Name, command.Password, command.Email); _userRepository.Add(user); }}
這種架構風格帶來了大量的代碼工作,就是需要定義很多Command。CommandBus的具體實現就是運用了訂閱/發布,即一個Command發送過來,系統會去找對應的CommandHandler,這樣的代碼寫起來會顯示更乾淨。
CQRS不是一個讓你覺得是多麼炫麗的架構,他的這種複雜性其實也是合理的,因為他是為瞭解決資料顯示的複雜性。
接下來我就說說ES。什麼是ES?全稱是Event Sourcing,事件來源。在未用ES之前,資料庫中儲存的彙總只是最後一次完整狀態的資料,他不能反應彙總的曆史變遷,除非你使用了其他的方式。還記得之前我稍微說了一下事件驅動嗎?用了ES,必然要有事件驅動的(目前為止我還沒有其他好的方式),而且還要接受最終一致性。什麼是最終一致性?後面再說吧。還是用代碼示範,在這裡還是使用者註冊,為了方便這裡使用者密碼就先不加密了,領域內的代碼大致就這些
public class UserCreated : IDomainEvent{ public UserCreated(string name, string password) { this.Name = name; this.Password = password; } public string SourceId { get; set; } public int Version { get; set; } public string Name { get; private set; } public string Password { get; private set; }}public class User : IAggregateRoot{ private readonly IList<IDomainEvent> _uncommittedEvents = new List<IDomainEvent>(); IEnumerable<IDomainEvent> IAggregateRoot.Events { get { return this._uncommittedEvents; } } public User(string id) { this.Id = id; } public User(string name, string password) : this(Guid.NewGuid().ToString()) { OnUserCreated(new UserCreated(name, password)); } private void OnUserCreated(UserCreated @event) { @event.SourceId = this.Id; @event.Version = this.Version + 1; Handler(@event); this.Version = @event.Version; _uncommittedEvents.Add(@event); } private void Handle(UserCreated @event) { this.Name = @event.Name; this.Password = @event.Password; } void IAggregateRoot.LoadFrom(IEnumerable<IDomainEvent> events) { foreach (var @event in events) {
Handle(@event);
this.Version = @event.Version;
} } public string Id { get; private set; } public int Version { get; private set; } public string Name { get; private set; } public string Password { get; private set; } }public class IRepository{ T Get<T>(string id) where T : class, IAggregateRoot; void Save<T>(T aggregate, string commandId) where T : class, IAggregateRoot;}
不知道上面的代碼你能不能夠大致明白。在這裡倉儲的功能更為有限,只有擷取和儲存彙總。當new一個user時會產生個事件,同時為事件記錄一個版本號碼,彙總會得到最終的版本號碼,而且狀態的修改是由事件驅動的。這個時候我們還看不出來事件的作用。別急,簡單看下倉儲的實現。儲存彙總到底發生了什麼
public class SourcedEvent{ public SourcedEvent(string aggregateId, string aggregateName, int version) { this.AggregateId = aggregateId; this.AggregateName = aggregateName; this.Version = version; } public string AggregateId { get; private set; } public string AggregateName { get; private set; } public int Version { get; private set; } public string Payload { get; set; } public string CorrelationId { get; set; }}public class EventSourcedRepository : IRepository{ public void Save<T>(T aggregate, string commandId) where T : class, IAggregateRoot { var events = aggregate.Events .Select(@event => new SourcedEvent(aggregate.Id, typeof(T).Name, @event.Version) { CorrelationId = commandId, Payload = _serializer.Serialize(@event) }).ToArray(); using (var connection = new SqlConnection()) { using (var trans = connection.BeginTransaction()) { try { foreach (var @event in events) { //TODO添加事件sql } trans.Commit(); } catch (Exception) { trans.RollBack(); throw; } } } _eventBus.Publish(aggregate.Events); }}
你會看到此時儲存的僅僅是事件,持久化成功了,會將事件發布出去。這樣C端的寫資料庫設計可以簡單到只需要記錄Events的一張表。而且最大的好處在於只會對event進行insert,還是就是他的儲存介質不一定就需要db,甚至文字檔都行(為每個彙總建立一個檔案,然後將事件追加,多麼簡單),想想都會興奮,有點顛覆吧。
然後你再寫個同步讀資料庫的EventHandler,還是再貼上代碼,已經寫了這麼多了,不在乎再寫一個了
public class UserDataSyncHandler : IEventHandler<UserCreated>{ public void Handle(UserCreated @event) { string sql = string.Format("insert user(id, name, password) values(‘{0}‘,‘{1}‘,‘{2}‘)", @event.SourceId, @event.Name, @event.Password); }}
至此,大致做法已經介紹完了。在這過程中你會發現用了CQRS+ES架構,可以完全拋棄ORM了,喜歡寫sql的夥伴們可能會更興奮,也許會和我一樣想說“ORM我早就看你有點不爽了”,呵呵。還要說明一下前面所說的最終一致性,就是C端的事件持久化完時此時Q端的資料並沒有同步過來,會存在一點延遲,但這種延遲不會太久,甚至會感覺不到。到了這裡我還要有一個感觸就是有了這樣的架構去實現DDD,你還認為彙總模型就是資料模型嗎?或者說他倆是同胞兄弟嗎?
最後再展示下如何通過事件還原彙總,還是上代碼吧,誰讓我如此喜歡用代碼來描述呢
public class EventSourcedRepository : IRepository{ public T Get<T>(string id) where T : class, IAggregateRoot { IEnumerable<IDomainEvent> events; using (var connection = new SqlConnection()) { //TODO彙總名稱和彙總ID取出事件並對版本號碼進行升序 } var aggregate = (T)Activator.CreateInstance(typeof(T), id); aggregate.LoadFrom(events); return aggregate; }}
這樣彙總就可以還原到最後一次的狀態了。就像以前的電影膠片一樣,每個事件對應著一個畫面,放完了也就完了。
通過上面的介紹,你應該會了大致的瞭解了,現在來看這張圖估計你就不會覺得有多麼高大上了(先跳過wcf)
CQRS+ES的結合帶來了很大的亮點,但是要應用考慮的會很多,複雜度也會很大,如果同步資料,如果事件丟了怎麼辦?產生的事件執行順序跟我們的預期不一樣怎麼辦?遇到並發怎麼辦?實體的id建置原則等等好多問題。有了問題自己的知識範圍也會擴大和提高。總之,CQRS+ES可討論的太多了,我也無法一一列舉。就先寫到這兒了,這一篇應該是這周最長的一篇了,明天周末了,歇兩天。