DDD(域驱动设计),如何处理实体状态更改,以及封装需要处理大量数据的业务规则

public class Person { public IList SpecialBirthPlaces; public static readonly DateTime ImportantDate; public String BirthPlace {get;set;} public DateTime BirthDate { set { if (BirthPlace!=null && value < ImportantDate && SpecialBirthPlaces.Contains(BirthPlace)) { BirthPlace = DataBase.GetBirthPlaceFor(BirthPlace, value); } } } } 

这是尝试在我的域模型中封装一个简单的规则。 我试图捕获的规则是:当出于某种原因,我们更新一个人的出生日期(例如,原始用户输入中存在错误)时,我们需要检查该人的出生地并将其替换为来自其他人的其他值。数据库,如果它在我们的数据库中列为特殊的出生地。

但是,我在实现它时有两个问题:

  1. 此规则修改域实体状态(属性),我需要在用户界面中反映此更改。 我的域名模型是POCO。 我可以把这个逻辑放在ViewModel中,但这是错误的,因为它不是UI逻辑。 这是我需要捕获的一个重要的域规则。

  2. 我的SpecialBirthPlaces列表非常大,每次我从数据库中获取客户时我都不想填充它。 此外,当规则得到满足时,我需要替换Birthplace。 正如我所说,特殊的出生地和替代品的列表非常大,并存储在数据库中。

如何在DDD风格中实现我需要的逻辑?

我将这个问题封装起来的方式是修改跟踪,它采用工作单元模式。 我有我的DDD存储库与一个工作单元相关联,我可以查询我从任何存储库获得的任何实体集的工作单元,以查看哪些被修改。

至于大型集合,它似乎是一个只读集。 处理此问题的一种方法是在本地预加载和缓存它(如果它曾被访问过),然后存储库可以针对内存中的版本运行查询。 我使用NHibernate,用它来处理这种情况很容易。 如果它太大而无法存储在RAM中(如100或更多MB),您可能需要针对它进行特殊情况存储库查询,以便在数据库上执行SpecialBirthPlaces.Contains(BirthPlace)查询(可能在存储过程,哈!)。 您可能希望将SpecialBirthPlaces表达为实体的存储库,而不仅仅是一个大的字符串集合,这将允许“查询”模式使您无需加载整个事物。

在这个冗长的叙述之后,这里有一些例子:

 public class BirthPlace { public String Name { get; set; } } public class SpecialBirthPlace : BirthPlace { } public class Person { public static readonly DateTime ImportantDate; public BirthPlace BirthPlace { get; set; } public DateTime BirthDate { get; private set; } public void CorrectBirthDate(IRepository specialBirthPlaces, DateTime date) { if (BirthPlace != null && date < ImportantDate && specialBirthPlaces.Contains(BirthPlace)) { BirthPlace = specialBirthPlaces.GetForDate(date); } } } 

有一种传递更正出生日期的方法是一种更好的设计,因为它通过参数告诉您实际更正出生日期所需的内容:SpecialBirthPlace实体的存储库(即集合)和正确的日期。 这个明确的合同清楚地说明了域正在做什么,并且只需通过阅读实体合同就可以明确业务需求,其中将整个集合置于实体的状态中隐藏它。

现在我们已经将BirthPlace变成了一个实体,我们可以看到可能还有一个优化使域模型变得更平坦。 我们并不需要专门化BirthPlace但我们需要指出它是否特殊。 我们可以向对象添加一个属性(有些人不喜欢域对象的属性,但我不这样做,因为它使查询更容易,特别是使用LINQ)来指示它是否特殊。 然后我们可以完全摆脱Contains查询:

 public class BirthPlace { public BirthPlace(String name, Boolean isSpecial = false) { Name = name; IsSpecial = isSpecial } public String Name { get; private set; } public Boolean IsSpecial { get; private set; } } public class Person { public static readonly DateTime ImportantDate; public BirthPlace BirthPlace { get; set; } public DateTime BirthDate { get; private set; } public void CorrectBirthDate(IRepository birthPlaces, DateTime date) { if (BirthPlace != null && date < ImportantDate && BirthPlace.IsSpecial) { BirthPlace = birthPlaces.GetForDate(date); } } } 

我认为“我需要在用户界面中反映这一变化”和“这是我需要捕获的重要域规则”这些陈述描述了两个不同的问题。 显然,第一个需要解决; 目前尚不清楚第二个的确如此。

如果您的域模型的其他部分需要了解此处的更改,您最好查看域事件(例如, Udi Dahan的实现 )。 您也可以使用它在BirthDate设置时设置BirthPlace属性,如果它是一个可能很长的操作,甚至是异步的。

否则,让我们看一下UI问题。 首先,在我的域模型中,我将每个实体抽象为一个接口。 如果你不这样做,那么你可能需要至少使一些属性virtual 。 我还将使用一层抽象来生成/返回我的实体,例如IoC / factory / repository。 我认为这个层超出了域模型本身的范围。

现在,我们需要一种机制来通知UI域实体中属性的更改,但当然域模型本身在某种意义上是一个封闭的系统:我们不想引入新的成员或行为来满足任何需求。外界关注。

如果我们用实现INotifyPropertyChanged的实现装饰有问题的实体怎么办? 我们可以在我们的存储库中执行此操作,我们已经建立的存储库超出了域的范围,因此我们不会修改域模型本身,只使用组合来包含具有域模型外部系统所需function的实体。 重申一下,重新计算BirthPlace仍然是域模型的一个问题,而UI通知逻辑仍然是域模型之外的一个问题。

它看起来像这样:

 public class NotifyPerson : IPerson, INotifyPropertyChanged { readonly IPerson _inner; public NotifyPerson(IPerson inner) // repository puts the "true" domain entity here { _inner = inner; } public DateTime BirthDate { set { if(value == _inner.BirthDate) return; var previousBirthPlace = BirthPlace; _inner.BirthDate = value; Notify("BirthDate"); if(BirthPlace != previousBirthPlace) Notify("BirthPlace"); } } void Notify(string property) { var handler = PropertyChanged; if(handler != null) handler(this, new PropertyChangedEventArgs(property)); } } 

如果不使用接口,则只需从Personinheritance并覆盖BirthDate属性,在base而不是_inner上调用成员。

以下是示例实现。 此实现由多个层组成:域层,服务层和表示层。 服务层的目的是将域层的function公开给其他层,例如表示层或Web服务。 为此,其方法对应于可由域层处理的特定命令。 特别是我们有命令改变生日。 此外,该实现使用Udi Dahan的域事件框架版本。 这样做是为了将域实体与与更改生日相关联的业务逻辑分离。 这可以被视为利益和缺点。 缺点是您的整体业务逻辑分布在多个类中。 好处是您可以在处理域事件方面获得很大的灵活性。 此外,这种方法更具可扩展性,因为您可以将订阅者添加到执行辅助function的BirthDateChangedEvent 。 另一个好处(导致Udi实施背后的推理)是您的Person实体不再需要知道任何存储库,这些存储库似乎超出了域实体的范围。 总体而言,这种实现需要相当多的基础架构,但是如果您想要在您的域中进行大量投资,那么最初的麻烦是值得的。 另请注意,此实现假定基于ASP.NET MVC的表示层。 在有状态UI中,表示逻辑需要更改,ViewModel需要提供更改通知。

 ///  /// This is your main entity, while it may seem anemic, it is only because /// it is simplistic. ///  class Person { public string Id { get; set; } public string BirthPlace { get; set; } DateTime birthDate; public DateTime BirthDate { get { return this.birthDate; } set { if (this.birthDate != value) { this.birthDate = value; DomainEvents.Raise(new BirthDateChangedEvent(this.Id)); } } } } ///  /// Udi Dahan's implementation. ///  static class DomainEvents { public static void Raise(TEvent e) where TEvent : IDomainEvent { } } interface IDomainEvent { } ///  /// This is the interesting domain event which interested parties subscribe to /// and handle in special ways. ///  class BirthDateChangedEvent : IDomainEvent { public BirthDateChangedEvent(string personId) { this.PersonId = personId; } public string PersonId { get; private set; } } ///  /// This can be associated to a Unit of Work. ///  interface IPersonRepository { Person Get(string id); void Save(Person person); } ///  /// This can implement caching for performance. ///  interface IBirthPlaceRepository { bool IsSpecial(string brithPlace); string GetBirthPlaceFor(string birthPlace, DateTime birthDate); } interface IUnitOfWork : IDisposable { void Commit(); } static class UnitOfWork { public static IUnitOfWork Start() { return null; } } class ChangeBirthDateCommand { public string PersonId { get; set; } public DateTime BirthDate { get; set; } } ///  /// This is the application layer service which exposes the functionality of the domain /// to the presentation layer. ///  class PersonService { readonly IPersonRepository personDb; public void ChangeBirthDate(ChangeBirthDateCommand command) { // The service is a good place to initiate transactions, security checks, etc. using (var uow = UnitOfWork.Start()) { var person = this.personDb.Get(command.PersonId); if (person == null) throw new Exception(); person.BirthDate = command.BirthDate; // or business logic can be handled here instead of having a handler. uow.Commit(); } } } ///  /// This view model is part of the presentation layer. ///  class PersonViewModel { public PersonViewModel() { } public PersonViewModel(Person person) { this.BirthPlace = person.BirthPlace; this.BirthDate = person.BirthDate; } public string BirthPlace { get; set; } public DateTime BirthDate { get; set; } } ///  /// This is part of the presentation layer. ///  class PersonController { readonly PersonService personService; readonly IPersonRepository personDb; public void Show(string personId) { var person = this.personDb.Get(personId); var viewModel = new PersonViewModel(person); // UI framework code here. } public void HandleChangeBirthDate(string personId, DateTime birthDate) { this.personService.ChangeBirthDate(new ChangeBirthDateCommand { PersonId = personId, BirthDate = birthDate }); Show(personId); } } interface IHandle where TEvent : IDomainEvent { void Handle(TEvent e); } ///  /// This handler contains the business logic associated with changing birthdates. This logic may change /// and may depend on other factors. ///  class BirthDateChangedBirthPlaceHandler : IHandle { readonly IPersonRepository personDb; readonly IBirthPlaceRepository birthPlaceDb; readonly DateTime importantDate; public void Handle(BirthDateChangedEvent e) { var person = this.personDb.Get(e.PersonId); if (person == null) throw new Exception(); if (person.BirthPlace != null && person.BirthDate < this.importantDate) { if (this.birthPlaceDb.IsSpecial(person.BirthPlace)) { person.BirthPlace = this.birthPlaceDb.GetBirthPlaceFor(person.BirthPlace, person.BirthDate); this.personDb.Save(person); } } } } 

IMO最佳方法是在数据库中创建存储过程,并在属性更改事件上标记实体,以便在将更改提交到db(SaveChanges()调用)时调用它。 在这种情况下, ObjectContext.ExecuteFunction是你的朋友。

将所有出生地查找和更新的逻辑放在该sproc中。 确保事务中包含sproc – 以便在更新失败时回滚更改。

编辑:抱歉没有DDD相关的答案。