我们真的可以在OOP中使用不变性而不丢失所有关键的OOP功能吗?


11

我看到了使程序中的对象不变的好处。当我真正地为自己的应用程序考虑一个好的设计时,我常常自然而然地想到我的许多对象都是不可变的。我常常想让所有对象保持不变。

这个问题涉及相同的想法,但没有答案表明什么是不变性的好方法以及何时实际使用它。是否有一些好的不变设计模式?总体思路似乎是“使对象不可变,除非您绝对需要更改它们”,这在实践中毫无用处。

我的经验是,不变性将我的代码越来越多地驱动到功能范式,并且这种发展总是会发生:

  1. 我开始需要持久性(在功能上)数据结构,例如列表,地图等。
  2. 使用交叉引用非常不方便(例如,树节点引用其子节点,而子节点引用其父节点),这使我根本不使用交叉引用,这又使我的数据结构和代码更具功能性。
  3. 继承不再有任何意义,我开始改用合成。
  4. OOP的整个基本概念(如封装)开始瓦解,而我的对象开始看起来像函数。

在这一点上,我几乎不再使用OOP范例中的任何内容,而可以切换到纯函数式语言。因此,我的问题是:是否有一种一致的方法来进行良好的不可变OOP设计,或者总是总是这样,当您将不可变的想法发挥到最大潜力时,您总是最终会使用一种功能语言进行编程,而该语言不再需要OOP世界中的任何东西?是否有任何好的指导方针来决定哪些类应该是不可变的,哪些应该保持可变以确保OOP不会崩溃?

为了方便起见,我将提供一个示例。让我们拥有一个ChessBoard不可变棋子的不可变集合(扩展抽象类)Piece)。从OOP的角度来看,一块负责从板上的位置生成有效的移动。但是要生成移动,棋子需要引用其棋盘,而棋盘需要引用其棋盘。好了,有一些技巧可以根据您的OOP语言创建这些不可变的交叉引用,但是它们很难管理,最好不要用它来引用其董事会。但是由于不知道棋盘的状态,所以棋子无法产生移动。然后,片段变成仅包含片段类型及其位置的数据结构。然后,您可以使用多态函数来生成各种片段的移动。这在函数式编程中是完全可以实现的,但是如果没有运行时类型检查和其他不良的OOP惯例,在OOP中几乎是不可能的...然后,


3
我喜欢基本的问题,但是细节方面我很难。例如,当您最大化不变性时,为什么继承会停止起作用?
马丁·巴恩

1
您的对象没有方法?
停止伤害莫妮卡


4
“从OOP的角度来看,一块负责从其在板上的位置产生有效的移动。” -绝对不会,设计这样的作品很可能会损害SRP。
布朗

1
@lishaak您“用简单的术语尽力用继承来解释问题”,因为这不是问题。设计不佳是一个问题。继承是OOP的本质,如果需要,继承是必要的。实际上,许多所谓的“继承问题”是没有明确的覆盖语法的语言的问题,在设计更好的语言中,它们的问题要少得多。没有虚拟方法和多态性,您将没有OOP。您已经使用有趣的对象语法进行了程序编程。因此,如果您避免使用OO,也就不足为奇了!
梅森惠勒

Answers:


24

我们真的可以在OOP中使用不变性而不丢失所有关键的OOP功能吗?

不明白为什么不这样。在Java 8完全发挥功能之前,已经做了很多年了。听说过弦乐吗?从一开始就不错而且一成不变。

  1. 我开始需要持久性(在功能上)数据结构,例如列表,地图等。

一直都需要那些。使我的迭代器无效是因为您在我阅读集合时对其进行了突变是很不礼貌的。

  1. 使用交叉引用非常不方便(例如,树节点引用其子节点,而子节点引用其父节点),这使我根本不使用交叉引用,这又使我的数据结构和代码更具功能性。

循环引用是一种特殊的地狱。不变性不会拯救您。

  1. 继承不再有任何意义,我开始改用合成。

好吧,我在这里与您同在,但看不到与不变性有什么关系。我喜欢合成的原因不是因为我喜欢动态策略模式,而是因为它可以让我改变抽象水平。

  1. OOP的整个基本概念(如封装)开始瓦解,而我的对象开始看起来像函数。

我不禁思索您对“像封装一样的OOP”的想法是什么。如果涉及getter和setter,则请停止调用该封装,因为不是。从来没有。它是面向方面的编程手册。进行验证的机会和放置断点的位置很好,但这不是封装。封装保留了我不知道或不在乎内部发生了什么的权利。

您的对象应该看起来像函数。它们是功能包。它们是一起移动并重新定义自己的功能包。

目前,函数式编程很流行,人们正在摆脱对OOP的误解。不要让这让您迷惑,这就是OOP的终结。功能和OOP可以很好地融合在一起。

  • 函数式编程是关于赋值的正式形式。

  • OOP是关于函数指针的正式形式。

真的是这样。Dykstra告诉我们,这goto是有害的,因此我们对此进行了正式介绍,并创建了结构化编程。就像那样,这两种范例是关于找到完成工作的方法,同时避免因随意执行这些麻烦的事情而带来的陷阱。

让我给你看一些东西:

f n(x)

那是一个功能。它实际上是功能的连续体:

f 1(x)
f 2(x)
...
f n(x)

猜猜我们如何用OOP语言表达它?

n.f(x)

那个小n东西f决定了使用哪种实现,并决定了该函数中使用的某些常量是什么(坦率地说,这是同一件事)。例如:

f 1(x)= x + 1
f 2(x)= x + 2

这与闭包提供的功能相同。在闭包引用其封闭范围的情况下,对象方法引用其实例状态。对象可以更好地完成关闭。闭包是从另一个函数返回的单个函数。构造函数返回对整个函数包的引用:

g 1(x)= x 2 +1 1
g 2(x)= x 2 + 2

是的,你猜对了:

n.g(x)

f和g是一起更改并一起移动的函数。因此,我们将它们放在同一袋中。这才是真正的对象。保持n常数(不变)仅意味着在调用它们时更容易预测它们的作用。

现在,这只是结构。我对OOP的思考方式是一堆与其他小事物交谈的小事情。希望能选择一些小的东西。当我编写代码时,我想象自己是对象。我从对象的角度看事物。而且我会尽量保持懒惰,这样我就不会过度处理对象。我接受简单的消息,对其进行一些处理,然后仅将简单消息发送给我最好的朋友。当我处理完该对象后,我跳入另一个对象,并从它的角度看事情。

班级责任卡是第一个教我这样思考的卡片。伙计,我当时对它们感到困惑,但是该死的,如果它们今天仍然不重要的话。

让我们将ChessBoard作为不可变棋子的不可变集合(扩展抽象类Piece)。从OOP的角度来看,一块负责从板上的位置生成有效的移动。但是要生成移动,棋子需要引用其棋盘,而棋盘需要引用其棋盘。

啊!再次使用不必要的循环引用。

怎么样:ChessBoardDataStructure将xy线变成参考。这些片段具有一种方法,该方法采用x,y和一个特定值,ChessBoardDataStructure然后将其转换为一系列包含new ChessBoardDataStructure的品牌。然后将其推入可以采取最佳动作的位置。现在ChessBoardDataStructure可以是不变的,碎片也可以是不变的。这样,您在内存中只会拥有一个白色的棋子。在正确的xy位置只有几处引用。面向对象,功能齐全且不可变。

等等,我们不是已经开始谈论象棋了吗?


6
每个人都应该阅读。然后读取的了解数据抽象,再访威廉·R·库克。然后再读一遍。然后阅读库克关于“对象”和“面向对象”的简化,现代定义建议。在阿兰凯之遥“ 的大思路是‘消息’ ”,他OO的定义
约尔格W¯¯米塔格


1
@Euphoric:我认为这是一个相当标准的表述,与“功能性编程”,“命令式编程”,“逻辑编程”等的标准定义相匹配。否则,C是一种功能性语言,因为您可以在其中编码FP,逻辑语言,因为您可以在其中编码逻辑程序,动态语言,因为您可以在其中编码动态类型,演员语言,因为您可以在其中编码角色系统,等等。和Haskell是世界上最伟大的命令式语言,因为你其实可以命名的副作用,将它们存储在变量,它们传递的...
约尔格W¯¯米塔格

1
我认为我们可以使用与马蒂亚斯·费莱森(Matthias Felleisen)关于语言表现力的天才论文中类似的想法。OO程序从Java到C♯的迁移只能通过局部转换完成,但是将OO程序迁移到C则需要全局重组(基本上,您需要引入消息分发机制并重定向通过它进行的所有函数调用),因此Java和C♯可以表示OO,而C可以​​“仅”对其进行编码。
约尔格W¯¯米塔格

1
@lishaak因为您要为其他事物指定它。这将其保持在一个抽象级别,并防止了重复的位置信息存储,否则可能会导致不一致。如果您只是讨厌额外的输入,则将其粘贴在方法中,这样您只需输入一次即可。如果您告诉它在哪里,则不必记住它在哪里。现在,作品只存储颜色,移动和图像/缩写之类的信息,这些信息始终是真实且不变的。
candied_orange

2

我认为,OOP引入主流的最有用的概念是:

  • 适当的模块化。
  • 数据封装。
  • 专用接口和公用接口之间的明确分隔。
  • 清晰的代码扩展机制。

所有这些好处也可以在没有传统实现细节的情况下实现,例如继承或类。艾伦·凯(Alan Kay)最初的“面向对象的系统”思想使用的是“消息”而不是“方法”,并且比例如C ++更接近Erlang。看一下Go,它取消了许多传统的OOP实现细节,但是仍然感觉很面向对象。

如果使用不可变对象,则仍然可以使用大多数传统的OOP优点:接口,动态分派,封装。您不需要设置器,通常甚至不需要更简单的对象的获取器。您还可以从不变性中受益:您始终可以确保对象在此期间没有发生变化,没有防御性复制,没有数据争用,并且很容易使方法变得纯净。

看看Scala如何尝试将不变性和FP方法与OOP结合起来。诚然,这不是最简单优雅的语言。不过,这实际上是相当成功的。另外,看看Kotlin,它为类似的混合提供了许多工具和方法。

尝试使用不同于N年前语言创建者所想到的方法的常见问题是与标准库的“阻抗不匹配”。OTOH Java和.NET生态系统目前都对不可变数据结构提供了合理的标准库支持。当然,也存在第三方库。

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.