首页 > 解决方案 > DDD 函数式方式:为什么在函数式语言中应用 DDD 时最好将状态与行为解耦?

问题描述

我已经阅读了几篇文章(也是这本书Functional domain modeling),他们建议将域对象的状态与行为分离,但我无法理解这种方法相对于范围域模型的优势。

这是到达域模型的示例:

case class Account(id: AccountId, balance: Money) {
  def activate: Account = {
   // check if it is already active, eg, enforce invariant 
   ...
  }
  def freeze: Account = ???
} 

我可以通过以下方式与此帐户链接操作:

account.activate.freeze

这是他们建议的“贫血”方法的示例:

case class Account(id: AccountId, balance: Money)

object AccountService {
  def activate =  (account: Account) => {
   // check if it is already active, eg, enforce invariant 
    ...
  }

  def freeze =  (account: Account) =>   {
    ...     
  }
}

在这里我可以像这样链接操作

activate andThen freeze apply account

除了“优雅”的语法之外,第二种方法的优势是什么?

此外,在到达域模型的情况下,我将在单个类中强制执行不变量,但在“贫血”模型的情况下,逻辑/不变量可以跨服务传播

标签: scalafunctional-programmingdomain-driven-designaggregateroot

解决方案


我提供了两个思考过程,可以帮助解释这个谜题:


state你的例子和书中的概念不同。 (我确实希望我们都指的是功能性和反应性领域建模)。

您的activatefreeze示例状态可能是领域概念,而本书讨论的状态仅用作标记。它们不一定在域逻辑中发挥作用,并且仅用于消除工作流状态的歧义。前任。申请批准充实


函数式编程就是实现行为,这些行为独立于传递给它们的数据。

在实施此类行为时,有两个方面需要注意。

行为可以跨上下文重用。它可以是一个抽象特征,如果你愿意,可以是一个幺半群,它采用任何类型T并对其执行相同的操作。在您的示例中,freeze可能是这样的行为,适用于Account, Loan,Balance等。

这种行为没有任何副作用。人们应该能够使用相同的数据集一次又一次地调用该行为并接收相同的预期响应,而不会影响系统或引发错误。参考您的示例,在帐户上重复调用 freeze 不应引发错误。

结合这两点,可以说将行为实现为跨不同上下文的可重用代码片段(作为 a Service)同时确保输入得到验证(即,在处理之前验证作为输入提供的对象的状态)是有意义的.

通过将对象的可接受状态表示为单独的类型并使用此显式类型参数化模型/对象,我们可以在编译期间强制对输入进行静态检查。参考书中提供的示例,您只能approve andThen enrich. 任何其他不正确的序列都会引发编译时错误,这比使用防御性守卫在运行时检查输入要好得多。

因此,第二种方法最终不仅仅是优雅的语法。它是一种基于对象状态构建编译时检查的机制。


因此,虽然输出看起来像贫血模型,但第二种方法是利用函数式编程购买的一些漂亮模式。


推荐阅读