.项目分析

  在上篇中介绍了什么是”干净架构”,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企业级应用架构设计 第二版