领域驱动设计实战案例(一):搭建支持DDD的轻量级框架
DDD实战进阶第一波:开发一般业务的大健康行业直销系统(概述)
近年来,关于如何开发基于业务的软件系统与产品一直是软件行业的一个重要内容。对于架构师与软件开发人员来说,开发此类系统头痛的问题大概是以下几个方面:
1.如何将需求准确的转为软件的设计?
2.系统的架构与代码如何有效的体现我们的设计?
3.如何将领域逻辑与技术分离?
4.如何能够让团队人员的开发能够专注与业务,而不是技术本身?
5.如何交付高质量的软件,如何在出现问题时能够快速定位到代码?
6.如何快速响应需求的变更?
7.如何能够有一个框架或思想限定,让开发人员遵循一个约束,有节奏感的开发?
为了解决以上问题,软件行业提出了一个成熟的思想(或叫方法论):领域驱动设计(DDD)。通过DDD,我们能够很好的对需求应对到设计,能够让开发聚焦业务本身,能够让代码体现我们设计,能够让团队在一个框架内有节奏的开发。
有些开发人员或架构师也许了解过DDD,但总时认为很难落地,本系列文章就是通过一个大健康行业的直销系统实战案例,让大家了解如何能够基于DDD的思想和框架,开发一个业务系统。
本系列文章将达到以下几个目的:
1.熟悉DDD基本概念。
2.基于DDD基本概念构建一个轻量级的框架。
3.基于DDD设计与开发一个实际的直销系统的后端。
4.能够举一反三,开发其他的业务系统或产品。
5.本系列文章不涉及高性能、大并发的系统开发。
本系列文章需要你具备的技术基础:
1.熟悉C#。
2.熟悉http://Asp.net 或http://Asp.net Core。
3.熟悉EF或EF Core。
4.有一定的系统开发经验。
本系列文章大体的内容安排:
1.直销系统概述。
2.DDD基本概念、开发支持DDD基本概念的轻量级框架。
3.开发直销系统的产品上下文。
4.开发直销系统的经销商上下文。
5.开发直销系统的订单上下文。
6.简单了解对前端的其他支持、前端的开发。
直销系统需求(裁剪后,便于大家理解DDD应用即可)概述:
一.产品管理
1.产品上架,产品分为主产品与多个子产品规格。
2.子产品用于经销商购买。
3.子产品有价格与相应的PV(PV用于经销商购买产品后累加,用于核算经销商奖金)。
二.经销商发展
1.子经销商由上级经销商介绍并注册。
2.根据注册时的电子币确定子经销商的会员级别。
3.被注册的子经销商属于一个介绍层级。
4.一个经销商最多介绍两个子经销商。
三.产品下单
1.经销商通过电子币购买产品。
2.确定邮寄地址。
3.其他功能与普通电商类似。
搭建支持DDD的轻量级框架
要实现软件设计、软件开发在一个统一的思想、统一的节奏下进行,就应该有一个轻量级的框架对开发过程与代码编写做一定的约束。虽然DDD是一个软件开发的方法,而不是具体的技术或框架,但拥有一个轻量级的框架仍然是必要的,为了开发一个支持DDD的框架,首先需要理解DDD的基本概念和核心的组件。
一.什么是领域驱动设计(DDD)
首先要知道DDD是一种开发理念,核心是维护一个反应领域概念的模型(领域模型是软件最核心的部分,反应了软件的业务本质),然后通过大量模式来指导模型设计与开发。
DDD的一般过程是:首先通过软件需求规格说明书或原型生成一个领域模型(类、类的属性、类与类之间的关系);然后根据模式(应该如何分层?、领域逻辑写在哪?与持久化如何交互?如何协调多对象领域逻辑?如何实现逻辑与数据存储解耦等)指导来实现代码模型。
二.为什么使用DDD
DDD能应对复杂性与快速变化:
1.从技术维度实现分层:能够在每层关注自己的事情,比如领域层关注业务逻辑的事情,仓储关注持久化数据的事情,应用服务层关注用例的事情,接口层关注暴露给前端的事情。
2.业务维度:通过将大系统划分层多个上下文,可以让不同团队和不同人只关注当前上下文的开发。
3.时间维度:通过敏捷式迭代快速验证,快速修正。
三.DDD核心组件
1.界限上下文:
首先要将大系统划分层多个界限上下文,比如大健康行业直销系统可以划分为产品、经销商、订单等几个界限上下文,每个界限上下文有自己的领域逻辑、数据持久化、用例、接口等。每个界限上下文根据特点,具体实现方式又不同,比如有些界限上下文基本没有业务逻辑,就是增删改查,则可以使用CRUD最简单的模式;有些界限上线文有一定的业务逻辑,但对高并发、高性能没要求,则可以使用经典DDD模式;有些界限上下文有一定的业务逻辑,而且有高性能要求,则可以使CQRS模式。
2.实体:
有业务生命周期,采用业务标识符进行跟踪。比如一个订单就是实体,订单有生命周期的,而且有一个订单号唯一的标识它自己,如果两个订单所有属性值全部相同,但订单号不同,也是不同的实体。
3.值对象:
无业务生命周期,无业务标识符,通常用于模式实体。比如订单的收货地址、订单支付的金额等就是值对象。
4.服务:
无状态,有行为,通常就是一个用例来协调多个领域逻辑完成功能。
5.聚合:
通常将多个实体和值对象组合到一个聚合中来表达一个完整的概念,比如订单实体、订单明细实体、订单金额值对象就代表一个完整的订单概念,而且生命周期是相同的,并且需要统一持久化到数据库中。
6.聚合根:
将聚合中表达总概念的实体做成聚合根,比如订单实体就是聚合根,对聚合中所有实体的状态变更必须经过聚合根,因为聚合根协调了整个聚合的逻辑,保证一致性。当然其他实体可以被外部直接临时查询调用。
7.服务:
协调聚合之间的业务逻辑,并且完成用例。
8.仓储:
用于对聚合进行持久化,通常为每个聚合根配备一个仓储即可。仓储能够很好的解耦领域逻辑与数据库。
9.工厂:
用于创建复杂的领域对象,能够将领域对象复杂的创建过程保护起来。
了解了DDD的好处与基本的核心组件后,我们先不急着进入支持DDD思想的轻量级框架开发,也不急于直销系统需求分析和具体代码实现,我们还少一块,那就是经典DDD的架构,只有了解了经典DDD的架构,你才能知道具体在哪层要实现哪些功能,编写哪些代码,具体在开发DDD的轻量级框架与具体模块代码实现时,才能做到有的放矢。
在这里需要说明的是,我们的大健康行业直销系统有一定的业务复杂性,没有高并发、高性能的需求,所以无论是经销商上下文、产品上下文还是订单上下文的具体实现,我们都将遵循经典DDD架构,而不是CRUD简单方式或CQRS DDD架构的方式。
传统三层架构以及问题:
问题:
1.过分注重数据访问层,而不重视领域。
2.业务逻辑直接与数据访问层耦合,与领域为核心的DDD思想背道而驰。
3.没有一系列的模式与方法论指导这种分层架构的开发约束。
经典DDD架构:
1.基础结构层:整个产品或系统的底层支撑
a.常用工具、支撑功能:这个.net core项目至少要实现以下的功能:Json配置文件的读取、WebApi返回给前端的基本格式对象的定义、Json序列化与反序列化、加密功能、依赖注入框架的二次封装等。
b.支持DDD框架:这个.net core 项目至少要实现以下的功能:聚合根接口定义、实体接口定义、值对象接口定义、仓储接口定义、仓储接口的EF Core顶层实现(工作单元模式)。
c.聚合根仓储实现:这个.net core项目严格来讲其实不属于基础结构层部分,只是由于习惯,把它放到基础结构层这个解决方案文件夹中。它其实是引用了领域层的领域对象,并且 从领域层对应的聚合根仓储接口中继承,然后实现领域对象持久化到数据库,这样,仓储实现是依赖衣领对象,领域对象与领域逻辑就不需要依赖仓储。领域模型才是系统真正的核心。
2.领域层:界限上下文的领域逻辑
a.首先要实现这个界限上下文的领域对象的POCO模型。
b.然后针对这个界限上下文的所有领域对象,建立每个领域对象自己的业务逻辑,注意的是,领域对象的业务逻辑最好不与仓储直接发生交互,就算领域逻辑要临时查询数据库也不要这样。
c.定义该界限上下文聚合根的仓储接口,这个接口代表的是聚合根与持久化打交道的基础约束,具体实现还是在基础结构层的聚合根仓储中实现,这样就实现了解耦。把聚合根仓储接口定义在领域层的意义是可以为领域层的调用方-应用服务层的用例提供对聚合持久化支持。
d.定义该界限上下文的EF Core上下文接口并实现,这样就通过映射关系,EF Core就可以处理领域对象与数据库表之间的映射了。
3.应用服务层:界限上下文的用例
a.某个上下文的应用服务层的某个用例,通过调用领域对象的领域逻辑,完成相关领域逻辑的实现。
b.领域逻辑完成后,应用服务层用例调用领域层的聚合根的仓储接口的方法,完成领域对象的预持久化。(应用服务通过基础结构层的依赖注入框架与Json配置文件找到聚合根仓储接口对应的实现)
c.应用服务层用例然后调用基础结构层的EF Core仓储接口的工作单元方式,完成真正的持久化。(应用服务通过基础接口层的依赖注入框架与Json配置文件找到顶层仓储接口对应的工作单元实现)
d.用例返回给接口层需要的前端所需的json对象格式。
4.接口层:非常薄的一层
a.只需要调用应用服务层用例
b.向前端返回所需的json对象格式
从上述架构特点可以看出,聚合根的仓储与领域逻辑完全解耦,是通过应用服务层的用例将他们协调起来完成功能。
我们讲了经典DDD架构对比传统三层架构的优势,以及经典DDD架构每一层的职责后,下面将介绍基础结构层中支持DDD的轻量级框架的主要代码。
这里需要说明的是,DDD轻量级框架能够体现DDD的思想即可,没必要做得很重,你也可以根据理解,自己实现支持DDD的框架。
1.实体、聚合根与值对象的顶层体现
实体顶层定义:
public interface IEntity
{ string Code { get; set; } Guid Id { get; set; } }
Id是一个未来存储到数据库表中的技术主键,Code是领域对象的唯一业务标识符。你也可以扩展这个接口,定义两个实体比较接口(未来实现就是比较两个实体如果Code一致,则代表两个实体相等)。
聚合根顶层定义:
public interface IAggregationRoot:IEntity
{ }
聚合根接口就是从实体接口继承,只是未来的用法可以在仓储中定义持久化时的领域对象必须从这个接口或继承了这个接口的抽象类继承下来的。
值对象顶层定义:
public interface IValueObject
{ Guid Id { get; set; } }
值对象接口只需要保留一个技术主键即可,它没有业务标识符。在数据库中,值对象可能作为单独表存储,也可以作为实体的一部分存储。你也可以扩展这个接口,定义两个值对象比较接口(未来实现就是比较两个值对象如果所有属性值一致,则代表两个值对象相等)。
工作单元顶层定义:
public interface IUnitOfWork
{ void Commit(); }
工作单元接口就定义了一个提交方法,在具体实现时,其实就是对应的EF Core的整个聚合的事务提交方法。
仓储接口顶层定义:
public interface IRepository:IUnitOfWork,IDisposable
{ }
仓储接口从工作单元接口与资源释放接口继承,为未来的数据访问框架和可替换性提供顶层约束。
EF Core顶层仓储持久化实现:
public class EFCoreRepository : IRepository
{ private readonly DbContext context; public EFCoreRepository(DbContext context) { this.context = context; } public void Commit() { try { context.SaveChanges(); } catch(Exception error) { throw error; } } public void Dispose() { context.Dispose(); } }
从上述代码中可以看到,主要实现了仓储接口的Commit方法,其实就是使用了EF Core的DbContext数据访问上下文类的SaveChanges()事务提交方法,应用服务层的用例就可以获取到某个聚合根的当前状态,然后调用仓储接口的Commit方法,实现了整个聚合所有对象的一次性事务提交。
2.常用工具类的实现
我们还应该定义另一个项目,这个项目是整个系统都需要使用到的工具,其中至少应该包括http://Asp.net Core Json配置文件的读,Json序列化与反序列化,加密,依赖注入,返回给前端的对象格式定义等,这里先列出几个需要的,其他的在后面具体案例中在补充。
http://Asp.net Core Json配置文件读取:
Json配置文件会存储我们的一些配置信息,比如数据库连接字符串,微信AppId与AppSecure等,所以需要有功能支持Json配置文件的Key到Value的读取
public class AppSetting
{ private static IConfigurationSection appsections = null; public static void SetAppSetting(IConfigurationSection section) { appsections = section; } public static string GetAppSetting(string key) { string str = ""; if (appsections.GetSection(key) != null) { str = appsections.GetSection(key).Value; } return str; } }
返回前端的对象格式定义:
我们的应用服务层将返回WebApi接口一定的数据格式,WebApi接口也会将这个数据返回给前端,前端拿到后就会做相应的处理。
public class ResultEntity<T>
{ public bool IsSuccess { get; set; } public string Msg { get; set; } public T Data { get; set; } public int ErrorCode { get; set; } public int Count { get; set; } }
public class BaseAppSrv
{ protected ResultEntity<T> GetResultEntity<T>(T vobj,string msg="未成功获取到对象",int errorcode = 0) { var ueresult = new ResultEntity<T>(); var issuccess = true; if(vobj is int && Convert.ToInt32(vobj) <= 0) { issuccess = false; } else if(vobj is bool && !Convert.ToBoolean(vobj)) { issuccess = false; } else if(vobj is string && string.IsNullOrEmpty(Convert.ToString(vobj))) { issuccess = false; } if (!issuccess) { ueresult.Msg = msg; ueresult.ErrorCode = 200; } ueresult.IsSuccess = issuccess; ueresult.Data = vobj; return ueresult; } }
未来所有的用例都将从BaseAppSrv继承,最终返回的格式都是ResultEntity<T>。
好了,基本的框架搭建好了,下一章就可以直接进入案例,看案例中如何通过DDD思想进行设计,并通过经典DDD架构与DDD轻量级框架进行实际业务系统的代码编写。