为什么“功能与数据之间的紧密耦合”不好?


38

我在第40页的“ Clojure的喜悦 ”中找到了这句话。32岁,但上周晚饭时有人对我说了同样的话,我在其他地方也听说过:

[A]面向对象编程的缺点是函数和数据之间的紧密耦合。

我理解为什么不必要的耦合在应用程序中不好。我也很高兴地说,即使在面向对象的编程中,也应避免避免可变状态和继承。但是我看不到为什么在类上粘贴函数本质上是不好的。

我的意思是,向类添加功能似乎就像在Gmail中标记邮件,或将文件粘贴在文件夹中。这是一种组织技巧,可帮助您再次找到它。您选择一些条件,然后将类似的东西放在一起。在OOP之前,我们的程序几乎是文件中方法的一大包。我的意思是,您必须将函数放在某个地方。为什么不组织它们?

如果这是对类型的公开攻击,为什么他们不只是说将输入和输出的类型限制为一个函数是错误的?我不确定是否可以同意这一点,但是至少我对pro和con类型安全性论点很熟悉。在我看来,这似乎是一个主要的问题。

当然,有时人们会错误地将功能放在错误的类上。但是与其他错误相比,这似乎是一个很小的麻烦。

因此,Clojure具有名称空间。在OOP中将函数粘贴在类上与在Clojure中将函数粘贴在命名空间中有什么不同,为什么这么糟?请记住,类中的函数不一定只在该类的成员上运行。看一下java.lang.StringBuilder-它可以对任何引用类型进行操作,也可以通过自动装箱对任何类型进行操作。

PS此引用引用了一本书,但我没有读过:《Leda中的Multiparadigm编程》:Timothy Budd,1995年


20
我相信作者只是不能正确理解OOP,仅需要一个理由就可以说Java不好而Clojure很好。/ rant
欣快感,2013年

6
实例方法(不同于自由函数或扩展方法)不能从其他模块中添加。当您考虑只能由实例方法实现的接口时,这将成为更多的限制。您不能在不同的模块中定义接口和类,然后使用第三个模块中的代码将它们绑定在一起。像haskell的类型类这样的更灵活的方法应该能够做到这一点。
CodesInChaos

4
@Euphoric我相信作者确实理解,但是Clojure社区似乎希望成为OOP的稻草人,并将其烧毁,以作为编程的所有弊端,然后再进行垃圾回收,大量内存,快速处理器和大量的磁盘空间。我希望他们放弃对OOP的抨击,并瞄准真正的原因:例如冯·诺伊曼(Von Neuman)架构。
GlenPeterson 2013年

4
我的印象是,对OOP的大多数批评实际上是对Java中实现的对OOP的批评。不是因为那是一个故意的稻草人,而是因为这是他们与OOP相关联的东西。人们抱怨静态类型也存在类似的问题。大多数问题不是该概念固有的,而仅仅是该概念的流行实现中的缺陷。
CodesInChaos

3
您的标题与问题正文不符。很容易解释为什么功能和数据的紧密耦合不好,但是您的文本提出了以下问题:“ OOP这样做吗?”,“如果这样,为什么?” 和“这是一件坏事吗?”。到目前为止,您已经足够幸运地收到涉及这三个问题中的一个或多个的答案,而没有一个假设标题中的简单问题。
itsbruce 2013年

Answers:


34

从理论上讲,松散的功能数据耦合使添加更多功能来处理相同数据变得更加容易。不利的一面是,很难更改数据结构本身,这就是为什么在实践中,精心设计的功能代码和精心设计的OOP代码具有非常相似的耦合级别。

以有向无环图(DAG)为例的数据结构。在函数式编程中,您仍然需要一些抽象以避免重复自己,因此您将要制作一个具有以下功能的模块:添加和删除节点和边,找到给定节点可到达的节点,创建拓扑排序等。即使编译器不强制执行,它们也有效地紧密耦合到数据。您可以通过困难的方式添加节点,但是为什么要这样做呢?一个模块内的内聚性阻止了整个系统的紧密耦合。

相反,在OOP方面,除基本DAG操作之外的任何功能都将在单独的“视图”类中完成,并将DAG对象作为参数传递。添加对DAG数据进行操作的任意数量的视图,创建与功能程序中相同级别的功能数据解耦,就很容易。编译器不会阻止您将所有东西都塞进一堂课,但是您的同事会。

更改编程范例不会更改抽象,内聚和耦合的最佳实践,而只会更改编译器帮助您实施的实践。在函数式编程中,当您需要函数数据耦合时,它是由先生们而不是编译器同意执行的。在OOP中,模型视图分离是由先生们而不是编译器同意执行的。


13

如果您还不了解它,那就可以理解一下:面向对象和闭包的概念是同一枚硬币的两个方面。也就是说,什么是封闭?它从周围的作用域中获取变量或数据,并在函数内部或从面向对象的角度绑定到变量或数据,例如,当您将某些东西传递给构造函数时,您实际上会做同样的事情,以便以后可以使用该实例的成员函数中的一条数据。但是,从周围范围中拿走东西并不是一件好事-周围范围越大,这样做越有害(尽管从实用上讲,完成工作通常需要一些邪恶)。全局变量的使用将这一点推到了极致,程序中的函数在程序范围内使用变量-确实非常邪恶。有关于全局变量为何有害的其他地方的很好描述

如果您遵循OO技术,那么您基本上已经接受了程序中的每个模块都会有一定程度的邪恶。如果您采用功能性的方法进行编程,那么您将追求一种理想的目标,即程序中的任何模块都不会包含闭包,尽管您可能仍然有闭包,但它比OO少得多。

那就是OO的缺点-它鼓励这种邪恶的做法,即通过使闭包成为标准(一种打破常规的编程理论),将数据耦合到功能上。

唯一的好处是,如果您知道要使用很多闭包作为开始,那么OO至少为您提供了一个思想框架来帮助组织这种方法,以便普通程序员可以理解它。特别是被关闭的变量在构造函数中是显式的,而不是在函数闭包中隐式地采用。使用很多闭包的功能程序通常比等效的OO程序更隐秘,尽管不一定会不太优雅:)


8
每日引用:“完成工作通常需要一些邪恶”
GlenPeterson 2013年

5
您还没有真正解释为什么您所说的邪恶是邪恶的。你只是称他们为邪恶。说明为什么它们是邪恶的,您可能会对绅士的问题有一个答案。
罗伯特·哈维

2
最后一段保存了答案。根据您的说法,它可能是唯一的优势,但这并不是一件小事。我们所谓的“普通程序员”实际上欢迎一定数量的仪式,这肯定足以使我们知道到底发生了什么。
罗伯特·哈维

如果OO和闭包是同义词,那么为什么有那么多OO语言无法为其提供明确支持?您引用的C2 Wiki页面与该站点的正常情况相比,有更多的争议(并且共识更少)。
itsbruce

1
@itsbruce基本上没有必要了。相反,将被“封闭”的变量变为传递到对象中的类变量。
Izkata

7

关于类型耦合:

对象中内置的用于该对象的函数不能在其他类型的对象上使用。

在Haskell中,您可以编写针对类型类使用的函数-因此,给定函数可以处理许多不同类型的对象,只要它是函数可以使用的给定的类型即可。

独立的函数可以实现这样的去耦,当您专注于编写可在A型内部工作的函数时,这种解耦是不会实现的,因为如果您没有A型实例,那么即使您使用该函数,也无法使用它们否则应足够通用以用于B型实例或C型实例。


3
这不是接口的全部要点吗?为了提供允许类型B和类型C在您的函数中看起来相同的东西,以便它可以对多个类型进行操作?
Random832 2013年

2
@ Random832绝对是,但是如果不使用该数据类型,为什么还要在该数据类型中嵌入一个函数呢?答案是:这是唯一的原因,以嵌入功能的数据类型。除了静态类,您什么都不能写,并且使所有函数都不关心它们封装的数据类型,以使它们与自己的类型完全脱钩,但是那为什么还要麻烦将它们放入类型中呢?功能性方法说:不用费心,编写函数以实现各种接口,然后就没有理由将它们与数据封装在一起了。
Jimmy Hoffa 2013年

您仍然必须实现接口。
Random832 2013年

2
@ Random832接口是数据类型;他们不需要封装在其中的功能。有了免费功能,所有需要消耗的接口就是它们可以为功能使用的数据。
Jimmy Hoffa 2013年

2
与OO中常见的现实世界对象相关联的@ Random832,请考虑一本书的界面:它呈现信息(数据),仅此而已。您可以使用免费的翻页功能,该功能可处理具有页面类型的类别,该功能可处理各种书籍,新闻纸,K-Mart的海报锭子,贺卡,邮件,角。如果您将翻页书作为本书的一部分来实现,那么您会错过所有可以使用翻页书的东西,因为它是一项自由功能,因此不受限制。它只是在啤酒上抛出PartyFoulException。
Jimmy Hoffa 2013年

4

在Java和类似的OOP版本中,不能从其他模块中添加实例方法(与自由函数或扩展方法不同)。

当您考虑只能由实例方法实现的接口时,这将成为更多的限制。您不能在不同的模块中定义接口和类,然后使用第三个模块中的代码将它们绑定在一起。像Haskell的类型类这样的更灵活的方法应该能够做到这一点。


您可以在Scala中轻松做到这一点。我不熟悉Go,但是AFAIK也可以在那做。在Ruby中,在事实发生后向对象添加方法以使它们符合某个接口也是一种很常见的做法。您所描述的似乎是一个设计不良的类型系统,而不是与OO远程相关的任何系统。就像一个思想实验:谈论抽象数据类型而不是对象时,您的答案会有何不同?我不认为这会有所作为,这将证明您的论点与面向对象无关。
约尔格W¯¯米塔格

1
@JörgWMittag我认为您的意思是代数数据类型。和CodesInChaos一样,Haskell非常明确地阻止了您的建议。它称为孤立实例,并在GHC上发出警告。
jozefg

3
@JörgWMittag我的印象是,许多批评OOP的人都批评Java和类似语言中使用的OOP形式,因为它具有严格的类结构,并且专注于实例方法。我对那句话的印象是,它批评了对实例方法的关注,而并没有真正适用于其他类型的OOP,例如golang所使用的。
CodesInChaos

2
@CodesInChaos然后也许可以将其澄清为“基于静态类的OO”
jozefg

@jozefg:我说的是抽象数据类型。我什至看不到代数数据类型与本次讨论有多远的关系。
约尔格W¯¯米塔格

3

面向对象从根本上讲是关于过程数据抽象的(如果除去副作用是正交的问题,则是功能数据抽象)。从某种意义上说,Lambda Calculus是最古老,最纯净的面向对象语言,因为它提供功能数据抽象(因为它除了函数外没有任何构造)。

只有单个对象的操作才能检查该对象的数据表示。甚至同一类型的其他对象也无法做到这一点。(这是面向对象的数据抽象和抽象数据类型之间的主要区别:使用ADT,相同类型的对象可以检查彼此的数据表示形式,只有其他类型的对象的表示形式被隐藏。)

这意味着相同类型的几个对象可能具有不同的数据表示形式。即使是同一对象,在不同时间也可能具有不同的数据表示形式。(例如,在Scala中,Maps和Sets根据元素的数量在数组和哈希树之间切换,因为对于非常小的数字,由于常量因数非常小,所以数组中的线性搜索比对搜索树中的对数搜索快。 )

从对象的外部,您不应该,也不知道其数据表示形式。这紧密耦合相反


我在OOP中有一些类,它们可以根据情况切换内部数据结构,因此这些类的对象实例可以同时使用非常不同的数据表示形式。我会说基本数据隐藏和封装?那么Scala中的Map与OOP语言中正确实现的(wrt数据隐藏和封装)Map类有何不同?
Marjan Venema 2013年

在您的示例中,使用访问器函数将数据封装在一个类中(从而将这些函数与该数据紧密耦合)实际上使您可以将该类的实例与程序的其余部分轻松地耦合。您驳斥了报价的中心点-非常好!
GlenPeterson 2013年

2

数据和函数之间的紧密耦合是不好的,因为您希望能够彼此独立地进行更改,而紧密耦合则使此操作变得很困难,因为您无法在不了解另一种且可能无法更改另一种的情况下进行更改。

您希望向功能提供的不同数据不需要对功能进行任何更改,并且类似地,您希望能够对功能进行更改而无需更改正在运行的数据以支持那些功能更改。


1
是的,我想要那个。但是我的经验是,当您将数据发送到未明确设计为处理的非平凡函数时,该函数往往会崩溃。我不仅指类型安全,还指函数作者未预期的任何数据条件。如果该函数是旧的并且经常使用,则任何允许新数据流经的更改都可能破坏它,使其仍然需要工作的某些旧数据形式。尽管解耦可能是功能与数据的理想选择,但这种解耦的现实可能既困难又危险。
GlenPeterson 2013年
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.