19

面向对象设计 (OOD) 结合了数据及其方法。据我所知,这实现了两件伟大的事情:它提供了封装(所以我不关心有什么数据,只关心我如何获得我想要的值)和语义(它将数据与名称联系起来,以及它的方法始终如一地使用数据)。

那么OOD的实力在哪里呢?相比之下,函数式编程将丰富性归因于动词而不是名词,因此封装和语义都是由方法而不是数据结构提供的。

我使用的系统处于功能范围的末端,并且一直渴望 OO 的语义和封装。但我可以看到,OO 的封装可能成为对象灵活扩展的障碍。所以目前,我可以将语义视为更大的力量。

还是封装是所有有价值代码的关键?

编辑:我的意思是这里提供的封装 OO 的类型。changeColor(door,blue)变成door.changeColor(blue).

4

11 回答 11

29

您似乎使用了“封装”的相当狭窄的定义。假设您将封装定义为“将数据与方法相结合”,我是否正确?</p>

如果我错了,请忽略这篇文章的其余部分。

封装不是一个松散的术语;事实上,它是由国际标准化组织定义的。ISO 的开放分布式处理参考模型 - 定义了以下五个概念:

实体:任何具体或抽象的感兴趣的事物。

对象:实体的模型。一个对象的特征在于它的行为,双重地,它的状态。

(对象的)行为:行为的集合,对何时可能发生具有一组约束。

接口:对象行为的抽象,由该对象的交互的子集以及它们何时可能发生的一组约束组成。

封装:对象中包含的信息只能通过对象支持的接口的交互来访问的属性。

我们可以进一步提出一个不言而喻的建议:由于某些信息可以通过这些接口访问,因此某些信息必须在对象中隐藏和无法访问。此类信息表现出的属性称为信息隐藏,Parnas 通过认为模块应该被设计为隐藏困难的决策和可能改变的决策来定义,参见一篇伟大的计算论文:

http://www.cs.umd.edu/class/spring2003/cmsc838p/Design/criteria.pdf

重要的是要注意,不仅数据是信息隐藏的:它是与对象相关联的一些行为的子集,这些行为很难或可能会改变。

在您的帖子中,您似乎在说 OO 和函数式编程中的封装之间的区别源于数据管理,但至少根据 ISO 和 Parnas,数据管理不是封装的关键。所以我不明白为什么函数式编程中的封装需要与 OO 中的封装有任何不同。

此外,您在帖子中提到函数式编程提供封装,“……通过方法而不是数据结构。” 我认为,这是规模上的差异,而不是绝对的差异。如果我使用“对象”这个词而不是“数据结构”(如果我误解了,请再次告诉我),那么您似乎发现了 OO 的对象封装和函数式编程的方法封装的意义。

然而,根据上面的 ISO 定义,对象是我希望建模的任何东西。因此类可以被封装在一个包中,只要这些类中的一些对包的接口有贡献(即包的公共类)并且一些是信息隐藏的(包中的私有类)。

出于同样的原因,方法被封装在一个类中——一些方法是公共的,一些是私有的。您甚至可以将其降低一个档次,并说 McCabian 顺序代码序列封装在方法中。每个都形成一个封装在封装区域内的节点图;所有这些图形成一个图栈。因此函数式编程可以很好地封装在函数/文件级别,但这与OO的方法/类图没有区别,与OO的类/包图本质上也没有区别。

另外,请注意 Parnas 在上面使用的词:change。信息隐藏涉及潜在事件,例如未来困难设计决策的变化。你问OO的优势在哪里;好吧,封装当然是面向对象的强项,但问题就变成了,“封装的强项在哪里?” 答案非常明确:变革管理。特别是,封装减少了最大的潜在变化负担。

“潜在耦合”的概念在这里很有用。

“耦合”本身被定义为“通过从一个模块到另一个模块的连接建立的关联强度的度量”,在另一篇计算的伟大论文中:

http://www.research.ibm.com/journal/sj/382/stevens.pdf

正如论文所说,用从未有过的改进的话,“最小化模块之间的连接还可以最小化更改和错误传播到系统其他部分的路径,从而消除灾难性的“涟漪”效应,其中一个部分的变化会导致另一个错误,需要在其他地方进行额外的更改,从而产生新的错误,等等。”</p>

然而,正如这里所定义的,有两个限制可以很容易地解除。首先,耦合不能衡量模块内的连接,这些模块内的连接会产生与模块间连接一样多的“涟漪”效应(论文确实定义了“内聚”来关联模块内元素,但这不是根据定义耦合的元素之间的连接(即,对标签或地址的引用)来定义的)。其次,任何计算机程序的耦合都是给定的,因为模块是相互连接的;在耦合的定义中几乎没有管理 Parnas 所说的潜在变化的范围。

在某种程度上,这两个问题都通过潜在耦合的概念得到解决:程序的所有元素之间可形成的最大可能连接数。例如,在 Java 中,包中的包私有(默认访问器)类不能在其上形成连接(即,没有外部类可以依赖它,尽管有反射),但包中的公共类可以对它有依赖。即使目前没有其他类依赖它,这个公共类也会促成潜在的耦合——当设计发生变化时,类可能会依赖它。

要查看封装的强度,请考虑负担原则。负担原则有两种形式。

强形式表明,转换实体集合的负担是转换实体数量的函数。弱形式表明转换实体集合的最大潜在负担是转换实体的最大潜在数量的函数。

创建或修改任何软件系统的负担是创建或修改的类数量的函数(这里我们使用“类”,假设一个 OO 系统,并且关注类/包级别的封装;我们同样可以有关注函数式编程的函数/文件级别)。(请注意,“负担”是现代软件开发通常是成本或时间,或两者兼而有之。)依赖于特定修改类的类比不依赖于修改类的类更有可能受到影响.

修改后的类可以施加的最大潜在负担是对依赖它的所有类的影响。

因此,减少对已修改类的依赖关系会降低其更新影响其他类的可能性,从而减少该类可能施加的最大潜在负担。(这只不过是对“结构化设计”论文的重新陈述。)

因此,减少系统中所有类之间的最大潜在依赖关系数会降低对特定类的影响将导致对其他类进行更新的可能性,从而减少所有更新的最大潜在负担。

封装通过减少所有类之间的最大潜在依赖数,因此减轻了负担原则的弱形式。这一切都包含在“封装理论”中,它试图用数学方法证明这样的断言,使用潜在的耦合作为构建程序的逻辑手段。

但是请注意,当您问“封装是所有有价值代码的关键吗?”时,请注意。答案肯定是:不。所有有价值的代码都没有单一的关键。在某些情况下,封装只是一种帮助提高代码质量的工具,因此它可能变得“值得”。</p>

您还写道,“……封装可能成为对象灵活扩展的障碍。” 是的,它肯定可以:它确实被设计为阻止扩展难以或可能更改的对象的设计决策。然而,这并不是一件坏事。另一种方法是让所有类都公开,并让程序表达其最大的潜在耦合;但随后负担原则的弱形式表明更新将变得越来越昂贵;这些是衡量扩展障碍的成本。

最后,您在封装和语义之间进行了有趣的比较,在您看来,OO 的语义是其更大的优势。我也不是语义学家(在优秀的拉姆齐先生在他的评论中提到它之前,我什至不知道存在这样一个词)但我认为你的意思是“语义”,在“意义或解释一个词的含义”,并且非常基本的是,具有 woof() 方法的类应该称为 Dog。

这种语义确实有很大的力量。

令我好奇的是,您将语义与封装相提并论并寻找赢家;我怀疑你会找到一个。

在我看来,有两种力量推动封装:语义和逻辑。

语义封装仅仅意味着基于节点(使用通用术语)封装的含义的封装。所以如果我告诉你我有两个包,一个叫做“动物”,一个叫做“矿物”,然后给你三个类 Dog、Cat 和 Goat 并询问这些类应该封装到哪些包中,那么,给定没有其他信息,您完全正确地声称系统的语义表明这三个类被封装在“动物”包中,而不是“矿物”包中。

然而,封装的另一个动机是逻辑,特别是上面提到的潜在耦合的研究。封装理论实际上提供了用于封装多个类以最小化潜在耦合的包数量的方程式。

对我来说,封装作为一个整体是这种语义和逻辑方法之间的权衡:如果这使程序在语义上更易于理解,我将允许我的程序的潜在耦合高于最小值;但是巨大而浪费的潜在耦合级别将警告我的程序需要重新构建,无论它在语义上多么明显。

(如果优秀的拉姆齐先生还在阅读,你或你的语义学家朋友能否给我一个更好的词来形容我在这里使用的“语义”阶段?最好使用更合适的术语。)

问候,埃德。

于 2009-03-23T09:34:55.157 回答
9

封装和由此产生的抽象显然是 OO 的主要优势。“事物”表示可以对它们调用什么“动作”,因此名词比动词具有更高的语义重要性。

最终,如果没有某种程度的封装,很难设想以一致且可维护的形式设计一个复杂的系统。

于 2009-03-21T09:44:32.560 回答
5

作为一个 Lisp 程序员,他的对象系统可以说没有提供这些,我说:以上都没有。

jwz:“伪 Smalltalk 对象模型失败,通用函数(适当地受无外部覆盖规则约束)获胜”。

我认为您和其他人在此处列出的理想属性(封装、模块化等)并不像您想象的那样在 OO 中固有。它们通常与 Java 风格的 OO 一起提供,但不仅仅是它的结果。

于 2009-03-22T18:56:33.520 回答
4

隔离复杂性是 IMO 任何设计的主要目标:将功能封装在比功能本身更易于使用的接口后面。

OO 为此提供了各种机制 - 两个 oyu 提到:

封装允许设计一个独立于实际实现的自定义表面。(套用一句,“更简单意味着不同”)。

语义允许对表示问题域元素的实体进行建模,因此它们更容易理解。


任何达到一定规模的项目都会成为管理复杂性的练习。我敢打赌,多年来,编程已经超越了我们学会管理的复杂性的极限。

多年来我没有涉足函数式编程,但在我的理解中,最好用数学家对强大、优雅、和美丽这些词的含义来描述。在这种情况下,“美丽”和“优雅”试图描述对复杂系统的真实或相关结构的出色洞察力,从非常简单的角度看待它。它接受给定的复杂性,并尝试导航它。

根据我的理解,您提到的灵活性是根据您的需要更改 POV 的能力 - 但这与封装背道而驰:一个位置的无意义细节可能是另一个位置唯一相关的内容。

OO,OTOH,是还原论者的方法:我们通过提升到更高的水平来改变 POV。在“旧 OO”中,POV 有一个单一的层次结构,在这个模型中,接口是一种建模不同 POV 的方法。

如果我可以这样说,OO的力量更适合“普通人”。

于 2009-03-21T11:19:17.653 回答
4

某种形式的模块化是任何可扩展设计的关键。人类的局限性使人们无法一次“摸索”太多信息,因此我们必须将问题细分为易于管理的、有凝聚力的块,既为理解大型项目提供基础,也为细分工作分配提供了一种方法在许多人中的一个大项目。

如何选择一个大项目最有效的“划分”/“分区”来实现上述目标?经验表明,OO 是这里的大赢家,我想很多人会同意 OO 擅长这一点的两个关键属性是:

  • 封装:每个类都封装了一个“秘密”——一组特定于实现的假设——这些假设可能随着时间的推移而改变——并公开一个与这些假设无关的接口;将这种封装的抽象分层,可以构建一个健壮的设计,其中组件/实现可以在面对预期的变化时轻松交换。
  • 以名词为中心:在大多数领域中,人类似乎首先通过考虑领域的名词/数据来分解领域模型,然后识别与每个名词相关的支持动词。

关于函数式编程 (FP) 与 OO,我曾在博客中对此进行过讨论,但简要地说,我认为 FP 更多的是关于实现技术,而 OO 更多的是关于程序结构和设计,因此两者是互补的,而 OO 在规模的“大”端和 FP 在“小”端更占主导地位。也就是说,在大型项目中,高层结构最好由 OO 类设计来描述,但许多模块级细节(实现,以及模块接口形状的细节)最好由 FP 塑造影响。

于 2009-03-22T16:22:41.050 回答
3

面向对象设计的强度与设计中发生的后期绑定的数量成正比。这是面向对象的 Kay 概念,而不是 Nygaard 概念。艾伦凯写道

对我来说,OOP 仅意味着消息传递、本地保留、保护和隐藏状态过程,以及所有事物的极端后期绑定。它可以在 Smalltalk 和 LISP 中完成。可能还有其他系统可以做到这一点,但我不知道它们。

许多文献都忽略了后期绑定,而支持 C++ 面向对象的思想。

于 2009-04-01T02:20:17.310 回答
2

让我们退后一步,从更高的层面来看这个问题。任何语言功能的优点在于能够以更自然的方式就问题域简洁地表达问题/解决方案。

OOP 的机制很容易用带有结构和函数指针的普通 C 语言实现。你甚至可以通过这种方式获得一点 OOP 的感觉。然而,在这样的环境中,OOP 习语几乎不会出现。当对 OOP 有实际的语言支持时,范式的表达能力就会出现,语言实现想法的方式对“说”什么以及如何说有非常真实的影响。例如,查看在 lisp、python、ruby 等中使用闭包/lambdas 的代码差异。

因此,最终它不是关于组件和底层概念,而是它们如何组合和使用使 C++ 中的 OO 成为什么样子。

于 2009-04-02T18:26:22.373 回答
1

封装与多态性结合。大多数 OOP 语言中的类实现一个或多个接口的能力对我的软件开发产生了最大的影响。这个特性允许我精确定义两个对象之间的交互。

不仅要定义交互,还要记录它,以便多年后我可以回到那段代码并清楚地看到发生了什么。

这个特性是我更喜欢使用 OOP 语言而不是函数式语言的主要原因。虽然非常强大,但我发现用函数式语言编写的软件在维护周期以数十年为单位时维护起来很痛苦。(AutoCAD 中的 AutoLisp 软件)

于 2009-04-03T19:11:50.633 回答
1

恕我直言,OO 仅表示与其他对象交互的对象。封装只是意味着抽象一个概念。因此,您创建了一个 Socket 和 .Connect() 到某个东西。它是如何连接的,你并不关心(这基本上是我对封装的定义)。

而且,纯函数式编程可以使用对象进行通信。但这些对象需要是不可变的。因此,恕我直言,FP 可以轻松使用 OO 概念;命令式语言(例如 C)仍然可以使用 OO 的概念。例如,每个“类”的文件都有一个不应使用的私有部分。

于 2009-04-06T03:58:25.733 回答
0

您的问题类似于您想通过分析砖块来获得房屋的好处。

能够提供语义上下文和封装只是面向对象中类的基本能力。(就像一块砖可以承受一定的力量并占据一定的空间。)

继续类比:为了最大限度地利用积木,只需将它们放在一起即可。这同样适用于类和对象。

很多设计模式可用于 OO 编程。它们中的大多数依赖于您提到的“封装”和“语义”能力。

其中一些模式甚至是您问题第三段的答案:

  • 如果要扩展现有类的行为,可以创建派生类。
  • 如果您想扩展或更改现有对象的行为,您可以考虑使用装饰器模式
于 2009-04-05T15:55:38.903 回答
0

OO 的真正威力在于多态性而不是封装。封装在一定程度上是可以实现的,并且用在函数式语言中,但是如果用函数式语言实现,多态会很尴尬。

(阅读四人组的“设计模式”以了解 OO 的力量。)

@Phil,如果我理解正确,您提到的区别在于程序调用数据/方法的方式之间:在oo中,首先有一个对象/实例,然后通过对象调用对象的数据/方法; 在函数式中,直接调用方法。

但是,查看函数式程序的实现,我们看到数据和方法被包装(在文件中而不是在类中)。例如,一个 C 程序的头文件中声明了其他文件可以访问的函数,如果只能通过这些声明的函数访问,则该数据是私有数据。只要程序员足够细心,OO中的大部分封装都可以在函数式程序中实现。(甚至可以通过使用某些指针技巧来实现继承。)

于 2010-05-29T00:43:04.520 回答