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

.項目分析

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

  

  1. web層介紹

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

    項目web表現層引用了ApplicationCore領域層和Infrastructure基礎設施層,這種引用依賴是正常的。引用Infrastructure層是爲了添加EF上下文以及Identity用戶管理。 引用ApplicationCore層是爲了應用程序服務 調用 領域服務處理領域業務。express

    在DDD架構下依賴關係重點強調的是領域層的獨立,領域層是同心圓中最核心的層,因此在eShopOnWeb項目中,ApplicationCore層並無依賴引用項目其它層。再回頭看Equinox項目,領域層也不須要依賴引用項目其它層。windows

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

    

    (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接口的健康狀態,以下圖所示app

    (2) Extensionsasync

      向現有對象添加輔助方法。該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企業級應用架構設計 第二版

相關文章
相關標籤/搜索