文章出處

寫在前面

首先,本篇博文主要包含兩個主題:

  1. 領域服務中使用倉儲
  2. SELECT 某某某(有點暈?請看下面。)

上一篇:Repository 倉儲,你的歸宿究竟在哪?(二)-這樣的應用層代碼,你能接受嗎?

關于倉儲這個系列,很多園友問我:為什么糾結倉儲?我覺得需要再次說明下(請不要再“糾結”了),引用上一篇博文中某一段評論的回復:

關于“糾結于倉儲”這個問題,其實博文中我就有說明,不是說我糾結或是陷入這個問題,而是我覺得在實踐領域驅動設計中,倉儲的調用是一個很重要的東西,如果使用的不恰當,也許就像上面我所貼出來的應用層代碼一樣,我個人覺得,這是很多人在實踐領域驅動設計中,很容易踩的一個坑,我只是希望可以把這個過程分享出來,給有相同困惑的人,可以借鑒一下。

領域服務和倉儲的兩種“微妙關系”

這邊的“領域服務”和倉儲的關系,可以理解為在領域中調用倉儲,具體表現為在領域服務中使用。

在很久之前,我為了保持所謂的“領域純潔”,在領域服務設計的時候,沒有參雜倉儲任何的調用,但是隨著應用程序的復雜,很多業務添加進來,一個單純的“業務描述”并不能真正去實現業務用例,所以這時候的領域服務就被“架空”了,一些業務實現“迫不得已”放在了應用層,也就是上一篇我所貼出的應用層代碼,不知道你能不能接受?反正我是接受不了,所以我做了一些優化,領域服務中調用了倉儲。

關于領域服務中調用倉儲,在上一篇博文討論中(czcz1024、Jesse Liu、netfocus、劉標才...),主要得出兩種實現方式,這邊我再大致總結下:

  1. 傳統方式:倉儲接口定義在領域層,實現在基礎層,通過規約來約束查詢,一般返回類型為聚合根集合對象,如果領域對象的查詢邏輯比較多,具體體現就是倉儲接口變多。
  2. IQueryable 方式:和上面不同的是接口的設計變少了,因為返回類型為 IQueryable,具體查詢表達式的組合放在了調用層,也就是領域服務中,比如:xxxRepository.GetAll().Where(x=>....)

其實這兩種方式都是一把雙刃劍,關鍵在于自己根據具體的業務場景進行選擇了,我說一下我的一些理解,比如現實生活中車庫的場景,我們可以把車庫看作是倉儲,取車的過程看作是倉儲的調用,車子的擺放根據汽車的規格,也就是倉儲中的規約概念,比如我今天要開一輛德系、紅色、敞篷、雙門的跑車(條件有點多哈),然后我就去車庫取車,在車庫的“調度系統“(在倉儲的具體表現,可以看作是 EF)中輸入這些命令,然后一輛蘭博基尼就出現在我的眼前了。

在上面描述的現實場景中,如果是第一種傳統方式,“我要開一輛德系、紅色、敞篷、雙門的跑車”這個就可以設計為倉儲的一個接口,為什么?因為車庫可以換掉,而這些業務用例一般不會進行更改,車庫中的“調度系統”根據命令是如何尋找汽車的呢?答案是規格的組合,也就是倉儲中規約的組合,我們在針對具體業務場景設計的時候,一般會提煉出這個業務場景中的規約,這個也是不可變的,根據命令來進行對這些規約的組合,這個過車的具體體現就是倉儲的實現,約束的是聚合根對象。這種方式中,我個人認為好處是可以充分利用規約,倉儲的具體調用統一管理,讓調用者感覺不到它是如何工作的,因為它只需要傳一個命令過去,就可以得到想要的結果,唯一不好的地方就是:我心情不好,每天開的汽車都不一樣,這個就要死人了,因為我要設計不同的倉儲接口來進行對規約的組合。

如果是第二種方式,也就是把“調度系統”的使用權交到自己手里(第一種的這個過程可以看作是通過秘書),這種方式的好與壞,我就不多說了,我現在使用的是第一種方式,主要有兩個原因:

  1. 防止 IQueryable 的濫用(領域服務非常像 DAL)。
  2. 現在應用場景中的查詢比較少,沒必要。

上一篇博文中貼出的是,發送短消息的應用層代碼,發送的業務驗證放在了應用層,以致于 SendSiteMessageService.SendMessage 中只有一段“return true”代碼,修改之后的領域服務代碼:

    public class SendSiteMessageService : ISendMessageService
    {
        public async Task<bool> SendMessage(Message message)
        {
            IMessageRepository messageRepository = IocContainer.Resolver.Resolve<IMessageRepository>();
            if (message.Type == MessageType.Personal)
            {
                if (System.Web.HttpContext.Current != null)
                {
                    if (await messageRepository.GetMessageCountByIP(Util.GetUserIpAddress()) > 100)
                    {
                        throw new CustomMessageException("一天內只能發送100條短消息");
                    }
                }
                if (await messageRepository.GetOutboxCountBySender(message.Sender) > 20)
                {
                    throw new CustomMessageException("1小時內只能向20個不同的用戶發送短消息");
                }
            }
            return true;
        }
    }

代碼就是這樣,如果你覺得有問題,歡迎提出,我再進行修改。

這邊再說一下領域服務中倉儲的注入,緣由是我前幾天看了劉標才的一篇博文:DDD領域驅動設計之領域服務,文中對倉儲的注入方式是通過構造函數,這種方式的壞處就是領域服務對倉儲產生強依賴關系,還有就是如果領域服務中注入了多個倉儲,調用這個領域服務中的某一個方法,而這個方法只是使用了一個倉儲,那么在對這個領域服務進行注入的時候,就必須把所有倉儲都要進行注入,這就沒有必要了。

解決上面的問題的方式就是,在使用倉儲的地方對其進行解析,比如:IocContainer.Resolve<IMessageRepository>();,這樣就可以避免了上面的問題,我們還可以把倉儲的注入放在 Bootstrapper 中,也就是項目啟動的地方。

SELECT 某某某

上面所探討的都是倉儲的調用,而現在這個問題是倉儲的實現,這是兩種不同的概念。

什么是“SELECT 某某某”?答案就是針對字段進行查詢,場景為應用程序的性能優化。我知道你看到“SELECT”就想到了事務腳本模式,不要想歪了哦,你眼中的倉儲實現不一定是 ORM,也可以是傳統的 ADO.NET,如果倉儲實現使用的是數據庫持久化機制,其實再高級的 ORM,到最后都會轉換成 SQL 代碼,具體表現就是對這些代碼的優化,似乎不屬于領域驅動設計的范疇了,但不可否認,這是應用程序不能不考慮的。

應用程序中的性能問題

我說一下現在短消息項目中倉儲的實現(常用場景):底層使用的是 EntityFramework,為了更好的理解,我貼一段查詢代碼:

        protected override async Task<IEnumerable<TAggregateRoot>> FindAll(ISpecification<TAggregateRoot> specification, System.Linq.Expressions.Expression<Func<TAggregateRoot, dynamic>> sortPredicate, SortOrder sortOrder, int pageNumber, int pageSize)
        {
            var query = efContext.Context.Set<TAggregateRoot>()
                .Where(specification.GetExpression());
            int skip = (pageNumber - 1) * pageSize;
            int take = pageSize;

            if (sortPredicate != null)
            {
                switch (sortOrder)
                {
                    case SortOrder.Ascending:
                        return query.SortBy(sortPredicate).Skip(skip).Take(take).ToListAsync();
                    case SortOrder.Descending:
                        return query.SortByDescending(sortPredicate).Skip(skip).Take(take).ToListAsync();
                    default:
                        break;
                }
            }
            return query.Skip(skip).Take(take).ToListAsync();
        }

這種方式有什么問題嗎?至少在我們做一些 DDD 示例的時候,沒有任何問題,為什么?因為你沒有實際去應用,也就體會不到一些問題,前一段時間短消息頁面加載慢,一個是數據庫索引問題(詳見:程序員眼中的 SQL Server-執行計劃教會我如何創建索引?),還有一個就是消息列表查詢的時候,把消息表的所有字段都取出來了,這是完全沒有必要的,比如消息內容就不需要進行讀取,但是我們在跟蹤上面代碼執行的時候,會發現 EntityFramework 生成的 SQL 代碼為 SELECT *。。。

走過的彎路

上面這個問題,至少從那個數據庫索引問題解決完,我就一直郁悶著,也嘗試著用各種方式去解決,比如創建 IQueryable 的 Select 表達式,傳入的是自定義的聚合根屬性,還有就是擴展 Select 表達式,詳細過程就不回首了,我貼一下當時在搜索時的一些資料:

在 EntityFramework 底層,我們 Get 查詢的時候,一般都是返回 TAggregateRoot 聚合根集合對象,也就是說,你沒有辦法在底層進行指定屬性查詢,因為聚合根只有 ID 一個屬性,唯一的辦法就是傳入 Expression<Func<TAggregateRoot, TAggregateRoot>> selector 表達式,select 兩個范型約束為 TSource 和 TDest,這邊我們兩種類型都為 TAggregateRoot ,但是執行結果為:“The entity or complex type ... cannot be constructed in a LINQ to Entities query.”,給我的教訓就是 Select 中的 TSource 和 TDest 不能為同一類型(至少指定屬性的情況下)。

我的解決方案

EntityFramework 底層的所有查詢返回類型改為 IQueryable<TAggregateRoot>,倉儲的查詢返回類型改為 IEnumerable<MessageListDTO>,為什么是 MessageListDTO 而不是 Message?因為我覺得消息列表的顯示,就是對消息的扁平化處理,沒必要是一個 Message 實體對象,雖然它是一個消息實體倉儲,就好比從車庫中取出一個所有汽車列表的單子,有必要把所有汽車實體取出來嗎?很顯然沒有必要,我們只需要取出汽車的一些信息即可,我覺得這是應對業務場景變化所必須要調整的,具體的實現代碼:

        public async Task<IEnumerable<MessageListDTO>> GetInbox(Contact reader, PageQuery pageQuery)
        {
            return await GetAll(new InboxSpecification(reader), sp => sp.ID, SortOrder.Descending, pageQuery.PageIndex, pageQuery.PageSize)
                 .Project().To<MessageListDTO>()
                 .ToListAsync();
        }

“Project().To()” 是什么東西?這是 AutoMapper 對 IQueryable 表達式的一個擴展,詳情請參閱:戀愛雖易,相處不易:當 EntityFramework 愛上 AutoMapper,AutoMapper 擴展說明:Queryable Extensions,簡單的一段代碼就可以完成實體與 DTO 之間的轉化,我們再次用 SQL Server Profiler 捕獲生成的 SQL 代碼,就會發現,這就是我們想要的,根據映射配置 Select 指定字段查詢。

寫在最后

針對“SELECT 某某某”這個實際應用問題,以上只是我的個人實現方式,如果你有疑問或是有更好的實現,歡迎指教。。。


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


全站熱搜
創作者介紹
創作者 大師兄 的頭像
大師兄

IT工程師數位筆記本

大師兄 發表在 痞客邦 留言(0) 人氣()