欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

asp.net core系列 64 结合eShopOnWeb全面认识领域模型架构

程序员文章站 2022-07-11 07:52:47
一.项目分析 在上篇中介绍了什么是"干净架构",DDD符合了这种干净架构的特点,重点描述了DDD架构遵循的依赖倒置原则,使软件达到了低藕合。eShopOnWeb项目是学习DDD领域模型架构的一个很好案例,本篇继续分析该项目各层的职责功能,主要掌握ApplicationCore领域层内部的术语、成员职 ......

.项目分析

  在上篇中介绍了什么是"干净架构",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,结构目录如下所示:

asp.net core系列 64 结合eShopOnWeb全面认识领域模型架构

    

    (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接口的健康状态,如下图所示

asp.net core系列 64 结合eShopOnWeb全面认识领域模型架构

    (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)聚合

                     领域服务(也叫"服务")

                     仓储

    下面是领域层主要的成员:

asp.net core系列 64 结合eShopOnWeb全面认识领域模型架构

    下面是聚合与领域模型的关系。最终领域模型包含了:聚合、单个实体、值对象的结合。

asp.net core系列 64 结合eShopOnWeb全面认识领域模型架构

 

    (1) 领域模型

      领域模型是提供业务领域的概念视图,它由实体和值对象构成。在下图中entities文件夹是领域模型,可以看到包含了聚合、实体、值对象

asp.net core系列 64 结合eShopOnWeb全面认识领域模型架构

 

      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) 领域服务

      领域服务类方法实现领域逻辑,不属于特定聚合中(聚合是属于领域模型的),很可能跨多个实体。当一块业务逻辑无法融入任何现有聚合,而聚合又无法通过重新设计适应操作时,就需要考虑使用领域服务。下图是领域服务文件夹:

asp.net core系列 64 结合eShopOnWeb全面认识领域模型架构

        /// <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接口。 下面看下项目定义的仓储接口。

asp.net core系列 64 结合eShopOnWeb全面认识领域模型架构

    /// <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 来实现动态查询。

asp.net core系列 64 结合eShopOnWeb全面认识领域模型架构

    /// <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项目的基础设施层差不多,区别多了领域规则。

asp.net core系列 64 结合eShopOnWeb全面认识领域模型架构

           领域规则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企业级应用架构设计 第二版