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企业级应用架构设计 第二版
上一篇: 刷卡可以吗?
下一篇: 微软官方的.net系列文档