最近的開發工作涉及到兩個模塊“任務”和“日周報”。關系是日周報消費任務,因為用戶在寫日周報的時候,需要按一定的規則篩選當前用戶的任務,作為日周報的一部分提交。整個項目采用類似於Orchard那種平台加插件的架構,“任務”和“日周報”是兩個獨立的插件。
“任務”已經由一位同事事先寫好,周報中篩選任務的規則簡單描述如下:
- 截止日期在周一之前,且未完成的任務(超期或待審核);
- 截止日期在周一至周日之間的所有任務;
- 開始日期在周一至周日之間的所有任務;
- 截止日期在周日之后,且未設置開始日期的所有任務(進行中或待審核)。
看起來貌似挺簡單,敲代碼的時候卻發現下不了手,“任務”的倉儲層對“日周報”是不可見的,想要按照規則查詢任務列表,我只能調用TaskService,但TaskService中並沒有根據上述規則來篩選任務的方法。
怎么辦呢?為TaskService添加個實現上述規則的方法,比如GetTasksForWeeklyReport?想了想,貌似不是一個好的思路,因為是“日周報”在消費“任務”模塊,任務模塊應該是不知道日周報的存在的,直接寫一個只針對周報的方法總覺得心里有點不對勁。而且,也不希望以后日周報的需求更改而影響到任務。
再想想,日報中也有自己篩選任務的規則,按照上面那么搞,還需要為日報添加個方法GetTasksForDailyReport。如果其他的業務模塊也需要按一定的規則篩選任務列表的話,方法還得繼續追加下去。這樣勢必會造成TaskService的無比臃腫,而且其他的模塊的規則已修改,就要同步修改任務模塊。如果任務模塊單獨部署到一台機器上,這種麻煩程度就會更大。
這時候夜壺般的腦袋中閃過一個詞:規約。
規約模式可以簡單理解為條件判斷。就不在此照搬那些費解的概念了,按照現在遇到的問題舉例來說,我希望TaskService中有個這樣的方法:
GetTasksBySpecification(ISpecification specification);
specification是一個描述任務篩選規則的對象,TaskService可以根據這個對象所描述的規則來找出Task集合。對於周報來說,只需要實現ISpecification接口的具體實例,然后調用TaskService的GetTasksBySpecification方法並傳遞規約實例,就可以拿到想要的任務列表。對於日報來說,也一樣,實現自己的規約類就好。以后再有其他業務模塊需要根據自己的規則篩選任務的時候,也只需要實現一個規約類。
這樣就可以保證“任務”模塊的完整性,而且避免了TaskService無限臃腫的顧慮。
有了思想,就剩下具體實現了。主要參考了大神陳晴陽開發的DDD開發框架Apworks,其中提供了規約模式的.Net實現。
最終類圖如下:
ISpecification中定義了規約類需要實現的方法,其中IsSatisfiedBy用來判斷一個對象是否滿足改規約,GetExpression用來獲取表示該規約的表達式樹。DailyReportTaskSpecification和WeeklyReportTaskSpecification用來描述篩選規則。有時候查詢需要根據兩個規約以“and”條件進行查詢,所以又有了AndSpecification,用來把兩個規約以and條件組合到一起。
周報中任務篩選規則的規約類代碼大概是:
public class WeeklyReportTaskSpecification : SpecificationBase<TaskEntity>{ public override Expression<Func<TaskEntity, bool>> GetExpression(){ return task =>.....; } }
根據用戶Id篩選任務的規約類代碼:
public class UserInChargeTaskSpecification : SpecificationBase<TaskEntity>{ #region 私有字段 private readonly long _inchargeUserId; #endregion #region 構造器 public UserInChargeTaskSpecification(long inChargeUserId){ _inchargeUserId = inChargeUserId; } #endregion #region SpecificationBase<TaskEntity> 成員 public override Expression<Func<TaskEntity, bool>> GetExpression(){ return task =>task.UserIncharge!=null && task.UserIncharge == _inchargeUserId; } #endregion }
TaskService實現規約查詢的方法:
public IEnumerable<TaskEntity> GetTasksBySpecification(ISpecification<TaskEntity> spec){ return taskRepository.Table.Where(spec.IsSatisfiedBy); }
周報中通過如下代碼實現對TaskService中規約方法的調用:
public IEnumerable<TaskEntity> GetWeeklyTask(long userId, DateTime currentDateTime){ var userInChargeTaskSpecification = new UserInChargeTaskSpecification(userId); var weeklyReportTaskSpecification = new WeeklyReportTaskSpecification(); return TaskService.GetTasksBySpecification(userInChargeTaskSpecification.And(weeklyReportTaskSpecification)); }
除了需要根據規則篩選任務列表之外,還需要根據當前用戶的Id過濾,因為當前用戶只關心自己的任務。所以把兩個規約類通過And方法連接到一塊,組成一個規約,傳遞給GetTasksBySpecification方法。
試了下效果,五星好評!!!
補充:
往這篇博客中貼代碼的時候,TaskService中的GetTasksBySpecification中的實現讓我有點不放心。
因為ISpecification的IsSatisfiedBy屬性返回的是表達式樹Compile之后的委托,我直接傳遞給linq一個委托,會不會造成全表掃描?不會把整個表的數據加載到內存,然后挨個用委托過濾吧。這個很好驗證,查看一下最終執行的sql就可以了。
然后在園子里找到了dudu的這篇文章:Func引起的數據庫全表查詢;
於是GetTasksBySpecification的代碼修改如下:
public IEnumerable<TaskEntity> GetTasksBySpecification(ISpecification<TaskEntity> spec){ return taskRepository.Table.Where(spec.GetExpression()); }