背景
见过很多框架(包括我自己的)都会在实体的定义中包含一个IsValid()方法,用来判断实体的合法性,是否应该这样设计呢?本文就这个问题介绍一点想法,希望大家多批评。
实体能否处于“非法”状态?
实体是否应该包含IsValid()方法的深层次问题是:“实体能否处于非法状态?”。我们来定义一些术语,接下来我就引用这些术语:
- A模式:实体允许处于非法状态,但是实体要包含一个IsValid()方法进行校验。
- B模式:实体不允许处于非法状态,业务逻辑必须保证这一点。
关于A模式我不想多说了,A模式本身没有问题的,今天重点说说如何实现B模式。
如何实现B模式?
最好的说明就是写一个例子,下面是我们例子的需求:
- xxx属性不能为空。
- xxx属性必须唯一。
这个例子非常简单,也具有代表性,可以进一步抽象为:
- xxx属性不能为空,聚合自身的验证。
- xxx属性必须唯一,跨聚合验证。
让我们一个一个来。
xxx属性不能为空,聚合自身的验证。
聚合本身应该负责自己状态的完整性,反射可能会绕过这些验证,使用类似AutoMapper的工具需要注意(我已经处理了)。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 using Happy.Domain; 8 using Happy.Domain.Tree; 9 using Happy.Example.Events.TestGrids; 10 11 using Happy.Infrastructure; 12 13 namespace Happy.Example.Domain.TestGrids 14 { 15 public partial class TestGrid : AggregateRoot<Guid> 16 { 17 public System.Int64? BigIntField { get; set; } 18 public System.Boolean? BitField { get; set; } 19 public System.DateTime? DateField { get; set; } 20 public System.DateTime? DateTimeField { get; set; } 21 public System.Decimal? DecimalField { get; set; } 22 public System.Double? FloatField { get; set; } 23 public System.Int32? IntField { get; set; } 24 public System.Decimal? MoneyField { get; set; } 25 public System.Decimal? NumericField { get; set; } 26 public System.String NVarcharField { get; private set; } 27 public System.Single? RealField { get; set; } 28 public System.TimeSpan? TimeField { get; set; } 29 public System.Byte[] TimestampField { get; set; } 30 31 public void SetNVarcharField(string value) 32 { 33 value.MustNotNullAndNotWhiteSpace(value); 34 35 this.NVarcharField = value; 36 } 37 38 internal void PublishCreatedEvent() 39 { 40 this.PublishEvent(new TestGridCreatedEvent()); 41 } 42 } 43 }
xxx属性必须唯一,跨聚合验证。
仓储负责判断唯一性,应用服务负责验证,注意:是先验证,然后修改的实体。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 using Happy.Command; 8 using Happy.Application; 9 using Happy.Example.Domain.TestGrids; 10 using Happy.Example.Commands.TestGrids; 11 12 namespace Happy.Example.Application.TestGrids 13 { 14 public class TestGridCommandHandler : ApplicationService, 15 ICommandHandler<CreateTestGridComamnd>, 16 ICommandHandler<UpdateTestGridComamnd>, 17 ICommandHandler<DeleteTestGridComamnd> 18 { 19 public void Handle(CreateTestGridComamnd command) 20 { 21 var testGridService = this.Service<TestGridService>(); 22 23 testGridService.CheckNVarcharFieldUnique(command.NVarcharField); 24 var testGrid = command.CreateTestGrid(); 25 26 testGridService.Create(testGrid); 27 command.Result = testGrid.Id; 28 } 29 30 public void Handle(UpdateTestGridComamnd command) 31 { 32 var testGridService = this.Service<TestGridService>(); 33 34 var testGrid = testGridService.LoadAndDetach(command.Id); 35 if (testGrid.NVarcharField != command.NVarcharField) 36 { 37 testGridService.CheckNVarcharFieldUnique(command.NVarcharField); 38 } 39 command.UpdateTestGrid(testGrid); 40 41 testGridService.Update(testGrid); 42 } 43 44 public void Handle(DeleteTestGridComamnd command) 45 { 46 this.Service<TestGridService>().Delete(command.Id); 47 } 48 } 49 }
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 using AutoMapper; 8 9 using Happy.Infrastructure; 10 using Happy.Infrastructure.AutoMapper; 11 using Happy.Command; 12 using Happy.Example.Domain.TestGrids; 13 14 namespace Happy.Example.Commands.TestGrids 15 { 16 public class UpdateTestGridComamnd : ICommand, IHasIdProperty<Guid> 17 { 18 public Guid Id { get; set; } 19 public System.Int64? BigIntField { get; set; } 20 public System.Boolean? BitField { get; set; } 21 public System.DateTime? DateField { get; set; } 22 public System.DateTime? DateTimeField { get; set; } 23 public System.Decimal? DecimalField { get; set; } 24 public System.Double? FloatField { get; set; } 25 public System.Int32? IntField { get; set; } 26 public System.Decimal? MoneyField { get; set; } 27 public System.Decimal? NumericField { get; set; } 28 public System.String NVarcharField { get; set; } 29 public System.Single? RealField { get; set; } 30 public System.TimeSpan? TimeField { get; set; } 31 public System.Byte[] TimestampField { get; set; } 32 public byte[] OptimisticKey { get; set; } 33 34 internal void UpdateTestGrid(TestGrid testGrid) 35 { 36 Mapper.Map(this, testGrid); 37 testGrid.SetNVarcharField(this.NVarcharField); 38 } 39 40 static UpdateTestGridComamnd() 41 { 42 var map = Mapper.CreateMap<UpdateTestGridComamnd, TestGrid>(); 43 map.ForMember(x => x.Id, m => m.Ignore()); 44 map.IgnoreNotPublicSetter(); 45 } 46 } 47 }
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 using Happy.Example.Domain.TestGrids; 8 9 namespace Happy.Example.Application.TestGrids 10 { 11 public partial class TestGridService 12 { 13 protected override void AfterCreate(TestGrid aggregate) 14 { 15 base.AfterCreate(aggregate); 16 17 aggregate.PublishCreatedEvent(); 18 } 19 20 internal void CheckNVarcharFieldUnique(string value) 21 { 22 if (!this.Repository.IsNVarcharFieldExist(value)) 23 { 24 throw new InvalidOperationException("NVarcharField必须唯一"); 25 } 26 } 27 } 28 }
备注
一些好的资源:
- http://msdn.microsoft.com/en-us/library/ff664356(v=pandp.50).aspx
- http://gorodinski.com/blog/2012/05/19/validation-in-domain-driven-design-ddd/
- http://lostechies.com/jimmybogard/2009/02/15/validation-in-a-ddd-world/
- http://abdullin.com/journal/2009/1/29/ddd-and-rule-driven-ui-validation-in-net.html
- http://www.iteye.com/topic/413815
- http://devlicio.us/blogs/billy_mccafferty/archive/2009/02/17/a-response-to-validation-in-a-ddd-world.aspx