在函数式编程的背景下谈论贫血模型仍然有效吗?


39

DDD的大多数战术设计模式都属于面向对象的范式,而贫乏的模型则描述了将所有业务逻辑都置于服务而非对象中的情况,从而使它们成为一种DTO。换句话说,贫血模型是程序样式的同义词,不建议用于复杂模型。

我在纯函数式编程方面不是很有经验,但是我想知道DDD如何适合FP范例以及在这种情况下是否仍然存在“贫血模型”一词。

更新:纽崔里出版了有关该主题的书籍视频


1
如果您说的是我想在这里说的话,则DTO是贫乏的,但DDD中的一流对象,并且DTO和处理它们的服务之间存在自然的分离。我原则上同意。显然,此博客文章也是如此
罗伯特·哈维


1
“在那种情况下,术语“贫血模型”是否仍然存在”简而言之,贫血模型术语是在OO的背景下提出的。在FP的背景下谈论贫血模型根本没有任何意义。在描述什么是惯用FP的意义上可能有对等的含义,但与贫血模型无关。
plalx

5
埃里克·埃文斯(Eric Evans)曾经被问到对那些指责他的人说些什么,他在书中描述的只是一个好的面向对象设计,他回答说这不是一个指责,这是事实,DDD只是一个好的OOD,他只是写道记下一些食谱和模式,并给它们起名称,以便更轻松地遵循它们并进行讨论。因此,DDD与OOD链接也就不足为奇了。尽管您必须首先定义“功能编程”的含义,但更广泛的问题是,OOD和FPD之间的交叉点和区别是什么。
约尔格W¯¯米塔格

2
@JörgWMittag:您的意思不是通常的定义吗?有很多说明性平台,Haskell是最明显的平台。
罗伯特·哈维

Answers:


23

描述“贫血模型”问题的方式不能很好地转化为FP。首先,需要对其进行适当的概括。本质上,贫血模型是一个模型,其中包含有关如何正确使用模型的知识,而模型本身并未对此进行封装。取而代之的是,这些知识遍布一堆相关服务。这些服务应仅是模型的客户,但由于贫血,他们对此负有责任。例如,考虑一个Account类,除非通过AccountManager类进行处理,否则该类不能用于激活或停用帐户,甚至不能查询有关帐户的信息。该帐户应负责该帐户的基本操作,而不是某些外部经理类。

在函数式编程中,当数据类型不能准确表示应建模的对象时,也会出现类似的问题。假设我们需要定义一个表示用户ID的类型。“贫乏”的定义将指出用户ID是字符串。从技术上讲这是可行的,但由于没有像使用任意字符串那样使用用户ID,因此遇到了很多问题。将它们串联或切掉它们的子串是没有意义的,Unicode并不重要,它们应该容易地嵌入URL和其他具有严格字符和格式限制的上下文中。

解决此问题通常需要几个步骤。一个简单的第一步就是说“嗯,a UserID与字符串等效地表示,但是它们是不同的类型,您不能在期望另一个的地方使用它。” Haskell(和一些其他类型的功能语言)通过newtype以下方式提供此功能:

newtype UserID = UserID String

这定义了UserID函数给定,当String构建体,其值当作一个UserID由所述类型的系统,但其仍然只是一个String在运行时。现在,函数可以声明它们需要a UserID而不是字符串。UserID在以前使用字符串的地方使用s可以防止将两个UserIDs 连接在一起的代码。类型系统保证不会发生,不需要测试。

这里的缺点是,代码仍然可以采取任意String"hello"和构建一个UserID从它。进一步的步骤包括创建一个“智能构造函数”函数,该函数在给定字符串时会检查一些不变量,并UserID在满足条件时才返回。然后,将“哑” UserID构造函数设为私有,因此,如果客户端需要,UserID必须使用智能构造函数,从而防止出现格式错误的UserID。

甚至更进一步的步骤也UserID以这样的方式定义数据类型,即仅根据定义就不可能构造格式错误或“不合适” 的数据类型。例如,将a定义UserID为数字列表:

data Digit = Zero | One | Two | Three | Four | Five | Six | Seven | Eight | Nine
data UserID = UserID [Digit]

为了构造一个UserID数字列表,必须提供。有了这个定义,就很简单地表明不可能UserID存在无法在URL中表示的。在Haskell中定义这样的数据模型通常可以借助高级类型系统功能(如数据类型通用代数数据类型(GADT))辅助,这些功能允许类型系统定义和证明有关代码的更多不变性。当数据与行为脱钩时,数据定义是您必须强制执行行为的唯一手段。


2
聚合和聚合根守恒不变,那最后的那些也可以表达出来吗?对我来说,DDD最有价值的部分是业务模型到代码的直接映射。您的回答就与此有关。
帕维尔·沃罗宁

2
演讲很好,但没有回答OP问题。
SerG

10

在很大程度上,不变性使得没有必要像OOP所倡导的那样将函数与数据紧密耦合。您可以使用与原始代码相距甚远的代码制作任意数量的副本,甚至制作派生数据结构,而不必担心原始数据结构会意外地从您的身下变出来。

但是,进行此比较的更好方法可能是查看要分配给模型和服务层的函数。尽管它看起来与OOP中的外观不同,但在FP中尝试将应该将多个抽象级别塞进一个函数中却是一个常见错误。

据我所知,没有人称其为贫血模型,因为这是一个面向对象的术语,但是效果是一样的。您可以并且应该在适用的情况下重用通用函数,但是对于更复杂的或特定于应用程序的操作,还应该提供一组丰富的函数,仅用于处理模型。在任何范例中,创建适当的抽象层都是好的设计。


2
“在很大程度上,不变性使得没有必要像OOP所倡导的那样将函数与数据紧密耦合。”:耦合数据和过程的另一个原因是通过动态调度实现多态。
Giorgio

2
在DDD上下文中,将行为与数据耦合的主要优点是提供了有意义的业务相关接口。它总是在手边。我们有一种自然的自我记录代码的方式(至少这是我惯用的方式),这是与业务专家成功进行沟通的关键。然后在FP中如何完成?管道可能有帮助,但还有什么呢?FP的通用性是否会使业务需求更难从代码中进行反向工程?
帕维尔·沃罗宁

7

在OOP中使用DDD时,将业务逻辑放入域对象本身的主要原因之一是,通常通过更改对象的状态来应用业务逻辑。这与封装有关:Employee.RaiseSalary可能会更改实例的salary字段,而该字段Employee不应公开设置。

在FP中,避免了变异,因此您可以通过创建一个RaiseSalary接受现有Employee实例并返回具有新薪水的 Employee实例的函数来实现此行为。因此,不涉及任何突变:仅读取原始对象并创建新对象。因此,RaiseSalary不需要将此类函数定义为Employee类中的方法,而是可以在任何地方使用。

在这种情况下,将数据与行为分离是很自然的:一种结构代表Employee数据(完全贫血),而一个(或几个)模块包含对数据进行操作的功能(保持不变性)。

请注意,当像DDD中那样将数据和行为结合在一起时,通常会违反“单一职责原则”(SRP):Employee如果薪金变化的规则发生变化,可能需要更改;但是如果更改EOY奖金的计算规则,则可能还需要更改。使用分离方法时,情况并非如此,因为您可以有多个模块,每个模块都有一个责任。

因此,像往常一样,FP方法提供了更大的模块化/可组合性。


-1

我认为问题的实质是,一个贫乏的模型具有在模型上运行的服务中的所有域逻辑,基本上是过程编程-与“真实”的OO编程相反,在这种情况下,对象具有“智能”且不仅包含数据以及与数据最紧密相关的逻辑。

函数编程也存在相同的对比:“实际” FP意味着将函数用作一等实体,这些实体作为参数传递,并在运行时构造并作为返回值返回。但是,当您无法充分利用所有功能而只对在它们之间传递的数据结构进行操作的功能时,您最终将处在同一位置:您基本上是在进行过程编程。


5
是的,这基本上是OP在他的问题中所说的。你们两个似乎都忘记了仍然可以具有功能组合
罗伯特·哈维

-3

我想知道DDD如何适应FP范例

我认为确实可以,但是主要是作为在不可变值对象之间进行转换的战术方法,或者是在实体上触发方法的方法。(大多数逻辑仍然存在于实体中。)

以及在这种情况下是否仍然存在“贫血模型”一词。

好吧,如果您的意思是“以类似于传统OOP的方式”,那么它有助于忽略通常的实现细节并回到基础知识:领域专家使用哪种语言?您从用户那里捕获的意图是什么?

假设他们谈论链接过程和函数在一起,那么看来函数(或至少是“做事”对象)基本上就是您的域对象!

因此,在这种情况下,当您的“功能”实际上不可执行时,可能会出现“贫血模型”,而只是由实际工作的Service 解释的元数据星座。


1
当您将抽象数据类型(例如元组,记录或列表)传递给不同的函数进行处理时,就会出现贫血模型。您不需要像“不执行的功能”(无论是什么)一样奇特的东西。
罗伯特·哈维

因此,“功能”周围的引号用来强调标签在贫血时变得多么不合适。
达里安

如果您具有讽刺意味,那就有点微妙了。
罗伯特·哈维
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.