为什么贫血领域模型在C#/ OOP中被认为是不好的,而在F#/ FP中却非常重要?


46

在关于F#的博客文章中,它表示乐趣和收益,它表示:

在功能设计中,将行为与数据分开非常重要。数据类型是简单且“哑”的。然后分别有许多作用于这些数据类型的函数。

这与面向对象的设计正好相反,在面向对象的设计中,行为和数据必须结合在一起。毕竟,这正是一个类。实际上,在真正的面向对象设计中,除了行为外,您应该什么都没有-数据是私有的,只能通过方法访问。

实际上,在OOD中,围绕数据类型的行为不足被认为是一件坏事,甚至有一个名字:“ 贫血领域模型 ”。

鉴于在C#中,我们似乎继续从F#借用,并尝试编写更多的函数式代码;为什么我们不借用分离数据/行为的想法,甚至认为它不好呢?仅仅是因为该定义不与OOP一起使用,还是有一个具体的原因在C#中是不好的,由于某些原因在F#中不适用(实际上是相反的)?

(注意:我对C#/ F#中的差异特别感兴趣,因为它们可能会改变对优劣的看法,而不是对博客文章中的任一种看法持不同意见的人)。


1
嗨,丹!你的热情是鼓舞人心的。除了.NET(和haskell)平台之外,我鼓励您研究scala。Debashish戈什写了一对夫妇大约有功能性的工具领域建模的博客,这是很有见地的我,希望你也一样,在这里你去:debasishg.blogspot.com/2012/01/...
AndreasScheinert

2
今天,一位同事向我发送了一篇有趣的博客文章: blog.inf.ed.ac.uk/sapm/2014/02/04/… 似乎人们开始质疑贫血的领域模型是完全坏的想法。我认为这可能是一件好事!
Danny Tuppeny 2014年

1
您引用的博客文章基于一个错误的想法:“通常可以不封装而公开数据。数据是不可变的,因此不会因行为不当而受到“破坏”。即使是不可变类型,也需要保留不变式,这需要隐藏数据并控制如何创建数据。例如,您不能公开不可变的红黑树的实现,因为那样的话有人可以创建仅由红色节点组成的树。
2015年

4
@Doval是公平的,就像说您不能公开文件系统编写器,因为有人可能会填满您的磁盘。创建仅由红色节点构成的树的人员绝对不会损坏从其克隆的红黑树,也不会破坏整个系统中恰好使用该格式正确的实例的任何代码。如果您编写的代码主动创建新的垃圾实例或做危险的事情,那么不变性不会挽救您,但会挽救您。隐藏实现不会阻止人们编写无用的代码,最终导致代码被零除。
Jimmy Hoffa

2
@JimmyHoffa我同意,但这与我批评的内容无关。作者声称,公开数据通常是好的,因为它是不可变的,我是说不可变性并不能神奇地消除隐藏实现细节的需要。
2015年

Answers:


37

FP旨在实现此目标而C#OOP并非这样做的主要原因是,FP的重点在于参照透明性。即,数据进入功能并且数据输出,但是原始数据未更改。

在C#OOP中,有一个责任委托的概念,您可以将一个对象的管理委托给它,因此您希望它更改其自身的内部结构。

在FP中,您永远不想更改对象中的值,因此将函数嵌入到对象中没有任何意义。

此外,在FP中,您具有更高种类的多态性,可以使您的功能比C#OOP所允许的通用性高得多。这样,您可以编写适用于任何对象的函数,a因此将其嵌入数据块中是没有意义的。那会紧密耦合该方法,使其仅适用于特定类型a。在C#OOP中,这样的行为非常普遍,因为无论如何您通常都无法抽象函数,但是在FP中这是一个折衷。

我在C#OOP的贫血症域模型中看到的最大问题是,由于拥有DTO x而最终得到了重复的代码,并且由于4个不同的人没有看到其他实现,所以有4个不同的函数将活动f提交给DTO x 。将方法直接放在DTO x上时,这4个人都看到了f的实现并重新使用它。

C#OOP中的贫血症数据模型阻碍了代码重用,但是在FP中却不是这种情况,因为单个函数在许多不同类型中通用化,因此您可以获得更大的代码重用性,因为该函数在比您使用的函数更多的场景中可用将使用C#编写单个DTO。


正如评论中指出的那样,类型推断是FP允许实现如此显着的多态性所依赖的好处之一,特别地,您可以将其追溯到具有算法W类型推断的Hindley Milner类型系统。避免了C#OOP类型系统中的这种类型推断,因为由于需要穷举搜索,添加基于约束的推断时的编译时间变得非常长,请参见此处:https : //stackoverflow.com/questions/3968834/generics-why在这种情况下,编译器无法推断出类型的参数


F#中的哪些功能使编写此类可重用代码更加容易?为什么C#中的代码不能像可重用的那样?(我认为我看到了具有使方法可以接受具有特定属性的参数而无需接口的功能;我想这将是关键的一个?)
Danny Tuppeny 2013年

7
老实说,@ DannyTuppeny F#是一个比较差的例子,它只是一个稍微修饰的C#。与C#一样,它是一种可变的命令式语言,它具有C#所不具备但很少的一些FP功能。看看haskell,看看FP真正在什么地方脱颖而出,并且由于类型类和通用ADT而使类似的事情变得更有可能
Jimmy Hoffa 2013年

@MattFenwick我在此明确地指的是C#,因为这就是发布者要问的问题。在整个回答中,我指的是OOP,我的意思是C#OOP,我将对其进行澄清。
Jimmy Hoffa 2013年

2
允许这种重用的功能语言之间的共同特征是动态或推断的类型。尽管语言本身可以使用定义良好的数据类型,但是只要对它们执行的操作(其他函数或算术)有效,典型函数就不会在意什么数据。这也可以在OO范式中使用(例如,Go具有隐式接口实现,允许对象成为Duck,因为它可以进行Fly,Swim和Quack,而无需将对象明确声明为Duck),但是它是函数编程非常必要。
KeithS13年

4
@KeithS通过在Haskell中基于值的重载,我认为您的意思是模式匹配。Haskell具有多个具有相同模式的同名顶级功能的能力会立即降低到1个顶级功能+一个模式匹配。
jozefg

6

为什么贫血领域模型在C#/ OOP中被认为是不好的,而在F#/ FP中却非常重要?

您的问题有一个很大的问题,它将限制您得到的答案的效用:您是在暗示/假设F#和FP相似。FP是一大类语言,包括符号术语重写,动态和静态。即使在静态类型的FP语言中,也有许多种用于表达域模型的技术,例如OCaml和SML中的高阶模块(F#中不存在)。F#是这些功能语言之一,但以精益而著称,尤其是它既不提供高级模块也不提供高级类型。

实际上,我无法开始告诉您如何在FP中表示域模型。这里的另一个答案非常具体地讨论了如何在Haskell中完成该操作,并且根本不适用于Lisp(所有FP语言的母亲),ML语言家族或任何其他功能语言。

为什么我们不借用分离数据/行为的想法,甚至认为它不好呢?

泛型可能被认为是分离数据和行为的一种方式。来自ML系列功能编程语言的泛型不是OOP的一部分。当然,C#具有泛型。因此,有人可能会说C#正在慢慢借用分离数据和行为的想法。

仅仅是因为该定义不适合OOP,

我认为OOP基于根本不同的前提,因此,不能为您提供分离数据和行为所需的工具。出于所有实际目的,您需要乘积和求和数据类型并对其进行调度。在ML中,这意味着并集和记录类型以及模式匹配。

看看我在这里给的例子

还是有一个具体的原因,由于某种原因,它在C#中不好,所以在F#中不适用(实际上是相反的)?

从OOP跳转到C#时要小心。C#与其他语言相比,在OOP方面几乎没有什么纯粹的想法。.NET Framework现在充满了泛型,静态方法甚至lambda。

(注意:我对C#/ F#中的差异特别感兴趣,因为它们可能会改变对优劣的看法,而不是对博客文章中的任一种看法持不同意见的人)。

由于C#中缺乏联合类型和模式匹配,因此几乎不可能做到。当您只有一把锤子时,一切看起来都像钉子...


2
...当您拥有的只是一把锤子时,OOP员工将创建锤子工厂; P +1用于真正确定C#缺失的症结所在,以使数据和行为彼此完全抽象:联合类型和模式匹配。
Jimmy Hoffa 2015年

-4

我认为在业务应用程序中,您通常不希望隐藏数据,因为对不可变值进行模式匹配可以确保您涵盖所有可能的情况。但是,如果要实现复杂的算法或数据结构,则最好隐藏实现细节,从而将ADT(代数数据类型)转换为ADT(抽象数据类型)。


4
您能否进一步解释一下这如何应用于面向对象的编程与功能性编程?
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.