今天我們來分析另一個開源的CQRS+ES項目:Equinox。該項目可以在github上下載並直接本地運行,項目地址:https://github.com/EduardoPires/EquinoxProject,該項目是基於 .net core 2.2的,開發語言、編碼方式比Diary.CQRS更加新潮(CQRS+ES項目解析-Diary.CQRS),也更符合我們現在的開發習慣。
項目概覽
首先通過github獲取到項目源代碼,打開項目文件,你會看到如下分層:
- Presentation:展示層,UI在該層實現
- Services:WebApi在該層實現,同樣隸屬於UI
- Application:應用程序服務層,提供了對Domain層接口的封裝,注重數據交換,DTO對象在該層定義
- Domain:領域層,項目的核心部分,領域對象、領域服務在該層實現
- Infra:基礎設施層,項目的公共部分(數據訪問)、切片(身份認證、消息發布、依賴注入)部分在該層實現
通過項目分層,我們已經對該項目有了一個大致的輪廓,當從Presentation、Services層接收到來自客戶端的請求后,將會調用Application層的應用程序服務,應用程序服務將數據進行封裝和轉換,然后交給Domain層進行處理,Domain層則調用Infra相關的方法完成持久化、消息發布等功能。
Domain層
Domain層是Equinox項目的核心部分,Entity/ValueObject、Repository、UoW、Command、Event、EventStore等均在該層進行定義,我們來看一下。
Entity對象
實體對象,定義如下:
public abstract class Entity
{
public Guid Id { get; protected set; }
public override bool Equals(object obj)
{
//......
}
public static bool operator ==(Entity a, Entity b)
{
//......
}
public static bool operator !=(Entity a, Entity b)
{
//......
}
public override int GetHashCode()
{
//......
}
public override string ToString()
{
//......
}
}
每一個實體對象都要具備ID屬性,用來標記唯一性;重寫了Equals方法、定義了==、!=操作符,用於兩個對象的比較;重寫了ToString方法、GetHashCode方法。
ValueObject
值對象,與實體對象進行區分,值對象沒有Id屬性。定義如下:
public abstract class ValueObject<T> where T : ValueObject<T>
{
public override bool Equals(object obj)
{
//......
}
protected abstract bool EqualsCore(T other);
public override int GetHashCode()
{
//......
}
protected abstract int GetHashCodeCore();
public static bool operator ==(ValueObject<T> a, ValueObject<T> b)
{
//......
}
public static bool operator !=(ValueObject<T> a, ValueObject<T> b)
{
//......
}
}
與Entity相似,定義了一些基本的操作方法。
Repository
數據倉儲,用來進行數據訪問,定義如下:
public interface IRepository<TEntity> : IDisposable where TEntity : class
{
void Add(TEntity obj);
TEntity GetById(Guid id);
IQueryable<TEntity> GetAll();
void Update(TEntity obj);
void Remove(Guid id);
int SaveChanges();
}
定義了對數據的基本操作,添加、更新、刪除、查詢等方法
UoW
工作單元,定義如下:
public interface IUnitOfWork : IDisposable
{
bool Commit();
}
定義了Commit方法,當業務邏輯執行完成用,用於數據庫事物
Command/CommandHandler 和 Event/EventHandler
CQRS和ES的核心部分,Command、Event被定義為消息,擁有共同的基類Message,分別定義如下:
Command:
public abstract class Command : Message
{
public DateTime Timestamp { get; private set; }
public ValidationResult ValidationResult { get; set; }
protected Command()
{
Timestamp = DateTime.Now;
}
public abstract bool IsValid();
}
Event:
public abstract class Event : Message, INotification
{
public DateTime Timestamp { get; private set; }
protected Event()
{
Timestamp = DateTime.Now;
}
}
與Command、Event對應的處理程序用來處理相應的業務邏輯,此處不在介紹。感興趣的朋友可以參照上篇文章進行了解。
EventStore
EventStore也是ES的核心內容,負責對事件的存儲、提取工作。在Equinox項目中,EventStore的定義如下:
public interface IEventStore
{
void Save<T>(T theEvent) where T : Event;
}
額?只有一個Save方法,這不符合邏輯,只能進行事件的存儲,而沒有事件的查詢。通過查閱項目的其它代碼,我發現事件的查詢則是通過EventStoreRepository來實現的,這一點不太符合我們的開放封閉原則和模塊化思想。作者可能是想着對事件的操作也遵循CQRS模式嗎?這就未可知了。
Bus
消息通信,Equinox項目中使用MediatR實現的基於內存的消息通信。定義如下:
public interface IMediatorHandler
{
Task SendCommand<T>(T command) where T : Command;
Task RaiseEvent<T>(T @event) where T : Event;
}
Infra層
基礎設施層里面,定義了Domain層接口的實現,例如Data中實現了倉儲、工作單元,Bus中實現了InMemoryBus等。由於都是非常簡單的實現,不再展開介紹。
Application層
應用程序服務層有兩個作用,封裝底層(Infra、Domain)的操作,對UI層(Presentation、Services)數據進行轉換,它是UI層與Domain層的橋梁。此處不再展開介紹。
UI層
Equinox項目中,UI層由兩部分組成,分別是Presentation和Services,其中展示層提供了界面操作的功能,Services層提供了接口訪問的功能,這兩個項目采用MVC和WebApi技術,不再展開介紹。
Equinox項目總結
通過分析Equinox項目的結構和代碼,我們可以發現,這個項目並不是很完善,作者所說的不要用在生產環境是實話。
在這個項目中,對於ES的實現並不是很優雅,首先EventStore的操作,未提供查詢事件的接口,從而導致了需要通過Repository來獲取Event,破壞了EventStore的完整性;其次該項目沒有完成事件重放功能,我們只能通過事件查看到數據的變更,但是無法通過重放來獲取項目的某個時段的狀態的功能;最后,Equinox項目未實現讀寫分離,對於數據的查詢和增加更新等操作都混合在一個Repository中,不利於我們進行讀寫分離。
以上請大家參考。