1. 程式人生 > >asp.net core系列 64 結合eShopOnWeb全面認識領域模型架構

asp.net core系列 64 結合eShopOnWeb全面認識領域模型架構

一.專案分析

  在上篇中介紹了什麼是"乾淨架構",DDD符合了這種乾淨架構的特點,重點描述了DDD架構遵循的依賴倒置原則,使軟體達到了低藕合。eShopOnWeb專案是學習DDD領域模型架構的一個很好案例,本篇繼續分析該專案各層的職責功能,主要掌握ApplicationCore領域層內部的術語、成員職責。

  

  1. web層介紹

    eShopOnWeb專案與Equinox專案,雙方在表現層方面對比,沒有太大區別。都是遵循了DDD表現層的功能職責。有一點差異的是eShopOnWeb把表現層和應用服務層集中在了專案web層下,這並不影響DDD風格架構。

    專案web表現層引用了ApplicationCore領域層和Infrastructure基礎設施層,這種引用依賴是正常的。引用Infrastructure層是為了新增EF上下文以及Identity使用者管理。 引用ApplicationCore層是為了應用程式服務 呼叫 領域服務處理領域業務。

    在DDD架構下依賴關係重點強調的是領域層的獨立,領域層是同心圓中最核心的層,所以在eShopOnWeb專案中,ApplicationCore層並沒有依賴引用專案其它層。再回頭看Equinox專案,領域層也不需要依賴引用專案其它層。

    下面web混合了MVC和Razor,結構目錄如下所示:

    

    (1) Health checks 

      Health checks是ASP.NET Core的特性,用於視覺化web應用程式的狀態,以便開發人員可以確定應用程式是否健康。執行狀況檢查端點/health。

         //新增服務
           services.AddHealthChecks()
                .AddCheck<HomePageHealthCheck>("home_page_health_check")
                .AddCheck<ApiHealthCheck>("api_health_check");
         //新增中介軟體
          app.UseHealthChecks("/health");

       下圖檢查了web首頁和api介面的健康狀態,如下圖所示

    (2) Extensions

      向現有物件新增輔助方法。該Extensions資料夾有兩個類,包含用於電子郵件傳送和URL生成的擴充套件方法。

 

    (3) 快取

      對於Web層獲取資料庫的資料,如果資料不會經常更改,可以使用快取,避免每次請求頁面時,都去讀取資料庫資料。這裡用的是本機記憶體快取。

        //快取介面類
        private readonly IMemoryCache _cache;

        // 新增服務,快取類實現
      services.AddScoped<ICatalogViewModelService, CachedCatalogViewModelService>();

        //新增服務,非快取的實現
        //services.AddScoped<ICatalogViewModelService, CatalogViewModelService>();

 

  2. ApplicationCore層

    ApplicationCore是領域層,是專案中最重要最複雜的一層。ApplicationCore層包含應用程式的業務邏輯,此業務邏輯包含在領域模型中。領域層知識在Equinox專案中並沒有講清楚,這裡在重點解析領域層內部成員,並結合專案來說清楚。

    下面講解領域層內部的成員職責描述定義,參考了“Microsoft.NET企業級應用架構設計 第二版”。

    領域層內部包括:領域模型和領域服務二大塊。涉及到的術語:

                     領域模型(模型)

             1)模組

                         2)領域實體(也叫"實體")

                         3)值物件

                         4)聚合

                     領域服務(也叫"服務")

                     倉儲

    下面是領域層主要的成員:

    下面是聚合與領域模型的關係。最終領域模型包含了:聚合、單個實體、值物件的結合。

 

    (1) 領域模型

      領域模型是提供業務領域的概念檢視,它由實體和值物件構成。在下圖中Entities資料夾是領域模型,可以看到包含了聚合、實體、值物件。

 

      1.1 模組

        模組是用來組織領域模型,在.net中領域模型通過命令空間組織,模組也就是名稱空間,用來組織類庫專案裡的類。比如:

        namespace Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate

        namespace Microsoft.eShopWeb.ApplicationCore.Entities.BuyerAggregate

 

      1.2 實體

        實體通常由資料和行為構成。如果要在整個生命週期的上下文裡唯一跟蹤它,這個物件就需要一個身份標識(ID主鍵),並看成實體。 如下所示是一個實體: 

    /// <summary>
    /// 領域實體都有唯一標識,這裡用ID做唯一標識
    /// </summary>
    public class BaseEntity
    {
        public int Id { get; set; }
    }

    /// <summary>
    /// 領域實體,該實體行為由Basket聚合根來操作
    /// </summary>
    public class BasketItem : BaseEntity
    {
        public decimal UnitPrice { get; set; }
        public int Quantity { get; set; }
        public int CatalogItemId { get; set; }
 }

       1.3 值物件

        值物件和實體都由.net 類構成。值物件是包含資料的類,沒有行為,可能有方法本質上是輔助方法。值物件不需要身份標識,因為它們不會改變狀態。如下所示是一個值物件

    /// <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 聚合

        在開發中單個實體總是互相引用,聚合的作用是把相關邏輯的實體組合當作一個整體對待。聚合是一致性(事務性)的邊界,對領域模型進行分組和隔離。聚合是關聯的物件(實體)群,放在一個聚合容器中,用於資料更改的目的。每個聚合通常被限制於2~3個物件。聚合根在整個領域模型都可見,而且可以直接引用。   

    /// <summary>
    /// 定義聚合根,嚴格來說這個介面不需要任務功能,它是一個普通標記介面
    /// </summary>
    public interface IAggregateRoot
    {

    }
    

    /// <summary>
    /// 建立購物車聚合根,通常實現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();   
        //...
    }

   

    在該專案中領域模型與“Microsoft.NET企業級應用架構設計第二版”書中描述的職責有不一樣地方,來看一下:

      (1) 領域服務有直接引用聚合中的實體(如:BasketItem)。書中描述是聚合中實體不能從聚合之處直接引用,應用把聚合看成一個整體。

      (2) 領域實體幾乎都是貧血模型。書中描述是領域實體應該包括行為和資料。

 

    (2) 領域服務

      領域服務類方法實現領域邏輯,不屬於特定聚合中(聚合是屬於領域模型的),很可能跨多個實體。當一塊業務邏輯無法融入任何現有聚合,而聚合又無法通過重新設計適應操作時,就需要考慮使用領域服務。下圖是領域服務資料夾:

        /// <summary>
        /// 下面是建立訂單服務,用到的實體包括了:Basket、BasketItem、OrderItem、Order跨越了多個聚合,該業務放在領域服務中完全正確。
        /// </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企業級應用架構設計第二版”書中描述的領域服務職責不完全一樣,來看一下:

      (1) 專案中,領域服務只是用來執行領域業務邏輯,包括了訂單服務OrderService和購物車服務BasketService。書中描述是可能跨多個實體。當一塊業務邏輯無法融入任何現有聚合。

       /// <summary>
        /// 新增購物車服務,沒有跨越多個聚合,應該不放在領域服務中。
        /// </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專案雖然沒有完全遵循領域層中,成員職責描述,但可以理解是在程式碼上簡化了領域層的複雜性。

 

     (3) 倉儲

      倉儲是協調領域模型和資料對映層的元件。倉儲是領域服務中最常見型別,它負責持久化。倉儲介面的實現屬於基礎設施層。倉儲通常基於一個IRepository介面。 下面看下專案定義的倉儲介面。

    /// <summary>
    /// T是領域實體,是BaseEntity型別的實體
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public interface IAsyncRepository<T> where T : BaseEntity
    {
        Task<T> GetByIdAsync(int id);
        Task<IReadOnlyList<T>> ListAllAsync();
        //使用領域規則查詢
        Task<IReadOnlyList<T>> ListAsync(ISpecification<T> spec);
        Task<T> AddAsync(T entity);
        Task UpdateAsync(T entity);
        Task DeleteAsync(T entity);
        //使用領域規則查詢
        Task<int> CountAsync(ISpecification<T> spec);
    }

 

    (4) 領域規則

      在倉儲設計查詢介面時,可能還會用到領域規則。 在倉儲中一般都是定義固定的查詢介面,如上面倉儲的IAsyncRepository所示。而複雜的查詢條件可能需要用到領域規則。在本專案中通過強大Linq 表示式樹Expression 來實現動態查詢。

    /// <summary>
    /// 領域規則介面,由BaseSpecification實現
    /// 最終由Infrastructure.Data.SpecificationEvaluator<T>類來構建完整的表達樹
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public interface ISpecification<T>
    {
        //建立一個表達樹,並通過where首個條件縮小查詢範圍。
        //實現:IQueryable<T> query = query.Where(specification.Criteria)
        Expression<Func<T, bool>> Criteria { get; }

        //基於表示式的包含
        //實現如: 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資料夾中定義的介面,都由基礎設施層來實現。如:

             IAppLogger日誌介面

             IEmailSender郵件介面

             IAsyncRepository倉儲介面

 

  3.Infrastructure層

    基礎設施層Infrastructure依賴於ApplicationCore,這遵循依賴倒置原則(DIP),Infrastructure中程式碼實現了ApplicationCore中定義的介面(Interfaces資料夾)。該層沒有太多要講的,功能主要包括:使用EF Core進行資料訪問、Identity、日誌、郵件傳送。與Equinox專案的基礎設施層差不多,區別多了領域規則。

           領域規則SpecificationEvaluator.cs類用來構建查詢表示式(Linq expression),該類返回IQueryable<T>型別。IQueryable介面並不負責查詢的實際執行,它所做的只是描述要執行的查詢。

public class EfRepository<T> : IAsyncRepository<T> where T : BaseEntity
    {
        //...這裡省略的是常規查詢,如ADDAsync、UpdateAsync、GetByIdAsync ...

        //獲取構建的查詢表示式
      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:對每個元素呼叫的累加器函式
            //返回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企業級應用架構設計 第二版

&n