C# DDD(域驱动设计),如何处理实体状态更改,以及封装需要处理大量数据的业务规则 公共类人物 { 公共IList特殊出生地; 公共静态只读日期时间重要日期; 公共字符串出生地{get;set;} 公共日期时间出生日期 { 设置 { 如果(出生地!=null&& 值
这是在我的域模型中封装一个简单规则的尝试。我试图捕捉的规则是:当出于某种原因,我们更新一个人的出生日期(例如,原始用户输入中有错误)时,我们需要检查该人的出生地,并用数据库中的其他值替换它,如果它在我们的数据库中被列为特殊出生地 但是,我在实现它时有两个问题:C# DDD(域驱动设计),如何处理实体状态更改,以及封装需要处理大量数据的业务规则 公共类人物 { 公共IList特殊出生地; 公共静态只读日期时间重要日期; 公共字符串出生地{get;set;} 公共日期时间出生日期 { 设置 { 如果(出生地!=null&& 值,c#,domain-driven-design,C#,Domain Driven Design,这是在我的域模型中封装一个简单规则的尝试。我试图捕捉的规则是:当出于某种原因,我们更新一个人的出生日期(例如,原始用户输入中有错误)时,我们需要检查该人的出生地,并用数据库中的其他值替换它,如果它在我们的数据库中被列为特殊出生地 但是,我在实现它时有两个问题: 此规则修改域实体状态(属性),我需要在用户界面中反映此更改。我的领域模型是POCO。我可以将此逻辑放在ViewModel中,但这是错误的,因为它不是UI逻辑。这是一个重要的域规则,我需要捕获它 我的特殊出生地列表相当大,我不想每次从数据库
如何在DDD样式中实现我需要的逻辑?在性能方面,最好的方法是在数据库中创建一个存储过程,并在属性更改事件上标记entity,以便在将更改提交到数据库时调用它(SaveChanges()调用)
ObjectContext.ExecuteFunction在这种情况下是您的朋友
将所有出生地查找和更新的逻辑都放在该存储过程中。确保事务中包含存储过程-以便在更新失败时回滚更改
编辑:
很抱歉,没有给出与DDD相关的答案。我封装这个问题的方式,即修改跟踪,是使用工作单元模式。我的DDD存储库与一个工作单元相关联,我可以查询工作单元中我从任何存储库中获得的任何一组实体,以查看哪些被修改
至于大型集合,它似乎是一个只读集合。处理此问题的一种方法是在本地预加载和缓存(如果曾经访问过),然后存储库可以对内存中的版本运行查询。我使用NHibernate,它很容易处理这个案件。如果它太大而无法存储在RAM中(比如100MB或更大),您可能需要对它进行特殊情况存储库查询,以便在数据库上执行SpecialBirthPlaces.Contains(诞生地)
查询(可能在存储过程中,哈!)。您可能希望将SpecialBirthPlaces
表示为实体的存储库,而不仅仅是一个庞大的字符串集合,这将允许“查询”模式使您无需加载整个内容
在这个冗长的叙述之后,下面是一些例子:
public class Person
{
public IList<String> 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);
}
}
}
}
我认为“我需要在用户界面中反映这一变化”和“这是我需要捕获的一个重要领域规则”描述了两个不同的问题。显然,第一个问题需要解决;不清楚第二个是否有
如果域模型的其他部分需要了解此处的更改,那么最好查看域事件(例如)。您还可以在设置出生日期时使用此设置出生地属性,即使这是一个可能很长的操作,也可以异步设置
否则,让我们看看UI问题。首先,在我的域模型中,我将把每个实体抽象为一个接口。如果没有,那么您可能至少需要将一些属性设置为虚拟属性。我还将使用一个抽象层来生成/返回我的实体,如IoC/factory/repository。我认为这个层超出域模型本身的界限。
现在,我们需要一种机制来通知UI域实体中属性的更改,但当然域模型本身在某种意义上是一个封闭的系统:我们不想引入新成员或行为来满足任何外部关注的需要
如果我们用一个实现了INotifyPropertyChanged
的实现来装饰有问题的实体,该怎么办?我们可以在我们的存储库中这样做,我们已经建立的存储库在域的边界之外,因此我们不会修改域模型本身,只使用组合将实体与域模型之外的系统所需的功能包装在一起。重申一下,出生地的重新计算仍然是域模型的关注点,而UI通知逻辑仍然是域模型之外的关注点
它看起来像这样:
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<BirthPlace> birthPlaces, DateTime date)
{
if (BirthPlace != null && date < ImportantDate && BirthPlace.IsSpecial)
{
BirthPlace = birthPlaces.GetForDate(date);
}
}
}
如果不使用接口,您只需从Person
继承并重写BirthDate
属性,在base
上调用成员,而不是\u inner
以下是一个示例实现。此实现由几个层组成:域层、服务层和表示层。服务层的目的是将域层的功能公开给其他层,例如表示层或web服务。为此,其方法对应于域层可以处理的特定命令。特别是我们有更改生日的命令。此外,此实现使用Udi Dahan版本的。这样做是为了将域实体与与更改生日相关的业务逻辑解耦。这
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<BirthPlace> birthPlaces, DateTime date)
{
if (BirthPlace != null && date < ImportantDate && BirthPlace.IsSpecial)
{
BirthPlace = birthPlaces.GetForDate(date);
}
}
}
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));
}
}
/// <summary>
/// This is your main entity, while it may seem anemic, it is only because
/// it is simplistic.
/// </summary>
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));
}
}
}
}
/// <summary>
/// Udi Dahan's implementation.
/// </summary>
static class DomainEvents
{
public static void Raise<TEvent>(TEvent e) where TEvent : IDomainEvent
{
}
}
interface IDomainEvent { }
/// <summary>
/// This is the interesting domain event which interested parties subscribe to
/// and handle in special ways.
/// </summary>
class BirthDateChangedEvent : IDomainEvent
{
public BirthDateChangedEvent(string personId)
{
this.PersonId = personId;
}
public string PersonId { get; private set; }
}
/// <summary>
/// This can be associated to a Unit of Work.
/// </summary>
interface IPersonRepository
{
Person Get(string id);
void Save(Person person);
}
/// <summary>
/// This can implement caching for performance.
/// </summary>
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; }
}
/// <summary>
/// This is the application layer service which exposes the functionality of the domain
/// to the presentation layer.
/// </summary>
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();
}
}
}
/// <summary>
/// This view model is part of the presentation layer.
/// </summary>
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; }
}
/// <summary>
/// This is part of the presentation layer.
/// </summary>
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<TEvent> where TEvent : IDomainEvent
{
void Handle(TEvent e);
}
/// <summary>
/// This handler contains the business logic associated with changing birthdates. This logic may change
/// and may depend on other factors.
/// </summary>
class BirthDateChangedBirthPlaceHandler : IHandle<BirthDateChangedEvent>
{
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);
}
}
}
}