总结一下领域模型的验证(附代码下载)
一:什么是领域模型(Domain Model)
1,Entities
2,Value Objects
3,Relations
二:只谈验证(Validation)——三种常见的做法
1,Constructor/Method based Validation
2,Validate() Method
3,Validation Services
4,Validation Configuration
一:什么是领域模型(Domain Model)
我们可以在概念层次认为Domain Model就是在大的领域边界中的,可以用基于离散的思想来限定出的,承载“数据”和“关系”的小边界(个人给的定义,仅供参考)。定义中蕴含着这样一层意思:所谓模型乃是我们限定出的用于解决问题的承载着“数据”与“关系”的“问题边界”,也就是Model不一定跟现实中的真实物理对象一一对应,虽然大都一一对应。领域大边界由Model小边界来明确,Model小边界需要由领域大边界来给限定出问题的范围(因为宇宙是无穷的开放的,除了那个终极规律外的任何规律都是有适用范围的,不限定出范围就会寸步难行,无法认识任何问题了)。
领域模型承载的数据可以分为两个类别:Entities和Value Objects。Model中的数据依照一定“规则”模拟出一个有机的问题模型,这个“规则”约等于Relations。
1, Entity是这样的数据,在它的生命周期中需要一个标识(Identity)。在Domain Model中(注意这里的Model是在一个限定的领域中的)如果某条数据脱离了这个标识就没有了意义的话,那么这样的数据就是该Domain Model的Entities类别的数据了。比如在cnblogs的系统中,每一个BlogSite都有一个BlogTitle,但是这个BlogTitle是可以修改的,当我修改了我的BlogSite的BlogTile后我却依然能够通过“www.cnblogs.com/xuefly”访问到,这里可以说“www.cnblogs.com/xuefly”就是我的blog的标识了。如果没有这个标识存在的话,那么BlogTitle在空间中的存在也就没有了意义(当BlogTitle与BlogSite脱离后BlogTitle就变成了“字符串”,而在Blog领域中“字符串”是没有意义的)。像BlogTitle这样的数据就是属于BlogSite模型的的Entitys类别的数据,我们把这样的数据交给BlogSite来验证的话是合理的。比如当我们修改自己的BlogSite的BlogTile中包含敏感的非法字符时或者长度超过了一定限度时,修改就不会保存成功,这里的验证就该是由BlogSite本身来完成的。
2, Value Objects是这样的数据:它的存在不需要标识。存在就是要有意义,还是在cnblogs的系统中,每个BlogSite都会有博主(BlogOwner),因为我们的BlogSite是单用户的所以在BlogSite中应该会有一个BlogUser:Person类型的属性,我们假设这个属性被命名为BlogOwner。
BlogOwner是一个BlogUser类型的对象,BlogUser类型对象的存在不依赖于BlogSite的存在,也就是即使没有BlogSite的话这个被指向了BlogSite.BlogOwner的BlogUser类型的对象依然有存在的意义(是否可以这样理解,BlogUser是一个Person,而Person在Blog领域中是具有意义的)。这里的BlogOwner就可以归为BlogSite的Value Object类别的数据了。我们看到BlogSite有一个ID标识,这个ID标识是提供给BlogTitle这样的BlogSite的Entitys类别的数据的,并不是提供给BlogOwner的。再进一步说明就是:在一个BlogSite类型的对象blogSite1中,blogSite1的BlogOwner属性值原来指向user1,而现在我们把blogSite1.BlogOwner指向null,这时user1失去了与blogSite1的联系,然而user1却在整个Blog领域中依旧具有实际的意义,因为在Blog领域中必须有用户这个概念(BlogUser Class),user1是一个用户,user1在blog领域中依旧有意义。而BlogTitle就不同了(blogSite1.BlogTitle = blogTitle1),如果我们把blogSite1.BlogTitle指向null的话,BlogTitle1同样与blogSite1脱离了关系,这个时候blogTitle1由“博客标题”变成了“字符串”,blogTitle1是个字符串,它在Blog领域中没有意义。BlogOwner属于BlogSite的Value Objects类别的数据,BlogOwner的数据应交由BlogUser模型来验证,不应由BlogSite模型来验证。与BlogSite一样BlogUser.Name,BlogUser.LoginID,BlogUser.Password属于BlogUser模型的Entitys类别的数据。
3,关系(Relations)
……
二,只谈验证(Domain Model Validation)
业务规则要求我们的Domain Model必须满足某些约束,比如BlogSite的BlogTitle的长度不能大于等于255个字符等,这就是业务规则。如果说对BlogTitle的长度进行约束貌似还有点不怎么说的通的话,那么在Blog领域中业务规则要求BlogUser类型的对象的Age属性不能小于等于零就是无可厚非的了。正是这些被约束的数据组成了Domain Model,通过这些约束,低级的数据(基本数据类型)被我们组织成了更高一级的复杂类型的数据——Domain Model Class,然后领域中的所有Domain Model交织起来最终又诠释了整个领域。我们认为:没有边界的宇宙中的每一个概念都是被约束出来的,无论是“领域”还是领域中的“模型”,终极都是由规则约束出来的具有边界的问题模型。如果没有了约束就没有了Model没有了Domain,一切可以认识的东西都没有了,只剩下了一个开放的没有边界的宇宙了。可见“规则”(Rule)是多么的重要,而执行规则就需要“验证”(Validation)。
1, 基于构造的验证
将验证放在构造对象的时候,比如构造函数中或者放在属性中。在这种情况下,当验证失败的时候我们一般直接抛出异常,比如抛出自定义的ValidationException异常,将错误信息放在自定义异常中。
public class Person : Entity
{
private string _name;
private DateTime _birthday;
public string Name
{
get
{
return _name;
}
set
{
if (value.IsNullOrEmpty())
{
throw new IsNullOrEmptyException("名称不能为空");
}
_name = value;
}
}
public DateTime Birthday
{
get
{
return _birthday;
}
set
{
if (value >= DateTime.Now || DateTime.Now.AddYears(-120) > value)
{
throw new ValidationException("出生日期不在有效的范围内");
}
_birthday = value;
}
}
}
[TestFixture]
public class PersonTest
{
[TestCase]
public void TestPerson()
{
Person person = new Person { Name = "xuefly", Birthday = DateTime.Now.AddYears(-20) };
Assert.IsNotNull(person);
}
[TestCase]
[ExpectedException(typeof(IsNullOrEmptyException))]
public void NameShouldNotBeNullOrEmpty()
{
Person person = new Person { Name = "", Birthday = DateTime.Now.AddYears(-20) };
}
[TestCase]
[ExpectedException(typeof(ValidationException))]
public void BirthdayShouldNotGreatThanNow()
{
Person person = new Person { Name = "xuefly", Birthday = DateTime.Now.AddYears(1) };
}
}
2,Validate()方法验证
给每一个Domain Model实现一个“Validate()”方法。在这种情况下,我们在使用Domain Model的时候主动调用该方法来验证对象的合法性,如果验证失败直接抛出异常或者返回一个类似List<ValidationError>这样形式的集合。这样的话,约束Domain Model的“法律/规则”可以被违犯,执不执行法律的权利交给了上层的用户,比较灵活但同时上层的用户肩负起了责任。(就像google搞出来的那几个“google事件”一样,google违犯了我们的法律,确定无疑!皮球已经踢到了我们这边,处不处罚google是我们必须要决定的事情)
我们先定义一个用来封装业务规则的类叫BrokenBusinessRule:
public class BrokenBusinessRule
{
public BrokenBusinessRule(string property, string rule)
{
Property = property;
Rule = rule;
}
public string Property { get; set; }
public string Rule { get; set; }
}
public class BlogSite : Entity
{
protected bool IsValidated = false;
public string BlogTitle { get; set; }
public string DomainName { get; set; }
public BlogUser BlogOwner { get; set; }
// 该方法就是Validate() Method
public List<BrokenBusinessRule> GetBrokenRules()
{
List<BrokenBusinessRule> brokenRules;
if (IsValidated)
{
return new List<BrokenBusinessRule>();
}
// 下面的代码参考http://www.cnblogs.com/tristanguo/archive/2009/05/15/1457197.html 感谢tristanguo
brokenRules = new Validator<BlogSite>(this).Validate(b => BlogTitle.IsNullOrEmpty(), new BrokenBusinessRule("BlogTitle", "博客名称不能为空"))
.Validate(b => DomainName.IsNullOrEmpty(), new BrokenBusinessRule("DomainName", "域名不能为空"))
.BrokenRoles;
brokenRules.AddRange(BlogOwner.GetBrokenRules());
IsValidated = brokenRules.Count == 0;
return brokenRules;
}
}
public class BlogUserTest
{
[TestCase]
public void TestBlogUser()
{
BlogUser blogUser = new BlogUser { LoginID="", Name="", Birthday=DateTime.Now.AddYears(1), Password=""};
Assert.IsTrue(blogUser.GetBrokenRules().Count == 4);
}
[TestCase]
public void TestBlogUser1()
{
// Name和Birthday是在BlogUser的基类Person中定义的
BlogUser blogUser = new BlogUser { LoginID = "xuefly", Name = "", Birthday = DateTime.Now.AddYears(1), Password = "123456" };
Assert.IsTrue(blogUser.GetBrokenRules().Count == 2);
}
[TestCase]
public void TestBlogUser2()
{
// 1:LoginID为空;2:密码为空
BlogUser blogUser = new BlogUser { LoginID = "", Name = "xuefly", Birthday = DateTime.Now.AddYears(-20), Password = "" };// Name和Birthday是在BlogUser的基类Person中定义的
Assert.IsTrue(blogUser.GetBrokenRules().Count == 2);
}
}
测试也通过了。
3, 验证服务
这个类别是我通过观察Oxite2的验证机制分出来的,叫法不一定正确,下一篇书写。
欢迎加入Oxite小组一起学习:博客园Oxite小组
4, 基于配置的验证
这个我现在还不是很清楚,不知道跟我想象的是一回事不,等弄清楚了再书写吧。
代码这里下载:下载代码
参考: