【问题标题】:DDD (Domain Driven Design), how to handle entity state changes, and encapsulate business rules that requires large amount of data to be processedDDD(Domain Driven Design),如何处理实体状态变化,封装需要处理大量数据的业务规则
【发布时间】:2012-01-06 08:58:26
【问题描述】:
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);
            }
        }
    }
}

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

但是,我在实现它时遇到了 2 个问题:

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

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

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

【问题讨论】:

  • 我不太明白这个问题。您是在问如何缓存大量项目(SpecialBirthPlaces)或如何将更改持久化到域模型?从 DDD 的角度来看,Bounded Context 似乎是围绕一个人的出生日期和地点的规则的集合,而 Person 聚合根需要有一个允许更改日期和地点的方法。对出生日期和地点的更改将触发导致更新数据存储的事件。我可能完全离开了,因为我自己是 DDD 的新手。
  • 如果出生地在 1991 年之前和之后的“圣彼得堡”,您是否试图实现将“列宁格勒”显示为出生地?
  • 有点晚了,但我处于同样的情况,我是堆栈。您是否设法找到有效的解决方案?或者至少是一些关于 ddd 的正式方法?

标签: c# domain-driven-design


【解决方案1】:

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

/// <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);
            }
        }
    }
}

【讨论】:

  • 我喜欢你的实现,它是一个很好的代码,但它不是 DDD。您在这里所做的是从 DDD 借用基础设施和技术方面,而不使用域模型本身。我看到人们经常这样做。它们的域逻辑分散在服务中,它们的域实体只是 DTO。而且我不能责怪他们,在我看来这是一种合理的模式,因为现实世界的系统需要处理事务、外部服务,并且域逻辑并不像 DDD 示例中向我们展示的那么简单。
  • DomainEvents.Raise 只是 ServiceLocator.Resolve ().GetBirthPlaceFor (...) 的一个花哨名称
【解决方案2】:

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

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

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

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

如果我们用实现INotifyPropertyChanged 的实现来装饰有问题的实体怎么办?我们可以在我们的存储库中执行此操作,我们已经建立在域范围之外,因此我们不会修改域模型本身,只使用组合来包装具有域模型外部系统所需功能的实体。重申一下,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));
    }
}

如果不使用接口,您只需从Person 继承并覆盖BirthDate 属性,调用base 上的成员而不是_inner

【讨论】:

  • 你写的和ViewModel一模一样,封装了Model。但是,我想到我们检查 BirthPlace 是否在设置 BirthDate 后更改。我认为这隐藏了重要信息,即 BirthDate 的更改可能导致 BirthPlace 的更改。而在实体上的某种显式事件(如域事件)将使开发人员更容易注意到这一事实。
  • @AlexBurtsev ViewModel 适用于特定视图。另一方面,该对象将在整个应用程序中用作实体。它带来的好处只是保持域模型的原始状态。行为并没有隐藏在这里;它隐藏在模型中,这就是为什么我认为领域事件是一个很好的替代解决方案。不过,您仍然会遇到更新 UI 的问题。您要么必须执行我在此处显示的操作,要么从域模型外部订阅域事件。
  • 从域外订阅域事件有什么问题?这是不好的做法吗?
  • @AlexBurtsev 不,我认为这很好。
  • 问题是 PropertyChanged 不是域事件。在属性更改时通知侦听器不是域逻辑,而是应用程序的逻辑。而且我不认为 N​​otifyPerson 应该做任何事情,除了将控制权传递给真正的实体然后通知听众。 if(value == _inner.BirthDate) return; 它不知道 BirthDate 设置器在做什么,如果它计算更改出生日期的尝试怎么办? :) 。我不确定 NotifyPerson 是否应该知道哪些属性受方法影响,但可能没问题。
【解决方案3】:

我封装这个问题的方式,即修改跟踪,是使用工作单元模式。我的 DDD 存储库与一个工作单元相关联,我可以查询该工作单元以查找从任何存储库中获得的任何一组实体,以查看哪些已被修改。

至于大集合,它似乎是一个只读集合。处理此问题的一种方法是,如果曾经访问过它,则在本地预加载和缓存它,然后存储库可以针对内存中的版本运行查询。我使用 NHibernate,用它很容易处理这种情况。如果它方式太大而无法存储在 RAM 中(例如 100 多 MB 或更多),您可能需要针对它进行特殊情况的存储库查询,以便在数据库(也许在存储过程中,哈!)。您可能希望将 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<SpecialBirthPlace> 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<BirthPlace> birthPlaces, DateTime date)
    {
        if (BirthPlace != null && date < ImportantDate && BirthPlace.IsSpecial) 
        { 
            BirthPlace = birthPlaces.GetForDate(date); 
        }
    }
} 

【讨论】:

  • 这与我的想法很接近,但我不喜欢我们不能简单地设置 Birthdate,但是如果 BirthPlace 尚未设置(为 null )
  • 在构造函数中执行此操作。这是唯一的 DDD 替代方案。您的业​​务规则规定您不能“只设置它”,因此不要试图强制使用会破坏领域的技术习惯。
  • 如果我们不需要更新 BirthPlace,我仍然对无法设置 BirthDate 感到有些不安,您对代码的这种修改有何看法? pastebin.com/aF7v0a4z,当然 throwinf 异常不是要走的路,但我现在想不出别的。所以模型的消费者应该首先检查是否需要 CorrectBirthDate 调用。
  • IMO,这个答案是一个“好的 DDD”解决方案。类似的我用过很多次,效果很好。我要做的唯一改变是用不同的界面替换IRepository&lt;BirthPlace&gt;,例如IBirthPlaceService(需要更好的名称),因为我认为存储库将其与技术决策相结合。实体不需要关心在哪里/如何,这是一个实现细节。
  • @jasper 我认为存储库接口很好。 “服务”后缀让它变得过于晦涩:“给我一个可以做与出生地相关的各种事情的东西。”哦,亲爱的,这有什么用呢? “存储库”准确:“给我一个可以让我找回出生地的东西。”请注意,存储库(同义词:storehouse)这个词本身并不是一个技术术语,从领域的角度来看也是有意义的。
【解决方案4】:

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

把你所有的出生地查找和更新逻辑放在那个存储过程中。确保 sproc 包含在事务中 - 以便在更新失败时回滚更改。

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

【讨论】:

  • 对不起,保罗,但你的回答对于我的情况实际上是 100% 不正确的,这很有趣 -) 首先这与 DDD 无关。 2. 我正在使用没有存储过程的对象数据库,我的实体是 POCO 对象并且没有实现 INotifyPropertyChanged,我不使用 ObjectContexts (EF),在属性更改事件上调用存储过程是我最可怕的事情听过。但是保罗,你真的让我振作起来,在我看到你的回答之前我很沮丧,谢谢。
  • 很高兴我让你振作起来!我实际上并不是建议从 onPropertyChangedEvent 处理程序中提取 sproc - 事实上这会很可怕。关键是实际上在数据库级别进行查找(考虑到 bd 很少更改,因此无需将列表保存在内存中)并在将数据保存到数据库时执行操作(在同一事务中)。
  • @Paul - 当然,在数据库端进行查找的想法是合理的,因为它实际上只是一个“包含”查询,但您推荐的方法与域驱动方法相反,这使得将所有决策逻辑放在对象本身而不是像数据库这样的各个层中,使我们能够创建非常清晰、可维护的软件。
  • 这不是很可笑吗?将逻辑放在存储过程中可以解决很多问题:无论更改来自什么代码甚至哪个服务器,所需的相应逻辑总是会发生......但是我们会将一半存储过程中的业务规则。
猜你喜欢
  • 1970-01-01
  • 2017-11-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-01-14
相关资源
最近更新 更多