DDD的大多数战术设计模式都属于面向对象的范式,而贫乏的模型则描述了将所有业务逻辑都置于服务而非对象中的情况,从而使它们成为一种DTO。换句话说,贫血模型是程序样式的同义词,不建议用于复杂模型。
我在纯函数式编程方面不是很有经验,但是我想知道DDD如何适合FP范例以及在这种情况下是否仍然存在“贫血模型”一词。
DDD的大多数战术设计模式都属于面向对象的范式,而贫乏的模型则描述了将所有业务逻辑都置于服务而非对象中的情况,从而使它们成为一种DTO。换句话说,贫血模型是程序样式的同义词,不建议用于复杂模型。
我在纯函数式编程方面不是很有经验,但是我想知道DDD如何适合FP范例以及在这种情况下是否仍然存在“贫血模型”一词。
Answers:
描述“贫血模型”问题的方式不能很好地转化为FP。首先,需要对其进行适当的概括。本质上,贫血模型是一个模型,其中包含有关如何正确使用模型的知识,而模型本身并未对此进行封装。取而代之的是,这些知识遍布一堆相关服务。这些服务应仅是模型的客户,但由于贫血,他们对此负有责任。例如,考虑一个Account
类,除非通过AccountManager
类进行处理,否则该类不能用于激活或停用帐户,甚至不能查询有关帐户的信息。该帐户应负责该帐户的基本操作,而不是某些外部经理类。
在函数式编程中,当数据类型不能准确表示应建模的对象时,也会出现类似的问题。假设我们需要定义一个表示用户ID的类型。“贫乏”的定义将指出用户ID是字符串。从技术上讲这是可行的,但由于没有像使用任意字符串那样使用用户ID,因此遇到了很多问题。将它们串联或切掉它们的子串是没有意义的,Unicode并不重要,它们应该容易地嵌入URL和其他具有严格字符和格式限制的上下文中。
解决此问题通常需要几个步骤。一个简单的第一步就是说“嗯,a UserID
与字符串等效地表示,但是它们是不同的类型,您不能在期望另一个的地方使用它。” Haskell(和一些其他类型的功能语言)通过newtype
以下方式提供此功能:
newtype UserID = UserID String
这定义了UserID
函数给定,当String
构建体,其值当作一个UserID
由所述类型的系统,但其仍然只是一个String
在运行时。现在,函数可以声明它们需要a UserID
而不是字符串。UserID
在以前使用字符串的地方使用s可以防止将两个UserID
s 连接在一起的代码。类型系统保证不会发生,不需要测试。
这里的缺点是,代码仍然可以采取任意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))来辅助,这些功能允许类型系统定义和证明有关代码的更多不变性。当数据与行为脱钩时,数据定义是您必须强制执行行为的唯一手段。
在很大程度上,不变性使得没有必要像OOP所倡导的那样将函数与数据紧密耦合。您可以使用与原始代码相距甚远的代码制作任意数量的副本,甚至制作派生数据结构,而不必担心原始数据结构会意外地从您的身下变出来。
但是,进行此比较的更好方法可能是查看要分配给模型层和服务层的函数。尽管它看起来与OOP中的外观不同,但在FP中尝试将应该将多个抽象级别塞进一个函数中却是一个常见错误。
据我所知,没有人称其为贫血模型,因为这是一个面向对象的术语,但是效果是一样的。您可以并且应该在适用的情况下重用通用函数,但是对于更复杂的或特定于应用程序的操作,还应该提供一组丰富的函数,仅用于处理模型。在任何范例中,创建适当的抽象层都是好的设计。
在OOP中使用DDD时,将业务逻辑放入域对象本身的主要原因之一是,通常通过更改对象的状态来应用业务逻辑。这与封装有关:Employee.RaiseSalary
可能会更改实例的salary
字段,而该字段Employee
不应公开设置。
在FP中,避免了变异,因此您可以通过创建一个RaiseSalary
接受现有Employee
实例并返回具有新薪水的新 Employee
实例的函数来实现此行为。因此,不涉及任何突变:仅读取原始对象并创建新对象。因此,RaiseSalary
不需要将此类函数定义为Employee
类中的方法,而是可以在任何地方使用。
在这种情况下,将数据与行为分离是很自然的:一种结构代表Employee
数据(完全贫血),而一个(或几个)模块包含对数据进行操作的功能(保持不变性)。
请注意,当像DDD中那样将数据和行为结合在一起时,通常会违反“单一职责原则”(SRP):Employee
如果薪金变化的规则发生变化,可能需要更改;但是如果更改EOY奖金的计算规则,则可能还需要更改。使用分离方法时,情况并非如此,因为您可以有多个模块,每个模块都有一个责任。
因此,像往常一样,FP方法提供了更大的模块化/可组合性。
我想知道DDD如何适应FP范例
我认为确实可以,但是主要是作为在不可变值对象之间进行转换的战术方法,或者是在实体上触发方法的方法。(大多数逻辑仍然存在于实体中。)
以及在这种情况下是否仍然存在“贫血模型”一词。
好吧,如果您的意思是“以类似于传统OOP的方式”,那么它有助于忽略通常的实现细节并回到基础知识:领域专家使用哪种语言?您从用户那里捕获的意图是什么?
假设他们谈论链接过程和函数在一起,那么看来函数(或至少是“做事”对象)基本上就是您的域对象!
因此,在这种情况下,当您的“功能”实际上不可执行时,可能会出现“贫血模型”,而只是由实际工作的Service 解释的元数据星座。