asp.net core系列 64 結(jié)合eShopOnWeb全面認識領(lǐng)域模型架構(gòu)

一.項目分析

在上篇中介紹了什么是"干凈架構(gòu)"鱼鸠,DDD符合了這種干凈架構(gòu)的特點匿乃,重點描述了DDD架構(gòu)遵循的依賴倒置原則雹姊,使軟件達到了低藕合。eShopOnWeb項目是學(xué)習(xí)DDD領(lǐng)域模型架構(gòu)的一個很好案例耐朴,本篇繼續(xù)分析該項目各層的職責功能众弓,主要掌握ApplicationCore領(lǐng)域?qū)觾?nèi)部的術(shù)語、成員職責隔箍。

1. web層介紹

eShopOnWeb項目與Equinox項目谓娃,雙方在表現(xiàn)層方面對比,沒有太大區(qū)別蜒滩。都是遵循了DDD表現(xiàn)層的功能職責滨达。有一點差異的是eShopOnWeb把表現(xiàn)層和應(yīng)用服務(wù)層集中在了項目web層下,這并不影響DDD風(fēng)格架構(gòu)俯艰。

項目web表現(xiàn)層引用了ApplicationCore領(lǐng)域?qū)雍虸nfrastructure基礎(chǔ)設(shè)施層捡遍,這種引用依賴是正常的。引用Infrastructure層是為了添加EF上下文以及Identity用戶管理竹握。 引用ApplicationCore層是為了應(yīng)用程序服務(wù) 調(diào)用 領(lǐng)域服務(wù)處理領(lǐng)域業(yè)務(wù)画株。

在DDD架構(gòu)下依賴關(guān)系重點強調(diào)的是領(lǐng)域?qū)拥莫毩ⅲI(lǐng)域?qū)邮峭膱A中最核心的層啦辐,所以在eShopOnWeb項目中谓传,ApplicationCore層并沒有依賴引用項目其它層。再回頭看Equinox項目芹关,領(lǐng)域?qū)右膊恍枰蕾囈庙椖科渌鼘印?/p>

下面web混合了MVC和Razor续挟,結(jié)構(gòu)目錄如下所示:


image

(1) Health checks 
 Health checks是ASP.NET Core的特性,用于可視化web應(yīng)用程序的狀態(tài)侥衬,以便開發(fā)人員可以確定應(yīng)用程序是否健康诗祸。運行狀況檢查端點/health。

//添加服務(wù)
           services.AddHealthChecks()
                .AddCheck<HomePageHealthCheck>("home_page_health_check")
                .AddCheck<ApiHealthCheck>("api_health_check");
         //添加中間件
          app.UseHealthChecks("/health");

下圖檢查了web首頁和api接口的健康狀態(tài)轴总,如下圖所示


image

 (2) Extensions

向現(xiàn)有對象添加輔助方法直颅。該Extensions文件夾有兩個類,包含用于電子郵件發(fā)送和URL生成的擴展方法怀樟。
 (3) 緩存
 對于Web層獲取數(shù)據(jù)庫的數(shù)據(jù)功偿,如果數(shù)據(jù)不會經(jīng)常更改,可以使用緩存漂佩,避免每次請求頁面時脖含,都去讀取數(shù)據(jù)庫數(shù)據(jù)。這里用的是本機內(nèi)存緩存投蝉。

//緩存接口類
        private readonly IMemoryCache _cache;

        // 添加服務(wù)养葵,緩存類實現(xiàn)
      services.AddScoped<ICatalogViewModelService, CachedCatalogViewModelService>();

        //添加服務(wù),非緩存的實現(xiàn)
        //services.AddScoped<ICatalogViewModelService, CatalogViewModelService>();
2. ApplicationCore層

ApplicationCore是領(lǐng)域?qū)哟窭拢琼椖恐凶钪匾顝?fù)雜的一層关拒。ApplicationCore層包含應(yīng)用程序的業(yè)務(wù)邏輯,此業(yè)務(wù)邏輯包含在領(lǐng)域模型中。領(lǐng)域?qū)又R在Equinox項目中并沒有講清楚着绊,這里在重點解析領(lǐng)域?qū)觾?nèi)部成員谐算,并結(jié)合項目來說清楚。

下面講解領(lǐng)域?qū)觾?nèi)部的成員職責描述定義归露,參考了“Microsoft.NET企業(yè)級應(yīng)用架構(gòu)設(shè)計 第二版”洲脂。
領(lǐng)域?qū)觾?nèi)部包括:領(lǐng)域模型和領(lǐng)域服務(wù)二大塊。涉及到的術(shù)語:

領(lǐng)域模型(模型)

1)模塊
2)領(lǐng)域?qū)嶓w(也叫"實體")

3)值對象

4)聚合
領(lǐng)域服務(wù)(也叫"服務(wù)")
倉儲

下面是領(lǐng)域?qū)又饕某蓡T:



下面是聚合與領(lǐng)域模型的關(guān)系剧包。最終領(lǐng)域模型包含了:聚合恐锦、單個實體、值對象的結(jié)合疆液。



(1) 領(lǐng)域模型
領(lǐng)域模型是提供業(yè)務(wù)領(lǐng)域的概念視圖一铅,它由實體和值對象構(gòu)成。在下圖中Entities文件夾是領(lǐng)域模型堕油,可以看到包含了聚合潘飘、實體、值對象掉缺。
image

 1.1 模塊

模塊是用來組織領(lǐng)域模型卜录,在.net中領(lǐng)域模型通過命令空間組織,模塊也就是命名空間攀圈,用來組織類庫項目里的類暴凑。比如:

namespace Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate

        namespace Microsoft.eShopWeb.ApplicationCore.Entities.BuyerAggregate

1.2 實體
實體通常由數(shù)據(jù)和行為構(gòu)成峦甩。如果要在整個生命周期的上下文里唯一跟蹤它赘来,這個對象就需要一個身份標識(ID主鍵),并看成實體凯傲。 如下所示是一個實體:

/// <summary>
    /// 領(lǐng)域?qū)嶓w都有唯一標識犬辰,這里用ID做唯一標識
    /// </summary>
    public class BaseEntity
    {
        public int Id { get; set; }
    }

    /// <summary>
    /// 領(lǐng)域?qū)嶓w,該實體行為由Basket聚合根來操作
    /// </summary>
    public class BasketItem : BaseEntity
    {
        public decimal UnitPrice { get; set; }
        public int Quantity { get; set; }
        public int CatalogItemId { get; set; }
 }

1.3 值對象

值對象和實體都由.net 類構(gòu)成冰单。值對象是包含數(shù)據(jù)的類幌缝,沒有行為,可能有方法本質(zhì)上是輔助方法诫欠。值對象不需要身份標識涵卵,因為它們不會改變狀態(tài)。如下所示是一個值對象

/// <summary>
    /// 訂單地址 值對象是普通的DTO類荒叼,沒有唯一標識轿偎。
    /// </summary>
    public class Address // ValueObject
    {
        public String Street { get; private set; }

        public String City { get; private set; }

        public String State { get; private set; }

        public String Country { get; private set; }

        public String ZipCode { get; private set; }

        private Address() { }

        public Address(string street, string city, string state, string country, string zipcode)
        {
            Street = street;
            City = city;
            State = state;
            Country = country;
            ZipCode = zipcode;
        }
   }

1.4 聚合

在開發(fā)中單個實體總是互相引用,聚合的作用是把相關(guān)邏輯的實體組合當作一個整體對待被廓。聚合是一致性(事務(wù)性)的邊界坏晦,對領(lǐng)域模型進行分組和隔離。聚合是關(guān)聯(lián)的對象(實體)群,放在一個聚合容器中昆婿,用于數(shù)據(jù)更改的目的球碉。每個聚合通常被限制于2~3個對象。聚合根在整個領(lǐng)域模型都可見仓蛆,而且可以直接引用睁冬。

/// <summary>
    /// 定義聚合根,嚴格來說這個接口不需要任務(wù)功能看疙,它是一個普通標記接口
    /// </summary>
    public interface IAggregateRoot
    {

    }
    

    /// <summary>
    /// 創(chuàng)建購物車聚合根痴突,通常實現(xiàn)IAggregateRoot接口 
    /// 購物車聚合模型(包括Basket、BasketItem實體)
    /// </summary>
    public class Basket : BaseEntity, IAggregateRoot
    {
        public string BuyerId { get; set; }
        private readonly List<BasketItem> _items = new List<BasketItem>();
      
        public IReadOnlyCollection<BasketItem> Items => _items.AsReadOnly();   
        //...
    }

在該項目中領(lǐng)域模型與“Microsoft.NET企業(yè)級應(yīng)用架構(gòu)設(shè)計第二版”書中描述的職責有不一樣地方狼荞,來看一下:

(1) 領(lǐng)域服務(wù)有直接引用聚合中的實體(如:BasketItem)辽装。書中描述是聚合中實體不能從聚合之處直接引用,應(yīng)用把聚合看成一個整體相味。

(2) 領(lǐng)域?qū)嶓w幾乎都是貧血模型拾积。書中描述是領(lǐng)域?qū)嶓w應(yīng)該包括行為和數(shù)據(jù)。
 (2) 領(lǐng)域服務(wù)

領(lǐng)域服務(wù)類方法實現(xiàn)領(lǐng)域邏輯丰涉,不屬于特定聚合中(聚合是屬于領(lǐng)域模型的)拓巧,很可能跨多個實體。當一塊業(yè)務(wù)邏輯無法融入任何現(xiàn)有聚合一死,而聚合又無法通過重新設(shè)計適應(yīng)操作時肛度,就需要考慮使用領(lǐng)域服務(wù)。下圖是領(lǐng)域服務(wù)文件夾:


/// <summary>
        /// 下面是創(chuàng)建訂單服務(wù)投慈,用到的實體包括了:Basket承耿、BasketItem、OrderItem伪煤、Order跨越了多個聚合加袋,該業(yè)務(wù)放在領(lǐng)域服務(wù)中完全正確。
        /// </summary>
        /// <param name="basketId">購物車ID</param>
        /// <param name="shippingAddress">訂單地址</param>
        /// <returns>回返回類型</returns>
        public async Task CreateOrderAsync(int basketId, Address shippingAddress)
        {
            var basket = await _basketRepository.GetByIdAsync(basketId);
            Guard.Against.NullBasket(basketId, basket);
            var items = new List<OrderItem>();
            foreach (var item in basket.Items)
            {
                var catalogItem = await _itemRepository.GetByIdAsync(item.CatalogItemId);
                var itemOrdered = new CatalogItemOrdered(catalogItem.Id, catalogItem.Name, catalogItem.PictureUri);
                var orderItem = new OrderItem(itemOrdered, item.UnitPrice, item.Quantity);
                items.Add(orderItem);
            }
            var order = new Order(basket.BuyerId, shippingAddress, items);

            await _orderRepository.AddAsync(order);
        }

在該項目與“Microsoft.NET企業(yè)級應(yīng)用架構(gòu)設(shè)計第二版”書中描述的領(lǐng)域服務(wù)職責不完全一樣抱既,來看一下:

(1) 項目中职烧,領(lǐng)域服務(wù)只是用來執(zhí)行領(lǐng)域業(yè)務(wù)邏輯,包括了訂單服務(wù)OrderService和購物車服務(wù)BasketService防泵。書中描述是可能跨多個實體蚀之。當一塊業(yè)務(wù)邏輯無法融入任何現(xiàn)有聚合。

[

](javascript:void(0); "復(fù)制代碼")

/// <summary>
        /// 添加購物車服務(wù)捷泞,沒有跨越多個聚合足删,應(yīng)該不放在領(lǐng)域服務(wù)中。
        /// </summary>
        /// <param name="basketId"></param>
        /// <param name="catalogItemId"></param>
        /// <param name="price"></param>
        /// <param name="quantity"></param>
        /// <returns></returns>
        public async Task AddItemToBasket(int basketId, int catalogItemId, decimal price, int quantity)
        {
            var basket = await _basketRepository.GetByIdAsync(basketId);

            basket.AddItem(catalogItemId, price, quantity);

            await _basketRepository.UpdateAsync(basket);
        }

總的來說肚邢,eShopOnWeb項目雖然沒有完全遵循領(lǐng)域?qū)又幸佳撸蓡T職責描述拭卿,但可以理解是在代碼上簡化了領(lǐng)域?qū)拥膹?fù)雜性。

(3) 倉儲

倉儲是協(xié)調(diào)領(lǐng)域模型和數(shù)據(jù)映射層的組件贱纠。倉儲是領(lǐng)域服務(wù)中最常見類型峻厚,它負責持久化。倉儲接口的實現(xiàn)屬于基礎(chǔ)設(shè)施層谆焊。倉儲通郴萏遥基于一個IRepository接口。 下面看下項目定義的倉儲接口辖试。

/// <summary>
    /// T是領(lǐng)域?qū)嶓w辜王,是BaseEntity類型的實體
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public interface IAsyncRepository<T> where T : BaseEntity
    {
        Task<T> GetByIdAsync(int id);
        Task<IReadOnlyList<T>> ListAllAsync();
        //使用領(lǐng)域規(guī)則查詢
        Task<IReadOnlyList<T>> ListAsync(ISpecification<T> spec);
        Task<T> AddAsync(T entity);
        Task UpdateAsync(T entity);
        Task DeleteAsync(T entity);
        //使用領(lǐng)域規(guī)則查詢
        Task<int> CountAsync(ISpecification<T> spec);
    }

(4) 領(lǐng)域規(guī)則

在倉儲設(shè)計查詢接口時,可能還會用到領(lǐng)域規(guī)則罐孝。 在倉儲中一般都是定義固定的查詢接口,如上面?zhèn)}儲的IAsyncRepository所示呐馆。而復(fù)雜的查詢條件可能需要用到領(lǐng)域規(guī)則。在本項目中通過強大Linq 表達式樹Expression 來實現(xiàn)動態(tài)查詢莲兢。


/// <summary>
    /// 領(lǐng)域規(guī)則接口汹来,由BaseSpecification實現(xiàn)
    /// 最終由Infrastructure.Data.SpecificationEvaluator<T>類來構(gòu)建完整的表達樹
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public interface ISpecification<T>
    {
        //創(chuàng)建一個表達樹,并通過where首個條件縮小查詢范圍改艇。
        //實現(xiàn):IQueryable<T> query = query.Where(specification.Criteria)
        Expression<Func<T, bool>> Criteria { get; }

        //基于表達式的包含
        //實現(xiàn)如: Includes(b => b.Items)
        List<Expression<Func<T, object>>> Includes { get; }
        List<string> IncludeStrings { get; }

        //排序和分組
        Expression<Func<T, object>> OrderBy { get; }
        Expression<Func<T, object>> OrderByDescending { get; }
        Expression<Func<T, object>> GroupBy { get; }

        //查詢分頁
        int Take { get; }
        int Skip { get; }
        bool isPagingEnabled { get;}
    }

最后Interfaces文件夾中定義的接口收班,都由基礎(chǔ)設(shè)施層來實現(xiàn)。如:

IAppLogger日志接口

IEmailSender郵件接口

IAsyncRepository倉儲接口

3.Infrastructure層

基礎(chǔ)設(shè)施層Infrastructure依賴于ApplicationCore谒兄,這遵循依賴倒置原則(DIP)摔桦,Infrastructure中代碼實現(xiàn)了ApplicationCore中定義的接口(Interfaces文件夾)承疲。該層沒有太多要講的赊豌,功能主要包括:使用EF Core進行數(shù)據(jù)訪問熙兔、Identity麸锉、日志柳爽、郵件發(fā)送磷脯。與Equinox項目的基礎(chǔ)設(shè)施層差不多柿赊,區(qū)別多了領(lǐng)域規(guī)則诡蜓。



領(lǐng)域規(guī)則SpecificationEvaluator.cs類用來構(gòu)建查詢表達式(Linq expression)洽腺,該類返回IQueryable<T>類型核无。IQueryable接口并不負責查詢的實際執(zhí)行炼彪,它所做的只是描述要執(zhí)行的查詢拷橘。

public class EfRepository<T> : IAsyncRepository<T> where T : BaseEntity
    {
        //...這里省略的是常規(guī)查詢冗疮,如ADDAsync、UpdateAsync诅挑、GetByIdAsync ...

        //獲取構(gòu)建的查詢表達式
      private IQueryable<T> ApplySpecification(ISpecification<T> spec)
        {
            return SpecificationEvaluator<T>.GetQuery(_dbContext.Set<T>().AsQueryable(), spec);
        }  
    } 
public class SpecificationEvaluator<T> where T : BaseEntity
    {
        /// <summary>
        /// 做查詢時忿危,把返回類型IQueryable當作通貨
        /// </summary>
        /// <param name="inputQuery"></param>
        /// <param name="specification"></param>
        /// <returns></returns>
        public static IQueryable<T> GetQuery(IQueryable<T> inputQuery, ISpecification<T> specification)
        {
            var query = inputQuery;

            // modify the IQueryable using the specification's criteria expression
            if (specification.Criteria != null)
            {
                query = query.Where(specification.Criteria);
            }

            // Includes all expression-based includes
            //TAccumulate Aggregate<TSource, TAccumulate>(this IEnumerable<TSource> source, TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> func);
            //seed:query初始的聚合值
            //func:對每個元素調(diào)用的累加器函數(shù)
            //返回TAccumulate:累加器的最終值
            //https://msdn.microsoft.com/zh-cn/windows/desktop/bb549218
            query = specification.Includes.Aggregate(query,
                                    (current, include) => current.Include(include));

            // Include any string-based include statements
            query = specification.IncludeStrings.Aggregate(query,
                                    (current, include) => current.Include(include));

            // Apply ordering if expressions are set
            if (specification.OrderBy != null)
            {
                query = query.OrderBy(specification.OrderBy);
            }
            else if (specification.OrderByDescending != null)
            {
                query = query.OrderByDescending(specification.OrderByDescending);
            }

            if (specification.GroupBy != null)
            {
                query = query.GroupBy(specification.GroupBy).SelectMany(x => x);
            }

            // Apply paging if enabled
            if (specification.isPagingEnabled)
            {
                query = query.Skip(specification.Skip)
                             .Take(specification.Take);
            }
            return query;
        }
    }

參考資料

Microsoft.NET企業(yè)級應(yīng)用架構(gòu)設(shè)計 第二版

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末伐蒂,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子缕减,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件怜械,死亡現(xiàn)場離奇詭異享完,居然都是意外死亡,警方通過查閱死者的電腦和手機有额,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門般又,熙熙樓的掌柜王于貴愁眉苦臉地迎上來彼绷,“玉大人,你說我怎么就攤上這事茴迁〖拿酰” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵堕义,是天一觀的道長猜旬。 經(jīng)常有香客問我,道長倦卖,這世上最難降的妖魔是什么洒擦? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮怕膛,結(jié)果婚禮上熟嫩,老公的妹妹穿的比我還像新娘。我一直安慰自己褐捻,他們只是感情好掸茅,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著柠逞,像睡著了一般昧狮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上板壮,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天逗鸣,我揣著相機與錄音,去河邊找鬼个束。 笑死慕购,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的茬底。 我是一名探鬼主播沪悲,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼阱表!你這毒婦竟也來了殿如?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤最爬,失蹤者是張志新(化名)和其女友劉穎涉馁,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體爱致,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡烤送,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了糠悯。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片帮坚。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡妻往,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出试和,到底是詐尸還是另有隱情讯泣,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布阅悍,位于F島的核電站好渠,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏节视。R本人自食惡果不足惜拳锚,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望肴茄。 院中可真熱鬧晌畅,春花似錦、人聲如沸寡痰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拦坠。三九已至连躏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間贞滨,已是汗流浹背入热。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留晓铆,地道東北人勺良。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像骄噪,于是被迫代替她去往敵國和親尚困。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容