无法将实际逻辑放入DDD域层

时间:2011-09-05 09:21:34

标签: domain-driven-design

尽管已经研究了Domain Driven Design很长一段时间,但我仍然有一些基本要素。

似乎每当我尝试设计一个丰富的domain layer时,我仍然需要大量的Domain Services或一个厚Application Layer,我最终会得到一堆附近的除了“GetTotalAmount”之外,其中没有真正逻辑的贫血域实体。关键问题是实体不了解外部因素,并且向实体注入任何内容都是不好的做法。

让我举几个例子:

1。用户注册服务。用户持久保存在数据库中,生成并保存文件(用户帐户需要),并发送确认电子邮件。

确认电子邮件的示例已在其他主题中进行了大量讨论,但没有得出真正的结论。有人建议将逻辑放在application service中,从EmailService注入FileServiceinfrastructure layer。但那时我会在域外拥有业务逻辑,对吧?其他人建议创建一个domain service来注入infrastructure services - 但在这种情况下,我需要在infrastructure servicesdomain layer的接口IEmailServiceIFileService)看起来也不太好(因为domain layer无法引用infrastructure layer)。其他人建议实施Udi Dahan's Domain Events,然后让EmailService和FileService订阅这些事件。但这似乎是一个非常松散的实现 - 如果服务失败会发生什么?请告诉我您认为正确的解决方案。

2。从数字音乐商店购买歌曲。购物车已清空。购买持续存在。付款服务被呼叫。系统会发送电子邮件确认信息。

好的,这可能与第一个例子有关。这里的问题是,谁负责编排这笔交易?当然,我可以通过注入服务将所有内容放入MVC控制器中。但如果我想要真正的DDD,所有业务逻辑都应该在域中。但是哪个实体应该采用“购买”方法? Song.Purchase()Order.Purchase()OrderProcessor.Purchase()(域名服务)? ShoppingCartService.Purchase()(申请服务?)

在这种情况下,我认为在域实体中使用真实的业务逻辑非常困难。如果向实体注入任何东西不是好的做法,那么除了检查自己(及其聚合的)状态之外,他们怎么做其他事情呢?

我希望这些例子足够清楚,以显示我正在处理的问题。

3 个答案:

答案 0 :(得分:11)

Dimitry的回答指出了一些值得寻找的好事。通常/轻松地发现自己处于自己的场景中,数据从db到GUI通过不同的层进行挖掘。

我受到Jimmy Nilsson的简单建议“价值对象,价值对象和更多价值对象”的启发。通常人们倾向于将注意力集中在名词上并将其建模为实体。当然,您经常无法找到DDD行为。动词更容易与行为相关联。一个好处是让这些动词在您的域中显示为Value对象。

我在尝试开发域时使用的一些指导(必须说构建一个富域需要时间,通常是几次重构迭代......):

  • 最小化属性(获取/设置)
  • 尽可能多地使用价值对象
  • 尽可能少地暴露。使您的域聚合方法更直观。

不要忘记通过验证可以使您的域名变得丰富。只有您的域名知道如何进行购买,以及需要什么。

当您的实体从一个州转换到另一个州(工作流程验证)时,您的域名也应负责验证。

我会举几个例子: 以下是我在博客上撰写的关于您关于贫血领域http://magnusbackeus.wordpress.com/2011/05/31/preventing-anemic-domain-model-where-is-my-model-behaviour/

的问题的文章

我还可以真正推荐Jimmy Bogard关于实体验证的博客文章,并使用Validator模式和扩展方法。它使您可以自由地验证基础结构,而不会使您的域变脏: http://lostechies.com/jimmybogard/2007/10/24/entity-validation-with-visitors-and-extension-methods/

我使用Udi的Domain Events非常成功。如果您认为您的服务可能失败,您也可以使它们异步。您还将它包装在事务中(使用NServiceBus框架)。

在你的第一个例子中(现在只是集思广益,让我们的思想更多地考虑价值对象)。

  1. 您的MusicService.AddSubscriber(User newUser)应用服务与来自演示者/控制器/ WCF的新用户接听电话。 该服务已将IUserRepositoryIMusicServiceRepository注入ctor。
  2. 音乐服务“Spotify”通过IMusicServiceRepository
  3. 加载
  4. entity musicService.SignUp(MusicServiceSubscriber newSubsriber)方法接受Value对象MusicServiceSubscriber。 此Value对象必须在ctor中使用User和其他必需对象 (值对象是不可变的)。在这里,您还可以放置逻辑/行为,如handle subscriptionId等。
  5. SignUp方法也会执行,它会触发域事件NewSubscriberAddedToMusicService。 它被EventHandler HandleNewSubscriberAddedToMusicServiceEvent抓住了,IFileServiceIEmailService被注入其中。此处理程序的实现位于应用程序服务层中,但事件由域和MusicService.SignUp控制。这意味着域名处于控制之中。 Eventhandler创建文件并发送电子邮件。
  6. 您可以通过事件处理程序保留用户,也可以使用MusicService.AddSubscriber(...)方法。两者都将通过IUserRepository来做到这一点,但这是一个品味问题,也许它将如何反映实际领域。

    最后......我希望你能掌握上述内容......无论如何。最重要的是开始向实体添加“动词”方法并进行协作。您还可以在域中拥有不持久的对象,它们仅用于在多个域实体之间进行调解,并且可以托管算法等。

答案 1 :(得分:8)

  

用户注册服务。用户坚持使用   数据库,生成并保存文件(用户帐户需要),   并发送确认电子邮件。

您可以在此处申请Dependency Inversion Principle。定义这样的域界面:

void ICanSendConfirmationEmail(EmailAddress address, ...)

void ICanNotifyUserOfSuccessfulRegistration(EmailAddress address, ...)

接口可以被其他域类使用。使用真正的SMTP类在基础架构层中实现此接口。在应用程序启动时注入此实现。这样,您在域代码中声明了业务意图,并且您的域逻辑没有直接引用SMTP基础结构。这里的关键是界面的名称,它应该基于无所不在的语言。

  

从数字音乐商店购买歌曲。购物车   被清空了。购买持续存在。付款服务被呼叫。   发送电子邮件确认。好吧,这可能与第一个例子有关。这里的问题是,谁负责编排这笔交易?

使用OOP最佳实践来分配职责(GRASP和SOLID)。单元测试和重构将为您提供设计反馈。业务流程本身可以是精简应用层的一部分。来自DDD Layered Architecture

  

应用层:定义软件应该执行的作业并指示   表达域对象来解决问题。这一层的任务   负责对业务有意义或必要的   与其他系统的应用层交互。

     

此图层保持薄。它不包含业务规则或   知识,但只协调任务和代表工作   下一层中域对象的协作。它不是   有国家反映业务情况,但它可以有国家   这反映了用户或程序的任务进度。

答案 2 :(得分:0)

您的大部分请求都与面向对象的设计和职责分配有关,您可以想到GRASP PatternsThis,您可以从面向对象的设计书中受益,推荐以下

Applying UML and Patterns