在应用程序框架实战十四:DDD分层架构之领域实体(基础篇)一文中,我介绍了领域实体的基础,包括标识、相等性比较、输出实体状态等。本文将介绍领域实体的一个核心内容——验证,它是应用程序健壮性的基石。为了完成领域实体的验证,咱们在前面已经准备好了验证公共操做类和异常公共操做类。html
.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进行验证,那么其它验证代码也能够放到控制器上。ide
[HttpPost] public ActionResult 方法名( 实体名 model ) { if ( ModelState.IsValid == false ) { //验证失败就返回
} if ( model.A > 1 ) { //验证失败就返回
} if ( model.B > 2 ) { //验证失败就返回
} //验证成功就执行后面的代码
}
观察上面代码,model.A > 1 已经将本属于领域层的验证定义规则泄露到表现层来了,由于这句代码访问了实体的属性,所谓验证规则,就是对实体属性值进行某些约束。测试
既然能够在控制器上写验证,那么就会有人在这里写业务逻辑,因此到了后面,DDD分层架构如同虚设。ui
第二,错误的验证时机可能致使验证失败。this
考虑这样的场景,若是实体中某些属性须要调用特定方法来产生结果,当提交到控制器操做时,这些属性仍是空值,因为尚未调用特定方法,因此调用ModelState.IsValid可能致使验证失败。spa
能够看出,这实际上是由于验证的时机不对,验证几乎必定要在某些操做以后来进行,好比初始化操做,固然你能够在调用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 _validation; /// <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 ) { Id = id; _validation = validation; } }
在外部经过构造方法把须要的验证器实例传进来,这样甚至不须要在Util.Domains中引用任何程序集。这看起来很诱人,但不要盲目的追求低耦合。考虑验证器的稳定性,这应该很是高,你基本不会随便换掉它,更不会动态更换它。再看构造方法,多了一个参数,这会致使实体使用起来很是困难。因此为了避免必要的扩展性牺牲易用性,并不划算。
另外一种方法是经过Validate方法的参数注入,这样可能要好些,但仍是会让方法在调用时变得难用。
应用程序框架只是给你或你的团队在小范围使用的,它不像.Net Framework或第三方框架在全球范围使用,因此你没有必要追求很是高的扩展性,若是发生变化致使你须要修改应用程序框架,你打开来改一下也不是啥大问题,由于框架和项目源码都在你的控制范围内,不见得非要达到OCP原则。固然,若是发生变化的可能性高,你仍是须要考虑下降依赖。在依赖性和易用性间取舍,必定要根据实际状况,不要盲目追求低耦合。
另外再考虑每一个实体可能须要更换不一样的验证器吗?若是须要,那就得引入工厂方法模式。因为这个验证器只是用来验证DataAnnotations特性的,因此没这必要。
那么直接在EntityBase中new一个Validation实例好很差呢?嘿嘿,这我也只能说要求过低了。一个折中的方案是使用简单静态工厂,若是须要更换验证器实现,你就把这个工厂打开来改改,其它地方不动,通常来说这已经够用。
为Util.Domains引用Util.Validations.EntLib程序集,并在Util.Domains中添加ValidationFactory类。
using Util.Validations; using Util.Validations.EntLib; 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 ) return; 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 ) return; 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 = employee; } /// <summary>
/// 员工 /// </summary>
private readonly Employee _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 System.ComponentModel.DataAnnotations; namespace Util.Domains.Tests.Samples { /// <summary>
/// 程序员老男人的工资验证规则 /// </summary>
public class OldProgrammerSalaryRule { /// <summary>
/// 初始化程序员老男人的工资验证规则 /// </summary>
/// <param name="employee">员工</param>
public OldProgrammerSalaryRule( Employee employee ) { _employee = employee; } /// <summary>
/// 员工 /// </summary>
private readonly Employee _employee; /// <summary>
/// 验证 /// </summary>
public ValidationResult Validate() { if ( _employee.Job == "程序员" && _employee.Age > 40 && _employee.Gender == "男" && _employee.Salary < 10000 ) return new ValidationResult( "程序员老男人的工资不能低于1万" ); return ValidationResult.Success; } } }
验证规则对象虽然抽出来了,可是在哪调用它呢?最好的地方就是领域实体的Validate方法,由于这样应用层将很是简单。
为了可以在领域实体的Validate方法中调用验证规则对象,须要将验证规则添加到该实体中,这能够在Employee中增长一个AddValidationRule方法。
/// <summary>
/// 员工 /// </summary>
public class Employee : EntityBase { //构造方法和属性
/// <summary>
/// 验证规则集合 /// </summary>
private List<OldProgrammerSalaryRule> _rules; /// <summary>
/// 添加验证规则 /// </summary>
/// <param name="rule">验证规则</param>
public void AddValidationRule( OldProgrammerSalaryRule rule ) { if ( rule == null ) return; _rules.Add( rule ); } /// <summary>
/// 验证 /// </summary>
public override void Validate() { base.Validate(); foreach ( var rule in _rules ) { var result = rule.Validate(); if ( result == ValidationResult.Success ) continue; throw new Warning( result.ErrorMessage ); } } }
若是另外一个领域实体须要使用验证规则,就要复制代码过去改一下,这显然是不行的,因此须要把添加验证规则抽到基类EntityBase中。为了支持这个功能,首先要为验证规则抽象出一个接口,代码以下。
using System.ComponentModel.DataAnnotations; namespace Util.Validations { /// <summary>
/// 验证规则 /// </summary>
public interface IValidationRule { /// <summary>
/// 验证 /// </summary>
ValidationResult Validate(); } }
在EntityBase中添加AddValidationRule方法,并修改Validate方法,代码以下。
/// <summary>
/// 验证规则集合 /// </summary>
private readonly List<IValidationRule> _rules; /// <summary>
/// 添加验证规则 /// </summary>
/// <param name="rule">验证规则</param>
public void AddValidationRule( IValidationRule rule ) { if ( rule == null ) return; _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 ) return; throw new Warning( result.First().ErrorMessage ); }
如今让OldProgrammerSalaryRule实现IValidationRule接口,应用层能够像下面这样调用。
employee.AddValidationRule( new OldProgrammerSalaryRule( employee ) ); employee.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 ) return; throw new Warning( results.First().ErrorMessage ); } }
为了可以更换验证处理器,须要在EntityBase中提供一个方法SetValidationHandler,代码以下。
/// <summary>
/// 验证处理器 /// </summary>
private IValidationHandler _handler; /// <summary>
/// 设置验证处理器 /// </summary>
/// <param name="handler">验证处理器</param>
public void SetValidationHandler( IValidationHandler handler ) { if ( handler == null ) return; _handler = 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 ) return; _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() ); return result; } /// <summary>
/// 验证并添加到验证结果集合 /// </summary>
/// <param name="results">验证结果集合</param>
protected virtual void Validate( ValidationResultCollection results ) { } /// <summary>
/// 处理验证结果 /// </summary>
private void HandleValidationResult( ValidationResultCollection results ) { if ( results.IsValid ) return; _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.Success; }
这样不只概念上更清晰,并且当多个地方须要对“程序员老男人”进行验证时,还能体现出更强的封装性。
因为代码较多,完整代码就不粘贴了,若有须要请自行下载。
若是你有更好的验证方法,请必定要告诉我,等我理解之后分享给你们。
.Net应用程序框架交流QQ群: 386092459,欢迎有兴趣的朋友加入讨论。
谢谢你们的持续关注,个人博客地址:http://www.cnblogs.com/xiadao521/
下载地址:http://files.cnblogs.com/xiadao521/Util.2014.11.20.1.rar