什么叫.net ddd框架架

2474人阅读
Apworks 是.net基于DDD开发的开源框架,对DDD不了解的,也可以通过这个框架“最佳实践”一下同时也可以在博客园找到大牛写的教程(我就是看到他写的才知道这个框架^_^)
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:4231634次
积分:58558
积分:58558
排名:第43名
原创:1195篇
转载:573篇
评论:1092条
(1)(1)(1)(4)(2)(2)(1)(1)(2)(1)(1)(3)(3)(1)(5)(1)(2)(3)(3)(4)(6)(8)(12)(3)(7)(1)(2)(8)(5)(2)(2)(1)(2)(2)(10)(9)(9)(2)(12)(10)(3)(6)(4)(3)(4)(16)(5)(12)(12)(7)(20)(25)(24)(10)(7)(4)(2)(4)(34)(12)(2)(6)(6)(1)(2)(4)(1)(8)(28)(9)(3)(3)(3)(5)(4)(2)(5)(2)(2)(3)(2)(1)(3)(3)(3)(2)(3)(2)(16)(7)(4)(10)(3)(8)(10)(11)(47)(151)(158)(11)(12)(47)(16)(16)(87)(71)(65)(164)(89)(61)(14)(10)(12)(19)(6)(5)(18)(14)(21)(11)(31)(20)(30)(4)(2)(1)(6)  在一文中,我介绍了领域实体的基础,包括标识、相等性比较、输出实体状态等。本文将介绍领域实体的一个核心内容&&验证,它是应用程序健壮性的基石。为了完成领域实体的验证,我们在前面已经准备好了和。
  .Net提供的DataAnnotations验证方法非常强大,Mvc会自动将DataAnnotations特性转换为客户端Js验证,从而提升了用户体验。但是客户端验证是靠不住的,因为很容易绕开界面向服务端提交数据,所以服务端必须重新验证。换句话说,服务端验证才是必须的,客户端验证只是为了提升用户体验而已。
  为了在服务端能够进行验证,Mvc提供了ModelState.IsValid。
[HttpPost]
public ActionResult 方法名( 实体名 model ) {
if ( ModelState.IsValid == false ) {
//验证失败就返回,可能会添加错误消息,也可能要转换为客户端能识别的消息格式
//验证成功就执行后面的代码
  在控制器里写if ( ModelState.IsValid == false )判断有几个问题,下面进行一些讨论。
  第一,可能误导初学者,导致分层不清。
  从分层架构的角度来讲,验证属于业务层,在DDD分层架构就是领域层。观察ModelState.IsValid可以发现,这句代码并不是在定义验证规则,而是调用验证。在控制器上直接调用验证可能并不是什么问题,但初学者可能会认为,既然可以在控制器上调用ModelState.IsValid进行验证,那么其它验证代码也可以放到控制器上。
[HttpPost]
public ActionResult 方法名( 实体名 model ) {
if ( ModelState.IsValid == false ) {
//验证失败就返回
if ( model.A & 1 ) {
//验证失败就返回
if ( model.B & 2 ) {
//验证失败就返回
//验证成功就执行后面的代码
  观察上面代码,model.A & 1 已经将本属于领域层的验证定义规则泄露到表现层来了,因为这句代码访问了实体的属性,所谓验证规则,就是对实体属性值进行某些约束。
  既然可以在控制器上写验证,那么就会有人在这里写业务逻辑,所以到了后面,DDD分层架构如同虚设。
  第二,错误的验证时机可能导致验证失败。
  考虑这样的场景,如果实体中某些属性需要调用特定方法来产生结果,当提交到控制器操作时,这些属性还是空值,由于还没有调用特定方法,所以调用ModelState.IsValid可能导致验证失败。
  可以看出,这其实是因为验证的时机不对,验证几乎一定要在某些操作之后来进行,比如初始化操作,当然你可以在调用ModelState.IsValid之前调用特定方法,但这会导致分层不清的问题。
  打个比方,实体中有一个订单号,它是一个字符串类型,并且添加了[Required]特性,需要调用某个方法来创建订单号,当订单实体被提交到控制器操作时,调用ModelState.IsValid就会失败,因为订单号现在是空值。当然你可以把生成订单号的操作提前到创建订单界面之前,这样再提交过来就没问题了,在这个例子上一般是可行的,但有些操作你可能无法提前。
  第三,无法保证验证完整性,可能需要多次验证。
  很多时候,DataAnnotations无法满足我们的需求,所以我们还需要为特定业务需求写一些定制的验证代码。而ModelState.IsValid只能验证DataAnnotations特性,所以这时候验证通过意义不大,因为你需要在后面再验证一次。当然你可以通过一些手段进行扩展,让ModelState.IsValid能够验证你的特定规则,但没有多大必要,因为表现层在分层上的要点就是尽量不要写代码。
  第四,导致冗余代码。
  现在来观察每个ModelState.IsValid判断都干了些什么工作,一般都会转换成客户端的特定消息,比如某种格式的Json,然后返回给客户端显示出来。为了这样一个简单的功能,需要在大量的方法上添加这个判断吗?更好的方法是把这个判断抽象到控制器基类,由基类来进行处理,其它地方有错误抛出异常就可以了。这样可以得到一个统一的异常处理模型,并且消除了大量冗余代码。从这里也可以看出,打造你的应用程序框架,总是从这些不起眼的地方着手,反复考虑每个判断,每行代码是不是可以消灭,把尽量多的东西抽象到框架中,这样在开发过程中更多工作就会自动完成,不断提炼可以让你的工作越来越轻松。
  综上所述,在表现层进行验证并不是一个好方法,执行验证可以在应用层,而定义验证就一定要在领域层。下面开始介绍如何对领域实体进行验证支持。
  现在有一个员工实体,叫Employee,如下所示。
/// &summary&
/// &/summary&
public class Employee : EntityBase {
/// &summary&
/// &/summary&
[Required( ErrorMessage = "姓名不能为空" )]
public string Name { get; set; }
/// &summary&
/// &/summary&
[Required( ErrorMessage = "性别不能为空" )]
public string Gender { get; set; }
/// &summary&
/// &/summary&
[Range(18,50,ErrorMessage = "年龄范围为18岁到50岁")]
public int Age { get; set; }
/// &summary&
/// &/summary&
[Required(ErrorMessage = "职业不能为空")]
public string Job { get; set; }
/// &summary&
/// &/summary&
public double Salary { get; set; }
  为了简单起见,我把一些东西简化了,比如性别用枚举更好,但用了字符串类型,而年龄根据出生年月推断会更好等等。这个例子只是想说明验证的方法,所以不用考虑它的真实性。
  可以看见,在员工实体的属性上添加了一些DataAnnotations特性,这些特性保证了基本的验证。现在定义了验证规则,那么怎么执行验证呢?前面已经说了,用ModelState.IsValid虽然可以实现这个功能,但不是最优方法,所以我们要另谋出路。
  执行验证的最简单方法可能长成这样:employee.Validate(),employee是Employee的实例,Validate是Employee中的一个实例方法。
  注意,现在我们在领域实体中定义了一个方法,这可能会打破你平时的习惯和认识。多年的习惯可能让你对实体的认识就是,只有一堆属性的对象。现在要把思维转变过来,这个转变至关重要,它是你进入面向对象开发的第一步。
  想想看,你现在要进行验证,应该上哪才能找到这个能执行验证的方法呢?如果它不在实体中,那么它可能在表现层,也可能在应用层,还可能在领域服务中,当然还有可能不存在,都还没人实现呢。
  所以我们需要给业务逻辑安家,这样才能帮你统一的管理业务逻辑,并提供唯一的访问点。这个家最好的地方就是实体本身,因为属性全都在这里面,属性上执行的逻辑也全部放进来,就能实现对象级别的高内聚。当属性和逻辑发生变化时,对外的方法接口可能不变,这时候所有变化引起的影响就被限制在实体内部,这样就达到了更低的耦合。
  下面,我们来实现Validate方法。
  首先考虑,这个方法应该被定义在哪呢?是不是每个实体上都定义一个,由于验证对于绝大部分实体都是必须的功能,所以需要定义到层超类型上,即EntityBase。
  再来考虑一下Validate的方法签名。需要一个返回值吗,比如bool值,我在之前的文章已经讨论了返回bool值来指示是否验证通过不是一个好方法,所以我们现在返回void。那么方法参数呢?由于现在是直接在实体上调用,所以参数也不是必须的。
/// &summary&
/// &/summary&
public void Validate() {
  为了实现这个方法,我们必须要能够验证实体上的DataAnnotations特性,这在前面的验证公共操作类已经准备好了。我们在Util.Validations命名空间中定义了IValidation接口,并使用企业库实现了这个接口。
  考虑在EntityBase的Validate方法中该如何获得IValidation的实例呢?依赖程度最低的方法是使用构造方法注入。
/// &summary&
/// 领域实体
/// &/summary&
/// &typeparam name="TKey"&标识类型&/typeparam&
public abstract class EntityBase&TKey& {
/// &summary&
/// 验证器
/// &/summary&
private IValidation _
/// &summary&
/// &/summary&
[Required]
public TKey Id { get; private set; }
/// &summary&
/// 初始化领域实体
/// &/summary&
/// &param name="id"&标识&/param&
/// &param name="validation"&验证器&/param&
protected EntityBase( TKey id, IValidation validation ) {
_validation =
  在外部通过构造方法把需要的验证器实例传进来,这样甚至不需要在Util.Domains中引用任何程序集。这看起来很诱人,但不要盲目的追求低耦合。考虑验证器的稳定性,这应该非常高,你基本不会随便换掉它,更不会动态更换它。再看构造方法,多了一个参数,这会导致实体使用起来非常困难。所以为了不必要的扩展性牺牲易用性,并不划算。
  另一种方法是通过Validate方法的参数注入,这样可能要好些,但还是会让方法在调用时变得难用。
  应用程序框架只是给你或你的团队在小范围使用的,它不像.Net Framework或第三方框架在全球范围使用,所以你没有必要追求非常高的扩展性,如果发生变化导致你需要修改应用程序框架,你打开来改一下也不是啥大问题,因为框架和项目源码都在你的控制范围内,不见得非要达到OCP原则。当然,如果发生变化的可能性高,你还是需要考虑降低依赖。在依赖性和易用性间取舍,一定要根据实际情况,不要盲目追求低耦合。
  另外再考虑每个实体可能需要更换不同的验证器吗?如果需要,那就得引入工厂方法模式。由于这个验证器只是用来验证DataAnnotations特性的,所以没这必要。
  那么直接在EntityBase中new一个Validation实例好不好呢?嘿嘿,这我也只能说要求太低了。一个折中的方案是使用简单静态工厂,如果需要更换验证器实现,你就把这个工厂打开来改改,其它地方不动,一般来讲这已经够用。
  为Util.Domains引用Util.Validations.EntLib程序集,并在Util.Domains中添加ValidationFactory类。
using Util.V
using Util.Validations.EntL
namespace Util.Domains {
/// &summary&
/// 验证工厂
/// &/summary&
public class ValidationFactory {
/// &summary&
/// 创建验证操作
/// &/summary&
public static IValidation Create() {
return new Validation();
  在EntityBase类中添加Validate方法。
/// &summary&
/// &/summary&
public void Validate() {
var result = ValidationFactory.Create().Validate( this );
if ( result.IsValid )
throw new Warning( result.First().ErrorMessage );
  我们在Validate方法中将领域实体本身传入Validation实例中进行验证,获得验证结果以后,判断如果验证失败就抛出异常,这里的异常是我们在上一篇定义的异常公共操作类Warning,这样我们就知道是业务上发生了错误,可以把这个抛出的消息显示给客户。
  完成了上面的步骤以后,就可以进行基本的验证了。但是只能用DataAnnotations进行基本验证,很明显无法满足我们的实际需求。
  现在来假想一个验证需求,你的老板是个好人,你们的人力资源系统也是自己开发的,他要求程序员老男人的工资不能小于一万。换句话说,如果是一个程序员老男人,他的信息被保存到数据库的时候,工资不能小于一万,否则就是非法数据。程序员老男人这个词汇很明显不存在,为了加深你的印象,用它来给你演示业务概念如何被映射到系统中。
  程序员老男人包含三个条件:
职业 == 程序员
性别 == 男
  你为了验证这个需求,能使用DataAnnotations特性吗,也许你真的可以,但是大部分人都做不到,哪怕做到也异常复杂。
  为了实现这个功能,你可能在调用了Validate()方法之后,紧接着进行判断。
employee.Validate();
if ( employee.Job == "程序员" && employee.Age & 40 && employee.Gender == "男" && employee.Salary & 10000 )
throw new Warning( "程序员老男人的工资不能低于1万" );
  如果你调用Validate是在应用层,这下好了,把验证逻辑泄露到应用层去了,很快,你的分层架构就会乱成一团。
  时刻记住,只要是业务逻辑,你就一定要放到领域层。验证是业务逻辑的一个重要组成部分,这就是说,没有验证,业务逻辑可能是错的,因为进来的数据不在合法范围。
  现在把这句判断移到Employee实体,最合适的地方就是Validate方法中,但这个方法是在基类EntityBase上定义的,为了能够给基类方法添加行为,可以把EntityBase中的Validate方法设为虚方法,这样子类就可以重写了。
  基类EntityBase中的Validate方法修改如下。
/// &summary&
/// &/summary&
public virtual void Validate() {
var result = ValidationFactory.Create().Validate( this );
if ( result.IsValid )
throw new Warning( result.First().ErrorMessage );
  在Employee实体中重写Validate方法,注意必须调用base.Validate(),否则对DataAnnotations的验证将丢失。
public override void Validate() {
base.Validate();
if ( Job == "程序员" && Age & 40 && Gender == "男" && Salary & 10000 )
throw new Warning( "程序员老男人的工资不能低于1万" );
  对于应用层来讲,它并不关心具体怎么验证,它只知道调用employee.Validate()就行了。这样就把验证给封装了起来,为应用层提供了一个清晰而简单的API。
  一般说来,DataAnnotations和重写Validate方法添加自定义验证可以满足大部分领域实体的验证需求。但是,如果验证规则很多,而且很复杂,会发现重写的Validate方法很快变成一团乱麻。
  除了代码杂乱无章之外,还有一个问题是,业务概念被淹没在大量的条件判断中,比如Job == "程序员" && Age & 40 && Gender == "男" && Salary & 10000这个条件实际上代表的业务概念是程序员老男人的工资规则。
  另一个问题是,有些验证规则只在某些特定条件下进行,直接固化到实体中并不合适。
  当验证变得逐渐复杂时,就需要考虑将验证从实体中拆分出来。将一条验证规则封装到一个验证规则对象中,这就是规约模式在验证上的应用。规约的概念很简单,它是一个谓词,用来测试一个对象是否满足某些条件。规约的强大之处在于,将一堆相关的条件表达式封装起来,清晰的表达了业务概念。
  把程序员老男人的工资规则提取到一个OldProgrammerSalaryRule类中,如下所示。
/// &summary&
/// 程序员老男人的工资验证规则
/// &/summary&
public class OldProgrammerSalaryRule {
/// &summary&
/// 初始化程序员老男人的工资验证规则
/// &/summary&
/// &param name="employee"&员工&/param&
public OldProgrammerSalaryRule( Employee employee ) {
_employee =
/// &summary&
/// &/summary&
private readonly Employee _
/// &summary&
/// &/summary&
public bool Validate() {
if ( _employee.Job == "程序员" && _employee.Age & 40 && _employee.Gender == "男" && _employee.Salary & 10000 )
return false;
return true;
  上面的验证规则对象,通过构造方法接收业务实体,然后通过Validate方法进行验证,如果验证失败就返回false。
  返回bool值的一个问题是,错误描述就拿不到了。为了获得错误描述,我把返回类型从bool改成ValidationResult。
using ponentModel.DataA
namespace Util.Domains.Tests.Samples {
/// &summary&
/// 程序员老男人的工资验证规则
/// &/summary&
public class OldProgrammerSalaryRule {
/// &summary&
/// 初始化程序员老男人的工资验证规则
/// &/summary&
/// &param name="employee"&员工&/param&
public OldProgrammerSalaryRule( Employee employee ) {
_employee =
/// &summary&
/// &/summary&
private readonly Employee _
/// &summary&
/// &/summary&
public ValidationResult Validate() {
if ( _employee.Job == "程序员" && _employee.Age & 40 && _employee.Gender == "男" && _employee.Salary & 10000 )
return new ValidationResult( "程序员老男人的工资不能低于1万" );
return ValidationResult.S
  验证规则对象虽然抽出来了,但是在哪调用它呢?最好的地方就是领域实体的Validate方法,因为这样应用层将非常简单。
  为了能够在领域实体的Validate方法中调用验证规则对象,需要将验证规则添加到该实体中,这可以在Employee中增加一个AddValidationRule方法。
/// &summary&
/// &/summary&
public class Employee : EntityBase {
//构造方法和属性
/// &summary&
/// 验证规则集合
/// &/summary&
private List&OldProgrammerSalaryRule& _
/// &summary&
/// 添加验证规则
/// &/summary&
/// &param name="rule"&验证规则&/param&
public void AddValidationRule( OldProgrammerSalaryRule rule ) {
if ( rule == null )
_rules.Add( rule );
/// &summary&
/// &/summary&
public override void Validate() {
base.Validate();
foreach ( var rule in _rules ) {
var result = rule.Validate();
if ( result == ValidationResult.Success )
throw new Warning( result.ErrorMessage );
  如果另一个领域实体需要使用验证规则,就要复制代码过去改一下,这显然是不行的,所以需要把添加验证规则抽到基类EntityBase中。为了支持这个功能,首先要为验证规则抽象出一个接口,代码如下。
using ponentModel.DataA
namespace Util.Validations {
/// &summary&
/// 验证规则
/// &/summary&
public interface IValidationRule {
/// &summary&
/// &/summary&
ValidationResult Validate();
  在EntityBase中添加AddValidationRule方法,并修改Validate方法,代码如下。
/// &summary&
/// 验证规则集合
/// &/summary&
private readonly List&IValidationRule& _
/// &summary&
/// 添加验证规则
/// &/summary&
/// &param name="rule"&验证规则&/param&
public void AddValidationRule( IValidationRule rule ) {
if ( rule == null )
_rules.Add( rule );
/// &summary&
/// &/summary&
public virtual void Validate() {
var result = ValidationFactory.Create().Validate( this );
foreach ( var rule in _rules )
result.Add( rule.Validate() );
if ( result.IsValid )
throw new Warning( result.First().ErrorMessage );
  现在让OldProgrammerSalaryRule实现IValidationRule接口,应用层可以像下面这样调用。
employee.AddValidationRule( new OldProgrammerSalaryRule( employee ) );
employee.Validate();
  可以在几个地方为领域实体设置验证规则对象。
领域实体的构造方法中。
具体的领域实体重写Validate方法中。
当工厂创建领域实体(聚合)时。
领域服务或应用服务调用领域实体进行验证时。
  设置验证规则的要点是,稳定的验证规则尽量放到实体中,以方便使用。
  现在还有一个问题是,验证处理是抛出一个异常,这个异常的消息设置为验证结果集合的第一个消息。这在大部分时候都够用了,但是某些时候对错误的处理会有所不同,比如你现在要显示全部验证失败的消息,这时候将要修改框架。所以把验证的处理提取出来是个不错的方法。
  定义一个验证处理的接口IValidationHandler,这个验证处理接口有一个Handle的处理方法,接收一个验证结果集合的参数,代码如下。
/// &summary&
/// 验证处理器
/// &/summary&
public interface IValidationHandler {
/// &summary&
/// 处理验证错误
/// &/summary&
/// &param name="results"&验证结果集合&/param&
void Handle( ValidationResultCollection results );
  由于只需要在特殊情况下更换验证处理实现,所以定义一个默认的实现,代码如下。
/// &summary&
/// 默认验证处理器,直接抛出异常
/// &/summary&
public class ValidationHandler : IValidationHandler{
/// &summary&
/// 处理验证错误
/// &/summary&
/// &param name="results"&验证结果集合&/param&
public void Handle( ValidationResultCollection results ) {
if ( results.IsValid )
throw new Warning( results.First().ErrorMessage );
  为了能够更换验证处理器,需要在EntityBase中提供一个方法SetValidationHandler,代码如下。
/// &summary&
/// 验证处理器
/// &/summary&
private IValidationHandler _
/// &summary&
/// 设置验证处理器
/// &/summary&
/// &param name="handler"&验证处理器&/param&
public void SetValidationHandler( IValidationHandler handler ) {
if ( handler == null )
_handler =
  在EntityBase构造方法中初始化_handler = new ValidationHandler(),并修改Validate方法。
/// &summary&
/// &/summary&
public virtual void Validate() {
var result = ValidationFactory.Create().Validate( this );
foreach ( var rule in _rules )
result.Add( rule.Validate() );
if ( result.IsValid )
_handler.Handle( result );
  最后,用提取方法重构来改善一下Validate代码。
/// &summary&
/// &/summary&
public virtual void Validate() {
var result = GetValidationResult();
HandleValidationResult( result );
/// &summary&
/// 获取验证结果
/// &/summary&
private ValidationResultCollection GetValidationResult() {
var result = ValidationFactory.Create().Validate( this );
Validate( result );
foreach ( var rule in _rules )
result.Add( rule.Validate() );
/// &summary&
/// 验证并添加到验证结果集合
/// &/summary&
/// &param name="results"&验证结果集合&/param&
protected virtual void Validate( ValidationResultCollection results ) {
/// &summary&
/// 处理验证结果
/// &/summary&
private void HandleValidationResult( ValidationResultCollection results ) {
if ( results.IsValid )
_handler.Handle( results );
  注意,这里添加了一个Validate( ValidationResultCollection results )虚方法,这是一个钩子方法,提供它的目的是允许子类向ValidationResultCollection中添加自定义验证的结果。它和重写Validate()方法的区别是,如果重写Validate()方法,那么你将需要自己处理验证,而Validate( ValidationResultCollection results )方法将以统一的方式被handler处理。
  这样,我们就实现了验证规则定义与验证处理的分离。
  最后,再对这个小例子完善一下,可以将&程序员老男人&这个概念封装到Employee的一个方法中。
/// &summary&
/// 是否程序员老男人
/// &/summary&
public bool IsOldProgrammer() {
return Job == "程序员" && Age & 40 && Gender == "男";
  OldProgrammerSalaryRule验证规则的实现修改为如下代码。
/// &summary&
/// &/summary&
public ValidationResult Validate() {
if ( _employee.IsOldProgrammer() && _employee.Salary & 10000 )
return new ValidationResult( "程序员老男人的工资不能低于1万" );
return ValidationResult.S
  这样不仅概念上更清晰,而且当多个地方需要对&程序员老男人&进行验证时,还能体现出更强的封装性。
  由于代码较多,完整代码就不粘贴了,如有需要请自行下载。
  如果你有更好的验证方法,请一定要告诉我,等我理解以后分享给大家。
  .Net应用程序框架交流QQ群: ,欢迎有兴趣的朋友加入讨论。
  谢谢大家的持续关注,我的博客地址:
  下载地址:
阅读(...) 评论()领域驱动架构(DDD)建模中的模型到底是什么? - 知乎166被浏览21495分享邀请回答325 条评论分享收藏感谢收起2添加评论分享收藏感谢收起更多9803人阅读
java分类(8)
今天是个开心的日子,又是周末,可以安心轻松的写写文章了。经过了大概3年的DDD理论积累,以及去年年初的第一个版本的框架的开发以及项目实践经验,再通过今年上半年利用业余时间的设计与开发,我的enode框架终于可以和大家见面了。
自从Eric Evan提出DDD领域驱动设计以来已经过了很多年了,现在已经有很多人在学习或实践DDD。但是我发现目前能够支持DDD开发的框架还不多,至少在国内还不多。据我所知道的java和.net平台,国外比较有名的有:基于java平台的是,该框架很活跃,作者也很勤奋,该框架已经在一些实际商业项目中使用了,算比较成功;基于.net平台的是,该框架早起比较活跃,但现在没有发展了,因为几乎没人在维护,让人很失望;国内有:banq的可以支持DDD+CQRS+EventSourcing的开发,但是它是基于java平台的,所以对于.net平台的人,没什么实际用处;.net平台,开源的主要就是园子里的晴阳兄开发的框架。晴阳兄在DDD方面,在国内的贡献很大,写了很多DDD系列的文章,框架和案例并行,很不错。当然,我所关注的紧紧是c#和java语言的框架,基于scala等其他语言实现的框架也有很多,这里就不一一例举了。
上面这么多框架都有各自的特点和优势,这里就不多做评价了,大家有兴趣的自己去看看吧。我重点想介绍的是我的enode框架,框架的特色,以及使用的前提条件。
enode框架简介
框架名称:enode框架特色:提供一个基于DDD设计思想,实现了CQRS + EDA + Event Sourcing + In Memory这些架构模式的,支持负载均衡的,轻量级应用开发框架。开源地址:nuget包Id:enode
使用该框架前需要了解或遵守以下几个约定:
一个command只允许导致一个聚合根的修改或一个聚合根的创建,如果违反这个规则,则框架不允许;如果一个用户操作会涉及多个聚合根的修改,则需要通过saga (process manager)来实现;拥抱最终一致性,简单的说就是通过将command+domain event不断的串联来最终实现最终一致性;如果想彻底的知道enode哪里与众不同,可以看一下源代码中的,相信这个会让你明白什么是我所说的事件驱动设计;框架的核心编程思想是异步消息处理加最终一致性,所以,如果你想实现强一致性需求,那这个框架不太适合,至少目前没有提供这样的支持;框架的设计目标不是针对企业应用开发,传统企业应用一般访问量不大且要求强一致性事务;enode框架更多的是针对互联网应用,特别是为一些需要支持访问量大、高性能、可伸缩且允许最终一致性的互联网站点提供支持;看过:的人应该知道要实现一个可伸缩的互联网应用,异步编程和最终一致性是必须的;另外,因为如果数据量一大,那我们一般会把数据分开存放,这就意味着,如果你还想实现强一致性,那就要靠分布式事务。但是很不幸,分布式事务的成本代价太高。伸缩、性能和响应延迟都受到分布式事务协调成本的反面影响,随着依赖的资源数量和用户访问数量的上升,这些指标都会以几何级数恶化。可用性亦受到限制,因为所有依赖的资源都必须就位。框架定位:目前定位于单台机器上运行的单个应用内的CQRS架构前提下的command端的实现;如果要实现多台机器多个应用之间的分布式集成,则大家需要再进一步借助ESB来与更高层的SOA架构集成;
enode框架架构图:
CQRS架构图
上面的架构图是enode框架的内部实现架构。当然,上面这个架构图并不是完整的CQRS架构图,而是CQRS架构图中command端的实现架构。完整的CQRS架构图一般如下:
从上图我们可以看到,传统的CQRS架构图,一般画的都比大范围,command端具体如何实现,实现方案有很多种。而enode框架,只是其中一种实现。
enode框架的内部实现说明
首先,client会发送command给command service,command service接受到command后,会通过一个command queue router来路由该command应该放到哪个command queue,每个command queue就是一个消息队列,队列里存放command。该消息队列是本地队列,但是支持消息的持久化,也就是说command被放入队列后,就算机器挂了,下次机器重启后,消息也不会丢失。另外,command
queue我们可以根据需要配置多个,上图为了示意,只画了两个;command queue的出口端,有一个command processor,command processor的职责是处理command。但是command processor本身不直接处理command,直接处理command的是command processor内部的一些worker线程,每个worker线程会不断的从command queue中取出command,然后按照图中标出的5个步骤对command进行处理。可以看出,由于command
processor中的worker线程都是在并行工作的,所以我们可以发现,同一时刻,会有多个command在被同时处理。为什么要这样做?因为client发送command到command queue的速度很快,比如每秒发送1W个command过来,也就是并发是1W,但是command processor如果内部只有单线程在处理command,那速度跟不上这个并发量,所以我们需要设计支持多个worker同时处理command,这样延迟就会降低;我们从架构图可以看到,command processor获取聚合根是从内存缓存(如支持分布式缓存的redis)获取,性能比较高;持久化事件,用的是MongoDB,由于mongoDB性能也很高;如果觉得事件持久化到单台MongoDB
server还是有瓶颈问题,那我们可以对MongoDB server做集群,然后对事件进行sharding,将不同的event存储到不同的MongoBD Server,这样,事件的持久化也不会成为瓶颈;这样,整个command processor的处理性能理论上可以很高,当然我还没测试过集群情况下性能可以达到多少;单个mongodb server,持久化事件的性能,5K不成问题;这里有一点借此在说明下,被持久化的其实不是单个事件,而是一个事件流,即EventStream。为什么是事件流是因为单个聚合根一次可能产生不止一个领域事件,但是这些事件比如一起被持久化,所以设计思路是把这些事件设计为一个事件流,然后将这个事件流作为一条mongodb的记录插入到mongodb;事件流在mongodb中的主键是聚合根ID+事件流的版本号,通过这两个联合字段作为主键,用来实现乐观锁;假如有两个事件流都是针对同一个聚合根的,且他们的版本号相同,那插入到mongodb时,会报主键索引冲突,这就是并发冲突了。需要对command进行自动重试(enode框架会帮你自动做掉这个自动重试)来解决这个问题;command processor中的worker处理完一个command后,会把产生的事件发布给一个合适的event queue。同样,内部也会有一个event queue router来路由到底该放到哪个event queue。那么event queue中的事件接下来要被如何处理呢?也就是event processor会做身事情呢?很简单,就是分发事件给所有的事件订阅者,即dispatch event to subscribers。那这些event
subscribers都会做什么事情呢?一般是做两种处理:1)因为是采用CQRS架构,所以我们不能仅仅持久化领域事件,还要通过领域事件来更新CQRS的查询端数据库(这种为了更新查询库的事件订阅者老外一般叫做denormalizer);由于更新查询库没有必要同步,所以设计event queue;2)上面提到过,有些操作会影响多个聚合根,比如银行转账,订单处理,等。这些操作本质上是一个流程,所以我们的方案是通过在领域事件的event handler中发送command来异步的实现串联整个处理流程;当然,如何实现这个流程,还是有很多问题需要讨论。我个人觉得比较靠谱的方案是通过process
manager,类似BPM的思想,国外也有很多人把它叫做saga。对saga或process manager感兴趣的看官,可以看看微软的这个例子:,对于如何用enode来实现一个process
manager,由于信息太多,所以我接下来会写一篇文章专门系统的介绍。
回顾enode框架所使用的关键技术
基于整个enode框架的架构图以及上面的文字描述说明,我们在看一下上面最开始框架简介中提到的框架所使用的关键技术。
DDD:指架构图中的domain model,采用DDD的思想去分析设计实现,enode框架会提供实现DDD所必要的基类聚合根以及触发领域事件的支持;CQRS:指整个enode架构实现的是CQRS架构中的command端,CQRS架构的查询端,enode框架没做任何限制,我们可以随意设计;EDA:指整个编程模型的思路,都要基于事件驱动的思想,也就是领域模型的状态更改是基于响应事件的,聚合根之间的交互,也不是基于事务,而是基于事件驱动和响应;Event Sourcing:中文意思是事件溯源,关于什么是事件溯源,可以看一下文章。通过事件溯源,我们可以不用ORM来持久化聚合根,而是只要持久化领域事件即可,当我们要还原聚合根时只要对该聚合根进行一次事件溯源即可;In Memory:是指整个domain model的所有数据都存储在内存缓存中,比如分布式缓存redis中,且缓存永远不会被释放。这样当我们要获取聚合根时,只要从内存缓存拿即可,所以叫in memory;NoSQL:是指enode用到了redis,mongodb这样的nosql产品;负载均衡支持:是指,基于enode框架的应用程序,可以方便的支持负载均衡;因为应用程序本身是无状态的,in memory是存储在全局的redis分布式缓存中,独立于应用本身;而event store则是用MongoDB,同样也是全局的,且也支持集群。所以,我们可以将基于enode框架开发的应用程序部署任意多份在不同的机器,然后做负载均衡,从而让我们的应用程序支撑更高的并发访问。
框架API使用简介
框架初始化
public void Initialize()
var connectionString = &mongodb://localhost/EventDB&;
var eventCollection = &Event&;
var eventPublishInfoCollection = &EventPublishInfo&;
var eventHandleInfoCollection = &EventHandleInfo&;
var assemblies = new Assembly[] { Assembly.GetExecutingAssembly() };
Configuration
.UseTinyObjectContainer()
.UseLog4Net(&log4net.config&)
.UseDefaultCommandHandlerProvider(assemblies)
.UseDefaultAggregateRootTypeProvider(assemblies)
.UseDefaultAggregateRootInternalHandlerProvider(assemblies)
.UseDefaultEventHandlerProvider(assemblies)
//使用MongoDB来支持持久化
.UseDefaultEventCollectionNameProvider(eventCollection)
.UseDefaultQueueCollectionNameProvider()
.UseMongoMessageStore(connectionString)
.UseMongoEventStore(connectionString)
.UseMongoEventPublishInfoStore(connectionString, eventPublishInfoCollection)
.UseMongoEventHandleInfoStore(connectionString, eventHandleInfoCollection)
.UseAllDefaultProcessors(
new string[] { &CommandQueue& },
&RetryCommandQueue&,
new string[] { &EventQueue& })
command定义
[Serializable]
public class ChangeNoteTitle : Command
public Guid NoteId { get; set; }
public string Title { get; set; }
发送command到ICommandService
var commandService = ObjectContainer.Resolve&ICommandService&();
commandService.Send(new ChangeNoteTitle { NoteId = noteId, Title = &Modified Note& });
Command Handler
public class ChangeNoteTitleCommandHandler : ICommandHandler&ChangeNoteTitle&
public void Handle(ICommandContext context, ChangeNoteTitle command)
context.Get&Note&(command.NoteId).ChangeTitle(command.Title);
Domain Model
[Serializable]
public class Note : AggregateRoot&Guid&,
IEventHandler&NoteCreated&,
IEventHandler&NoteTitleChanged&
public string Title { get; private set; }
public DateTime CreatedTime { get; private set; }
public DateTime UpdatedTime { get; private set; }
public Note() : base() { }
public Note(Guid id, string title) : base(id)
var currentTime = DateTime.N
RaiseEvent(new NoteCreated(Id, title, currentTime, currentTime));
public void ChangeTitle(string title)
RaiseEvent(new NoteTitleChanged(Id, title, DateTime.Now));
void IEventHandler&NoteCreated&.Handle(NoteCreated evnt)
Title = evnt.T
CreatedTime = evnt.CreatedT
UpdatedTime = evnt.UpdatedT
void IEventHandler&NoteTitleChanged&.Handle(NoteTitleChanged evnt)
Title = evnt.T
UpdatedTime = evnt.UpdatedT
&Domain Event
[Serializable]
public class NoteTitleChanged : Event
public Guid NoteId { get; private set; }
public string Title { get; private set; }
public DateTime UpdatedTime { get; private set; }
public NoteTitleChanged(Guid noteId, string title, DateTime updatedTime)
NoteId = noteId;
UpdatedTime = updatedT
Event Handler
public class NoteEventHandler :
IEventHandler&NoteCreated&,
IEventHandler&NoteTitleChanged&
public void Handle(NoteCreated evnt)
Console.WriteLine(string.Format(&Note created, title:{0}&, evnt.Title));
public void Handle(NoteTitleChanged evnt)
Console.WriteLine(string.Format(&Note title changed, title:{0}&, evnt.Title));
后续需要讨论的关键问题
既然是消息驱动,那如何保证消息不会丢失;如何保证消息至少被执行一次,且不能被重复执行;如何确保消息没执行成功就不能丢,也就是要求消息队列支持事务;因为是多线程并行持久化事件并且是多台机器集群负载均衡部署的,那如何保证领域事件被持久化的顺序与发布到事件订阅者的顺序完全一致;整个架构中,基于redis实现的memory cache以及基于mongodb实现的eventstore,是两个关键的存储点,如何确保高吞吐量和可用性;因为事件是并行持久化的,那如果遇到并发冲突如何解决?命令的重试如何实现?消息队列中的消息的重试机制如何实现?既然抛弃了强一致性的事务概念,而用process manager来实现聚合根交互,那如何具体实现一个process manager?
目前暂时想到以上8个我觉得比较重要的问题,我会在接下来的文章中,一一讨论这些问题的解决思路。我觉得写这种介绍框架的文章,一方面要介绍框架本身,更重要的是要告诉别人你设计以及实现框架时遇到的问题以及解决思路。要把这个分析和解决的思路写出来,这才是对读者意义最大的;
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:271846次
积分:3363
积分:3363
排名:第9723名
原创:47篇
转载:88篇
评论:58条
(1)(2)(1)(2)(2)(1)(2)(3)(2)(5)(5)(5)(5)(3)(14)(10)(1)(2)(5)(20)(18)(1)(1)(2)(5)(4)(1)(1)(1)(11)

我要回帖

更多关于 ddd开发框架 的文章

 

随机推荐