关于领域驱动设计的函数编程思考 - Naveen Negi


在过去的几年里,我一直在使用像Elixir和Clojure这样的函数式语言,即使我确信DDD可以应用于函数式语言,但这个领域并没有足够的资源介绍。嗯,也就是很少的相关讨论和博文,但大多数人又试图将DDD模式从OO直接映射到FP。

战略与战术模式
DDD分为战略模式和战术模式,战略模式由有界上下文,无所不在的语言和上下文映射组成,而战术模式由值对象,实体和聚合等概念组成。
战略模式很容易映射到任何语言,它主要涵盖更高级别的软件设计,如如何创建有界上下文,如何根据它们之间的关系集成这些有界上下文,以及如何通过上下文图映射这些关系。这些模式非常有用,不依赖于所使用的编程语言或框架。
然而,在实现战术模式时会产生混乱,因为它们的实现取决于所使用的编程语言。
OO更喜欢将数据和行为(方法)保持在一起(对象),因为期望对象具有状态,并且所有改变内部状态的操作必须由对象本身提供(通过“xx.method()”表示法)。但是,默认情况下,函数式编程语言是不可变的,将函数和数据结构保存在不同的模块中是很常见的。我花了一段时间才意识到FP中不存在贫血领域模型这个概念。

聚合
个演讲中,Ritch Hickey谈到了聚合而没有提到DDD,但我认为他提供了一个非常好的Aggregates解释。为Clojure(或任何其他功能语言)实现它们提供了一个很好的指导。

聚合背后的想法是强制一致性和不变量。

聚合是强制执行不变量的位置,并充当一致性边界。更新聚合的一部分时,可能还需要更新其他部分以确保一致性。
我在从OO到FP转换过程中遇到的一个误解是只考虑数据,因为数据和行为总是在OO中共存;但是,在FP中,您倾向于将数据和功能函数分开。
因此,在FP中实现Aggregate的关键是在数据和函数两方面考虑聚合。尝试将聚合建模为一组函数,例如,如果您有订单和订单行等实体,其中每个订单可以包含一个或多个订单行,那么订单Order将是您的聚合根,并包含一组强制执行一致性和不变量的函数。假设您有一条规则,即每个订单至少有一个订单行。然后,如果您尝试删除多个订单行,则聚合可确保在只剩下最后一行时会报出错误。

我们是否需要区分值对象和FP中的实体?
DDD的经典(读取面向对象)实现基于其可变性和身份标识概念来区分值对象类型和实体类型。值(对象)类型是不可变的,并且不会在其中传达足够的信息,例如,Color可以是值类型,其中Color类型本身不具有任何含义,但是当附加到实体时如衬衫或汽车(例如红色衬衫或黑色汽车)就代表他们在你的领域意味着什么。
相反,一个实体有一个生命周期。这些是可变的类型,并通过不同的生命周期事件进行更改,例如,Order可以是经历不同生命周期事件的实体,例如添加Item到Order中或从Order中删除Item。每个生命周期事件都会改变实体。
在FP中,默认情况下一切都是不可变的,这导致我们错误地认为我们不需要区分值类型和实体。正如我将在以下部分中解释的那样,如果要构建无限可伸缩的应用程序,这种区别以及其他DDD模式是至关重要的。

建模聚合
无论您在软件应用程序增长时是使用FP还是OO,您都可能最终对数据库进行分区,这实际上意味着以前存住在同一台机器(或机器集群)中的实体/聚合现在生存在不同的机器中。关于实体位置的应用程序代码的任何假设可能不再适用,并且在单个事务中修改多个实体的任何尝试都将面临分布式事务的危险。
以下是关于如何避免这些陷阱的指导原则(灵感来自Pat Helland的这篇着名论文)

  1. 聚合是作为事务边界的:每个聚合用作事务边界。这个Aggregate的唯一标识就是事务的范围边界,不要尝试在一个事务范围中放置多个聚合,因为如果这些聚合移动到不同的计算机,则无法保证事务的成功。
  2. .消息发送到Aggregate:无论您是构建微服务还是单体Monolith应用程序,重要的是我们不要假设其他聚合的存在。每个聚合应通过向其他聚合的地址发送消息来与另一个聚合进行通信,聚合地址是聚合的唯一标识身份ID。遵循这种方法,应用程序可以大规模扩展。良好的聚合建模实际上是聚合到聚合的消息传递。聚合发送一条消息给另一个聚合。接收方d的聚合包括上层(域层)业务逻辑和下层(基础架构层),但两个聚合之间的通信始终位于域层。
  3. Aggregate表示不相交的数据集:看到不少反模式,其中两个聚合共享模型或持久性模式,或者有时存在对属于另一个聚合的模型的隐藏的延迟加载。这些是我们应该不惜一切代价避免的陷阱。您可以通过“伸缩规模不可知的编程抽象”来实现所有这一切,这意味着,您将应用程序划分为有意义的完整聚合,每个聚合通过聚合边界(与规模无关的层)与另一个聚合进行对话,并且不直接访问任何模型或数据库。一个聚合的数据必须与其他聚合的数据不相交。

DDD和开发人员的心态
我见过许多开发人员考虑与OO有关的禁忌。他们甚至不想谈论DDD,因为它太过分了。然而,我们中的许多人都不理解OO仅仅是一个解决问题的工具,并且使工具失去信誉并没有使问题消失。例如,Elixir和Clojure中的协议是解决代码可扩展性问题的一种方法,我看到很多开发人员都不愿意使用它,因为代码可扩展性听起来如此OOish。
下面这个针对Clojure协议的StackOverflow答案清楚地解释了它。

Clojure中协议的目的是以有效的方式解决表达式问题。
那么,表达问题是什么?它指的是可扩展性的基本问题:我们的程序使用操作来操纵数据类型。随着我们的程序的发展,我们需要使用新的数据类型和新操作来扩展它们。特别是,我们希望能够添加与现有数据类型一起使用的新操作,并且我们希望添加与现有操作一起使用的新数据类型。而且我们希望这是真实的扩展,即我们不希望修改现有的程序,我们希望尊重现有的抽象,我们希望我们的扩展是单独的模块,在单独的命名空间中,单独编译,单独部署,单独类型检查。我们希望它们是类型安全的。[注意:并非所有这些都适用于所有语言。但是,例如,即使在像Clojure这样的语言中,使它们具有类型安全性的目标也是有意义的。仅仅因为我们无法静态检查类型安全并不意味着我们希望我们的代码随机中断,对吧?

类似地,实体,值(对象)类型,聚合的概念被FP开发人员嘲笑,因为这些概念只用于OO。