我们真的需要OO语言来管理软件复杂性吗?


209

这将是一个非技术性的软性问题,我不确定这是否是正确的平台。但是我是CS的初学者,所以我希望你们能容忍它。

在第一学期中,我们通过Java和UML介绍了OOP概念,例如封装,数据隐藏,模块化,继承等。(Java是我的第一门编程语言)

以我的理解,OOP是一种管理软件复杂性的方法。但是它的原理并不是新的或独特的,它们在某种意义上在所有工程领域都是通用的。

例如,汽车是一种非常复杂的结构,其复杂性由具有明确定义的行为和接口的模块化和封装组件的层次结构来管理。

但是我不明白引入新的编程范例的原因。我认为,用于管理复杂性的所有原理都可以通过过程编程语言来实现。例如,对于模块化,我们可以将程序分为许多小程序,这些小程序执行定义明确的任务,其代码包含在单独的文件中。这些程序将通过其定义明确的输入和输出相互交互。可以对文件进行保护(加密?)以实现封装。为了重新使用代码,我们只要在新程序中需要它们时就可以调用它们。这不是捕获所有的OOP还是我缺少非常明显的东西?

我不是要证明OOP可以管理复杂性。我认为确实可以。但是我认为,用于处理复杂性的所有原理(例如模块化,封装,数据隐藏等)都可以很容易地由过程语言实现。那么,如果我们不进行复杂性管理,为什么要真正实现面向对象设计呢?


41
您缺少接口和实现的分离。能够在运行时将一个实现换成另一个实现是非常重要的功能。基本上,这是通过带有继承的OO语言的动态方法分派来实现的。过程语言也可以做到这一点(阅读:空指针),但没有类型安全性。
marstato

81
很大程度上,面向对象的语言和设计的思想恰恰是使那些通用的,直观的概念尽可能容易地在代码中表示和重新创建。如果您有一个计划或一套准则来指导如何在没有固有的面向对象语言的情况下实现所有这些目标,那么有关如何做事情的建议将有效地成为所使用的面向对象的方法。实际的OO语言只是将其形式化和简化的一种方法。
站在

14
@RobbieDee你真的读过我的问题吗?通过质疑天气可以不使用OO来管理软件复杂性,就可以从更基本的角度理解OO。我不是要破坏OO,不是要发明新东西,我只是想更好地理解它,如果问题如此“不言而喻”,为什么它得到了Jorg的好评?
Steakhouseexchange

12
加密文件不是封装。您可能已经看不清其他开发人员的代码内容,但是不一定保护代码的内部工作不受其他代码的影响。如果原始作者可以回忆起如何做,则可以在加密之前或之后进行此操作。
JeffO

8
除了机器语言,您不需要任何其他东西-让程序员记住操作码并自己编写一个零。但是,在减少错误和提高生产率方面,使用某种“符号”语言非常有用,并且,正如Dijkstra所观察到的那样,强加某种“结构”(或至少使“结构”的维护更加容易)的语言会大有帮助。考虑到当前的语言复杂程度,OO语言可能不是理想的技术,但对于许多应用程序来说它们还是不错的。这个想法是在不妨碍您的情况下管理复杂性。
Daniel R Hicks's

Answers:


178

让我尝试一个非常低的理论答案:)

您真正要问的是:当可以使用过程语言来设计和编写OO代码时,为什么要直接在该语言中包括对对象定向(OO)的支持?

答案是:要有一个关于OO在源代码中如何表达的标准,这样您就不会在22种不同的实现中获得相同的抽象。

例如,假设我创建了一个MagicButton和一个MagicSlider可以在用户界面系统中使用的。我需要一种方法来对可以与MagicButton一起使用的方法,只能与MagicSlider一起使用的方法以及两者都可以使用的方法进行分组。这些对象共享某些方法,因为它们都是Magic gui对象。

我可以通过以特殊的方式命名函数来进行分组,方法MagicSlider_DoSomething ...是将方法包括在以特殊方式命名的特定文件中MagicSliderMethods.XXX,或者我可以找到其他一些特殊方式来做同样的事情。如果没有标准的语言方式可以做到这一点,那么我将与您有所不同,并且与其他任何人也有所不同。这使得共享代码更加困难。

是的,可以使用过程语言来实现后期分发(OO语言中的虚拟方法),但是有很多不同的方法来实现它。根据谁编写代码,您最终将在同一程序中使用不同的OO实现。

考虑一下可怜的维护开发人员。该人员必须管理不同的对象抽象和不同的方法来调用虚拟方法,具体取决于谁编写原始代码。

另外:使用该语言的抽象可以使高级代码编辑器(例如Eclipse)对代码进行大量静态分析。例如,Eclipse可以提供可用于对象的所有方法的列表,以及自动实现空的“ TODO方法”。Eclispe根据扩展的类和实现的接口确切地知道您的类必须实现的方法。如果没有用于OO的语言标准,这几乎是不可能的。


40
经典示例:Lua。它不是本地的OO,而是可以实现的,但这意味着大约有5个不同的,同样众所周知的OO库,它们并不是完全可以互操作的。
Kroltan's

55
@steakexchange您过多地关注绝对值。很少有“唯一目的”。语言都会做很多不同的事情以达到不同的质量。选择一种语言是在选择一组权衡因素,使其最适合您所需的目的。
Tim B

42
@nocomprende标准化抽象几乎就是编程语言的用途。甚至汇编语言也可以抽象化十年半中十多个硬件世代之间的差异。
David Moles

56
@DavidMoles标准化的抽象 从字面上什么编程语言。不要浪费一个绝好的机会来按字面意义使用“按字面意思”!
克莱门特·谢林

12
可以对此进行标准化。上世纪90年代中期,当我在大学时,我在X-Windows上做了相当多的工作(主要是基于Motif的,用于那些记得这类事情的人)。X-Windows确实确实允许您在常规C中实现面向对象的所有功能。不过,这样做的精神体操相当充实,并且严重依赖那些没有在盒子里面看的人(此时,施罗丁格的《小部件代码》通常以死亡告终)。OO语言与常规编译器对汇编器的作用方式相同,对编码器而言是隐藏起来的,并且生活更轻松。
Graham

212

在第一学期中,我们通过Java和UML介绍了OOP概念,例如封装,数据隐藏,模块化,继承等。(Java是我的第一门编程语言)

这些都不是面向对象的概念。它们都存在于OO之外,独立于OO,甚至有很多是在OO之前发明的。

所以,如果你认为是什么OO是一回事,那么你的结论是正确的:你可以做所有这些过程中的语言,因为他们什么都没有做OO

例如,关于模块化的开创性论文之一是关于将系统分解为模块的准则。那里没有提及OO。(它写于1972年,那时OO虽然已经有十多年的历史了,但它仍然是一个晦涩的小众市场。)

尽管数据抽象在OO中很重要,但它更多是OO(消息传递)主要功能的结果,而不是定义功能。另外,记住有不同种类的数据抽象也很重要。当今使用的两种最常见的数据抽象类型(如果我们忽略“什么也没有抽象”(可能仍比其他两种组合使用的更多))是抽象数据类型对象。因此,仅需说“信息隐藏”,“封装”和“数据抽象”,您就对OO没说什么,因为OO只是数据抽象的一种形式,而实际上这两种形式根本不同:

  • 对于抽象数据类型,抽象的机制是类型系统。隐藏实现的是类型系统。(类型系统不必一定是静态的。)使用对象时,实现隐藏在过程接口的后面,该过程不需要类型。(例如,可以使用闭包来实现,就像在ECMAScript中一样。)
  • 使用抽象数据类型,不同ADT的实例彼此封装在一起,但是同一 ADT的实例可以检查和访问彼此的表示形式和私有实现。对象总是封装在所有内容中。只有对象本身可以检查其自己的表示形式并访问其自己的私有实现。没有其他对象,甚至没有相同类型的其他对象,相同类的其他实例,具有相同原型的其他对象,该对象的克隆或任何可以执行的操作。

顺便说一下,这意味着在Java中,类不是面向对象的。同一类的两个实例可以访问彼此的表示形式和私有实现。因此,类的实例不是对象,它们实际上是ADT实例。interface但是,Java 确实提供了面向对象的数据抽象。因此,换句话说:在Java中,只有接口的实例才是对象,而类的实例则不是。

基本上,对于类型,只能使用接口。这意味着方法和构造函数的参数类型,方法的返回类型,实例字段,静态字段和局部字段的类型,instanceof运算符或强制转换运算符的参数以及泛型类型构造函数的类型参数必须始终为接口。一个类只能在new运算符之后直接使用,而不能在其他地方使用。

例如,对于模块化,我们可以将程序分为许多小程序,这些小程序执行定义明确的任务,其代码包含在单独的文件中。这些程序将通过其定义明确的输入和输出相互交互。可以对文件进行保护(加密?)以实现封装。为了重新使用代码,我们只要在新程序中需要它们时就可以调用它们。这不是捕获所有的OOP还是我缺少非常明显的东西?

您所描述的 OO。

这确实是考虑面向对象的好方法。实际上,这几乎就是OO的原始发明者所想到的。(Alan Kay进一步走了一步:他设想了许多小型计算机通过网络相互发送消息。)您所说的“程序”通常称为“对象”,而通常所说的“发送”而不是“发送消息” ”。

面向对象是关于消息传递(也称为动态调度)的。“面向对象”一词是由Smalltalk的首席设计师Alan Kay博士创造的,他这样定义

对我而言,OOP意味着仅消息传递,本地保留和保护以及状态过程的隐藏以及所有事物的极端后期绑定。

让我们分解一下:

  • 消息传递(如果您不熟悉Smalltalk,则为“虚拟方法分派”)
  • 状态过程应该是
    • 当地保留
    • 受保护的
  • 万物的极端后期绑定

实现明智的,消息是后期绑定过程调用,如果过程调用后期绑定,那么你就不能在设计时知道是什么,你要调用,所以你不能对状态的具体表现做任何假设。因此,实际上是关于消息传递的,后期绑定是消息传递的一种实现,封装是它的结果。

后来他澄清了“ 大想法是'消息传递' ”,并遗憾地称其为“面向对象”而不是“面向消息”,因为术语“面向对象”将重点放在不重要的事物上(对象)并分散真正重要的内容(消息传递):

轻轻提醒一下,我在上一次OOPSLA上付出了一些努力,试图提醒大家,Smalltalk不仅是其语法或类库,甚至与类无关。很抱歉,我很久以前为该主题创造了“对象”一词,因为它使许多人专注于较小的想法。

最大的想法是“消息传递”-这就是Smalltalk / Squeak的核心所在(这在我们的Xerox PARC阶段中从未完全完成)。日语有一个小字-ma-表示“介于两者之间的那个”-也许最接近的英语等效词是“ interstitial”。制作出色且可扩展的系统的关键在于设计模块的通信方式,而不是设计其内部属性和行为。想想互联网-要生存,它(a)必须允许超出任何单一标准的许多不同种类的想法和实现,并且(b)允许这些想法之间具有不同程度的安全互操作性。

(当然,今天,大多数人甚至不专注于对象,而是专注于类,这是错误的。)

消息是根本,以面向对象,无论是作为隐喻和作为一种机制。

如果您向某人发送消息,您将不知道他们如何处理该消息。在只有你可以观察到的东西,是他们的反应。您不知道他们是否自己处理了消息(即对象是否具有方法),是否将消息转发给其他人(委托/代理),甚至他们是否理解。这就是封装的全部内容,这就是OO的全部内容。只要代理响应您的期望,您甚至都无法将其与真实事物区分开。

“消息传递”的一个更“现代”的术语是“动态方法分派”或“虚拟方法调用”,但它失去了隐喻,而专注于该机制。

因此,有两种方法可以查看Alan Kay的定义:如果单独查看它,您可能会发现消息传递基本上是一个后期绑定过程调用,而后期绑定则意味着封装,因此我们可以得出结论:#1和#2实际上是多余的,并且OO都是关于后期绑定的。

但是,他后来澄清了重要的事情是消息传递,因此我们可以从另一个角度看待它:消息传递是后期绑定。现在,如果消息传递是唯一可能的事情,那么#3将是微不足道的:如果只有一件事,并且该事物是后期绑定的,那么所有事物都是后期绑定的。再一次,封装来自消息传递。

威廉·R·库克William R. Cook)重新审视的《关于理解数据抽象》以及他关于“对象”和“面向对象”的简化现代定义的提案中也提出了类似的观点:

动态分配操作是对象的基本特征。这意味着要调用的操作是对象本身的动态属性。操作不能被静态地识别,并且通常没有办法确切地知道响应给定请求将执行什么操作,除非通过运行它。这与始终动态分配的一流函数完全相同。

在Smalltalk-72中,甚至没有任何物体!有是得到了分析,重写和重新路由信息流。首先出现的是方法(解析和重新路由消息流的标准方法),后来出现的是对象(共享某些私有状态的方法组)。继承来得晚了很多,而类仅仅是作为支持继承的方式引入的。如果Kay的研究小组已经了解原型,那么他们可能根本不会引入类。

本杰明·皮尔斯(Benjamin Pierce)在类型和编程语言中提出,面向对象的定义特征是开放递归

因此:根据Alan Kay的说法,面向对象就是消息传递。根据William Cook所说,面向对象是关于动态方法分配的(实际上是同一回事)。根据本杰明·皮尔斯(Benjamin Pierce)的观点,面向对象是关于开放递归的,这基本上意味着自我引用是动态解析的(或者至少是一种思考的方式),或者换句话说,消息传递。

正如您所看到的,创造“ OO”一词的人对对象有一种形而上的形而上学的观点,Cook则具有相当务实的观点,而Pierce则具有非常严格的数学观点。但是重要的是:哲学家,实用主义者和理论家都同意!消息传递是OO的支柱之一。期。

注意这里没有提到继承!对于OO而言,继承不是必不可少的。通常,大多数OO语言都具有某种实现重用的方法,但不一定必须是继承。例如,它也可以是某种形式的委托。实际上,《奥兰多条约》讨论了委托作为继承的替代方法,以及不同形式的委托和继承如何在面向对象语言的设计空间内导致不同的设计要点。(请注意,实际上,即使在支持继承的语言(如Java)中,人们实际上也被告知要避免使用继承,这再次表明对OO而言是不必要的。)


16
+100-我们将内在的责任归咎于事物,因为它们的使用不当。
JeffO

55
抱歉,这是如此令人难以置信的错误。艾伦·凯(Alan Kay)可能想出了这个用语,但原理出在Smalltalk之前。面向对象的编程源于Simula,其OO风格与“消息”无关。几乎每一种成功的OO语言都基于Simula提出的基本原理(我们在Java中也是如此),并且每次重新引入Smalltalk风格的OO都是“思想市场”的失败,因为它根本无法很好地工作。名称是Kay所做的唯一真正有意义的事情。
梅森惠勒

23
@steakexchange否。“ OO的本质”是具有虚拟方法的对象,它使之真正与众不同。没有人使用Smalltalk是有原因的:消息传递系统在个人计算机规模上运行非常差。每当有好主意但天真的语言设计师尝试重新实现Smalltalk原理时,它都会失败。(最近的例子是Objective-C,如果史蒂夫·乔布斯(Steve Jobs)没把它推到整个iOS社区的喉咙,没人会用过。它从未在Apple生态系统之外找到吸引力,这是有原因的。 )
梅森·惠勒

28
@MasonWheeler您是否可以在回答中详细说明您的评论,因为您与Jorg的说法截然相反?
Steakhouseexchange

20
还值得一提的是,面向对象语言的概念发展了很多。如今,由于许多语言放弃了旧模型并采用了多范式,因此这些祖先的概念可能不那么正确。例如,以C#为例-这种语言一次混合了几乎所有在阳光下的东西,并且虽然通常被称为OO语言,但实际上是不同范例的混合。这使它成为周围开发人员真正具有表现力的工具。同样,基于类的OO是许多同样有效的OO编程形式之一。
T. Sar

66

但是我认为,用于处理复杂性的所有原理(例如模块化,封装,数据隐藏等)都可以很容易地由过程语言实现。

当您说“非常容易”时,您的发言就非常大胆。我的阅读方式是:“我看不到困难,所以它一定不会太大。” 当用这种方式表达时,很明显您不是在问“我们为什么需要OO”,而是在问“为什么其他编程范例遇到的导致OO发明的困难对我来说不是立即显而易见的? ”

这个问题的答案是,您正在开发的程序中并不存在许多此类困难。您无需更新40岁的意大利面条代码。您没有尝试为操作系统编写新的显示管理器。您不是在调试多线程分布式应用程序。

对于我们CS学生负责编写的许多玩具程序,我们也可以像Java或Python一样以BASIC或汇编语言编写它们。这是因为任务的内在复杂性非常低,只有一名开发人员,没有遗留的互操作性问题,性能也没有关系,并且代码可能只会在一台机器上运行几次。

想象一下,带一个学生司机,并要求他们在高峰时间合并在一条繁忙的街道上,没有同步的手动变速箱,驶向陡峭的山坡。灾害。为什么?他们无法管理同时遵循任务要求的所有规则所需的复杂程度。

现在想象同一位学生,同一辆汽车,在空旷的停车场中以步行的速度行驶。他们没问题,因为他们的技能水平足以胜任这项任务。没有压力,风险很小,它们可以承担启动,抓紧,换挡,加速,转向的单个子任务。

那位学生可能会问,如果熟练的驾驶员可以同时完成所有这些事情,为什么我们要有自动变速箱?答案是,熟练的驾驶员在最佳条件下不需要自动挡。但是我们并不是所有人都处于巅峰状态,所以我们通常希望让汽车的设计师为我们解决所有复杂性带来的便利。

熟练,训练有素的程序员确实可以用C或汇编语言创建一个运行正常的高复杂度系统。但是我们并不都是莱纳斯·托瓦尔兹。创建有用的软件也不必如此。

我个人甚至在解决当前问题之前都没有必要重新发明现代语言的所有功能。如果我可以利用包含已解决问题的解决方案的语言,那我为什么不呢?

因此,我将转而回答您的问题,并问您,如果语言提供便捷的功能(如封装和多态性),我们为什么不应该使用它们呢?


13
因此,基本上可以使用一种过程语言来进行OO,但这是在使用标准化的OO语言实现自动化和简化的同时手动进行的事情。
Steakhouseexchange

6
@steakexchange差不多就是这个。
Tim B

3
@steakexchange的一个很好的历史例子是X Windows的对象模型。它是用C语言编写的,但是基于面向对象的系统。因此,您必须遵循某些约定才能与之交互,这样您的类才能与其他所有人一起玩。
Ukko

7
@nocomprende当然。但有可能使自己的电脑做原始磁盘写入,而不是依赖于文件系统无法启动,而且是调试的问题确有困难的由一个新手建立了一个临时的对象系统。正确完成封装后,封装将使我们避免有意或无意地干预我们不应该做的事情。
克莱门特·谢林

3
对我来说很有趣的是,您给出的从OO中受益的应用程序示例通常不是用OO语言编写的。40岁的意大利面条可能用C,COBOL,FORTRAN或REXX编写。显示管理器很可能是用C写的(虽然OO-ISH公约),和许多成功的分布式多线程系统都用Erlang编写的,去或C.
James_pic

22

您所描述的不是OOP,而是抽象。在所有现代设计模型中都存在抽象,即使不是OOP的模型也是如此。OOP是一种非常特殊的抽象。

首先,值得注意的是,没有对OOP的单一定义,因此可能有些人不同意我所描述的OOP。

其次,必须记住,OOP受传统设计模型的启发,因此与汽车设计的相似之处并非巧合。

但是,OOP比您所说的有些细微之处:

  • 封装:这不仅涉及为模块设置接口(即抽象),而且还禁止对该接口的访问。在Java中,访问私有变量是编译错误,而在汽车设计中,您可以(在某些情况下)以与预期接口不同的方式使用事物。

  • 继承:这确实是使OOP独一无二的东西。定义接口后,您可以使该接口实现多种功能,并且可以采用继承的方式进行,更改其实现的特定部分,同时继承所有先前的部分,从而大大减少了代码重复。

    如果您从汽车的封装组件的角度来考虑,那么实际上并没有什么等效的。我没有办法采用其他手段并更改其实现的特定部分来适应。(至少我不这么认为,我对汽车不太了解)。

  • 多态性:定义接口后,从可用操作的角度来看,使用该接口的任何内容都应该是无法区分的,并且您无需知道使用什么实现来使用该接口。在这里,子类型化和Liskov替代原理变得很重要。

  • 耦合:OOP的一个关键方面是我们将事物与相同的操作紧密联系在一起,并展开它们可以具有的不同形式。数据与对该数据的操作捆绑在一起。这意味着添加新的数据形式(新的实现)非常容易,但是向接口添加新的操作则非常困难(因为您必须更新实现该接口的每个类)。这与功能语言中的代数数据类型相反,后者很容易添加新的操作(您只需编写一个处理所有情况的函数),但很难添加新的变体(因为您需要添加新的所有功能的大小写)。


1
好答案!我不同意的一部分:关于封装的区别是无效的。封装总是意味着“禁止访问此接口” –对于OOP和非OOP设置都是如此。因此,这部分并不是OOP特有的。
DW

@DW我试图澄清这一点,并不是说它对OOP来说是唯一的,而是封装和抽象之间的区别。感谢您的反馈!
jmite

2
好。但是,对于此处关于该主题的内容,我仍然持有不同的看法。您写道:“ OOP在某些方面比您所说的更加细微差别”,但是封装并不是OOP比问题中所写的更加细微差别。封装就是任何范式。在您写到“您所描述的不是OOP,而是抽象”的地方,我认为最初的问题是试图描述封装(而不仅仅是抽象)。我想我将以不同的观点发表此评论。我认为答案非常有帮助!
DW

继承是一个常见的功能,但是几种重要的OO语言缺少它。
Bradd Szonye

很好的答案,但是IMO夸大了汽车示例。给定模型的引擎具有明确定义的接口(凸轮轴,安装支架“插座”等)。您可以将普通的老式化油器替换为喷油的,更换涡轮增压器等,而不影响变速箱。(尽管柴油发动机确实需要改装的燃油箱IIRC。)相反,您可以将手动变速箱替换为完全不影响发动机的自动变速箱和AFAIK。
大卫

11

我们真的需要OO语言来管理软件复杂性吗?

这取决于“需要”一词的含义。

如果“需要”表示需要,则不,我们不需要。

如果“需要”的意思是“提供巨大利益”,那么我会说“是”,我们希望得到它。

大图

OO语言将功能绑定到数据。

您可以避免这种绑定,并编写传递数据值的函数。

但是随后您可能会发现组合在一起的数据星座,然后开始遍历元组,记录或数据字典。

实际上,所有方法调用都是:绑定数据集上的部分函数。

逐个特征

OOP的特点:

  • 继承允许代码(混合)和概念(抽象基类/接口)的重用-但是您可以通过在子作用域中重新定义函数和变量来实现。
  • 封装允许隐藏信息,因此我们可以在更高的抽象级别上工作-但您可以使用头文件,函数和模块来实现。
  • 多态性允许我们使用不同类型的参数,只要这些参数支持相同的接口即可,但是我们也可以使用函数来实现。

但是,这些事情都没有像使用具有这些功能的一流支持的面向对象语言那样容易地发生。

参考文献

对OOP有很多评论家

但是,研究似乎表明,通过OOP重用代码可以提高程序员的生产率。这是一个有争议的发现,一些研究人员说,鉴于某些限制,他们无法重现这些生产率的提高。(资源)

结论

我们不需要“ OOP”。但在某些情况下,用户想要 OOP。

我的理解是,成熟的程序员在面向对象的风格上可以非常有生产力。而且,当程序包的核心对象具有易于理解的简单接口时,即使是新程序员也可以迅速提高工作效率。


10

我会尽量简短。

OO的核心原则是将数据和行为结合在一个组织单位(一个对象)中。

这就是使我们能够控制复杂性的原因,它在出现时是一个非常新颖的概念。将其一方面与文件(纯数据)进行比较,另一方面将程序读取和处理这些文件(纯逻辑)并输出(再次进行纯数据)。

只有将数据和逻辑包打包在一起,对一些真实世界的实体进行建模之后,您才可以开始交换消息,创建子类,将私有数据与公共数据和行为分开,实现多态行为,以及所有这些面向对象的魔术。

因此,是的,OO很重要。不,这不只是一堆名字花哨的旧东西。

将所有内容拆开,看一下元素,然后说:“哦,好吧,这里没有我以前从未见过的东西”,这并不是在承认具有创新精神的组件。结果超过了各个部分的总和。


8

没有面向对象程序设计的“正式”定义,并且理性的人在实际定义OO质量上存在分歧。有人说消息传递,有人说子类型化,有人说继承,有人说数据和行为的捆绑。这并不意味着该术语是没有意义的,只是您不应该为真正的 OO是什么而过于纠结。

封装和模块化是设计的更基本原理,应在所有编程范例中应用。OO的支持者并不声称只能通过OO才能实现这些特性-只是OO特别适合于实现这一目标。当然,其他范式的支持者,例如说函数式编程,也对它们的范式主张相同。在实践中,许多成功的语言都是多范例的,并且应该将OO,功能等视为工具,而不是“一种唯一的方式”。

我认为,用于管理复杂性的所有原理都可以通过过程编程语言来实现。

没错,因为最终您可以使用任何编程语言执行任何操作。某些语言可能比其他语言更容易,因为所有语言都有各自的优缺点。


7

其他答案未提及:状态。

您将OO作为管理复杂性的工具。有什么复杂性?这是一个模糊的术语。我们都对它的含义有完形的理解,但是很难将其固定下来。我们可以测量循环复杂性,即通过代码的运行时路径的数量,但是我不知道这就是我们使用OO管理复杂性时要讲的内容。

认为我们正在谈论的是与状态相关的复杂性。

封装背后有两个主要思想。其中一个,实现细节的隐藏,在其他答案中很清楚。但是另一个隐藏了它的运行时状态。我们不会随意处理对象的内部数据。我们传递消息(或调用方法,如果您更喜欢实现细节而不是概念,如JörgMittag指出的那样)。为什么?

人们已经提到过,这是因为您不能更改数据的内部结构而不更改访问代码,而您希望在一个位置(访问器方法)而不是300个位置中进行操作。

但这也是因为它使代码难以推理:过程代码(无论是本质上是过程语言还是简单地以这种风格编写的语言)对于限制状态突变都没有什么帮助。任何事物都可以随时随地发生变化。调用函数/方法可能会产生诡异的动作。自动化测试更加困难,因为测试的成功取决于广泛访问/可访问的非局部变量的值。

其他两个大型编程范例(OO和函数)为状态相关的复杂性问题提供了有趣但几乎完全相反的解决方案。在函数式编程中,人们试图完全避免使用它:函数通常是纯函数,对数据结构的操作将返回副本,而不是就地更新原始副本,等等。

另一方面,OO提供了用于处理状态管理的工具(而不是用于避免状态管理的工具)。除了语言级别的工具(例如访问修饰符(受保护的/公共/私有的),getter和setter等)外,还有许多相关的约定,例如Demeter法则,建议不要通过对象来获取其他对象数据。

请注意,您实际上不需要对象即可执行任何操作:您可以有一个包含无法访问的数据的闭包,并返回一个函数的数据结构来对其进行操作。但这不是一个对象吗?从直觉上讲,这不符合我们对物体的概念吗?而且,如果我们有这个概念,那么用语言重新对其进行验证是否比依赖其他竞争性即席实现的爆炸式增长更好(如其他答案所述)?


5

我们真的需要OO语言来管理软件复杂性吗?

不。 但是他们可以在许多情况下提供帮助。

我使用单一的OO语言已经有几十年了,但是我的大部分代码实际上都是严格的OO风格的程序。但是,对于涉及GUI的任何事情,我都使用该语言的庞大的OO内置方法和对象库,因为它极大地简化了我的代码。

例如,使用原始低级Windows API来显示表单,按钮和编辑字段的Windows应用程序需要大量代码,而使用Visual Basic或C#或Delphi附带的对象库可以使它们相同程序小而琐碎。因此,我的面向对象的代码通常相对较小,并且对于GUI而言,而那些对象调用的代码通常要大得多,并且通常与面向对象无关(尽管它可以根据我要解决的问题而有所不同)。

我看到过OO程序过于复杂,依赖于复杂的关于对象如何实现的深奥规则,如果没有OO概念编写的话,它可能会简单得多。我也看到了相反的情况:复杂的系统迫切需要通过使用对象来重新实现和简化。

随着经验的积累,您会发现不同的情况需要使用不同的工具和解决方案,并且一种尺寸无法适应所有情况。


3

作为涉及完全用C编写的大型项目的人,我可以肯定地说答案是明确的“否”。

模块化很重要。但是模块化几乎可以用任何体面的语言实现。例如,C支持模块化编译,头文件和结构类型。这足以满足99%的情况。为所需的每种新抽象数据类型定义一个模块,并定义用于对该数据类型进行操作的功能。有时,您需要性能,而这些函数作为内联函数放在头文件中,而其他时候,您将使用标准函数。选择哪种方式对用户都是不可见的。

结构支撑成分。例如,您可以拥有一个由互斥锁和常规哈希表组成的锁定哈希表。这不是面向对象的编程。没有子类化。组合工具比面向对象编程的思想要古老得多。

对于只有1%的情况,编译级别的模块性还不够,并且您需要运行时模块性,这就是所谓的函数指针。它们允许具有明确定义的接口的单独实现。请注意,这不是非面向对象语言的面向对象编程。这是定义一个接口,然后实现它。例如,此处不使用子类化。

考虑一下其中最复杂的开源项目。即,Linux内核。它完全用C语言编写。它主要通过使用标准的编译级模块化工具(包括组合)来完成,然后偶尔在需要运行时模块化时,使用函数指针来定义和实现接口。

如果您尝试在Linux内核中找到面向对象编程的示例,那么我肯定很难找到这样的示例,除非您将面向对象编程扩展为包括诸如“定义接口然后实现”之类的标准任务。

请注意,如果您确实需要,甚至C编程语言都支持面向对象的编程。例如,考虑GTK图形用户界面工具包。它实际上是面向对象的,尽管是以非面向对象的语言编写的。因此,这表明您需要一种“面向对象的语言”的想法存在严重缺陷。面向对象的语言没有其他语言无法做到的。此外,如果您是专业的程序员,就会知道如何轻松地以任何语言编写面向对象的代码。例如,使用C并不是负担。

因此,结论是,面向对象的语言可能仅对不了解该概念实际实现方式的新手程序员有用。但是,我不想靠近程序员是这样的新手程序员的任何项目。


1
“结论是,面向对象的语言可能仅对不了解该概念实际实现方式的新手程序员有用。” 有趣。您想使用什么语言?您是否有用这些语言编写的开源项目失败或没有失败的示例?
文森特·萨瓦德

3
您提出了一些好的观点,但是您的主要想法存在缺陷。是的,可以使用诸如C之类的语言来实现OO概念,例如封装,虚拟分派,继承甚至是垃圾回收。也可以在汇编中实现。它不会使编程变得容易。用C之类的语言进行编程,尤其是设计,绝对比使用OO语言要困难得多。在C语言中,您需要将概念映射到其实现,而无需使用OO语言(至少对于OO概念而言)。
fishinear

1
“ [函数指针]允许单独使用一个定义良好的接口。请注意,这不是用非面向对象的语言进行的面向对象的编程。这是定义一个接口然后加以实现。” 抱歉,但这是完全错误的,因为这正是 OOP。“例如,此处不使用子类化”子类化不是OOP的必需功能。请注意,例如,JavaScript是一种面向对象的语言,不具有子类化(就此而言,根本就没有类……只是包含函数引用的对象)。
Jules

1
为了澄清我的最后一句话,我的观点是,OOP(不一定是任何特定的OO语言)与其他方法之间的主要区别是,在OOP中,我们定义了对数据进行抽象操作的接口,而无需了解该数据的格式。将接口的实现绑定到数据本身。那就是OOP。实现的方法无关紧要,无论是Java样式类,JavaScript对象(实际上是名称到属性的映射,可以是数据还是代码)还是包含函数指针和数据的void *的结构。
Jules

1
“因此,结论是,面向对象的语言可能仅对不了解该概念实际实现方式的新手程序员有用。” ....坦率地说,这是很侮辱性的。我非常了解如何实现这些概念。我已经做了足够的工作,然后才能精通。过去,我甚至为OO语言实现了解释器和编译器。但是因为我更喜欢使用具有一流对象的高级语言来工作,所以我必须是一个新手程序员,而您不希望与之合作?
Jules

2

引入编程范例(包括面向对象的方法)的原因是使制作更复杂和功能更强大的程序变得更加容易。在1981年8月发行的Byte Magazine中,Smalltalk的主要创建者之一Daniel Ingalls将“面向对象”定义为具有以下功能:

  • 自动存储管理
  • 交换消息的能力
  • 适用于语言所有操作的统一隐喻
  • 没有组件依赖于另一个组件的内部(模块化)
  • 程序仅定义对象的行为,而不定义它们的表示(多态)
  • 每个组件应仅出现在一个位置(分解)
  • 使用独立于硬件的虚拟机
  • 每个用户可访问的组件都应可用于观察和控制(反应原理)
  • 应该没有整体控制器(没有操作系统)

这些就是Ingalls确定为Xerox Parc Research开发的SmallTalk-80驾驶设计考虑因素的原则。在前面提到的杂志文章中,您可以阅读这些原理中的每一个的详细说明,以及它们如何根据Ingalls有助于面向对象的范例。

所有这些原理都可以使用任何图灵完整的语言来应用,无论是程序语言,汇编语言还是其他任何语言。这些是设计原则,而不是语言规范。面向对象的语言旨在使创建软件时更容易使用这些原理。

例如,要采用Ingall的第一项原则(自动存储管理),任何人都可以用过程语言编写自己的自动存储管理系统,但是要做很多工作。当使用诸如SmallTalk或Java之类的具有内置自动存储管理功能的语言时,程序员不必执行过多的内存管理工作。折衷方案是程序员对内存使用方式的控制较少。因此,存在优点和缺点。像面向对象编程这样的设计范式的思想是,至少对于某些程序员而言,范式的好处将大于弊端。


我认为相同的规则将适用于领域特定语言。相同的优点和缺点...一个区别是,可以使DSL足够简单,以供最终用户使用,因为“语言”对应于他们对问题空间的理解,没有其他内容。

0

一种管理软件复杂性的方法是完全使用领域特定语言将框架与所需的操作分开。这意味着编程代码的级别与配置所需结果的级别不同-完全不同的语言或系统。当正确完成此操作后,常规代码实际上将变成一个库,并且创建所需结果的用户或其他人员会将​​内容与脚本语言或可视化设计工具(例如报告生成器)一起插入。

为了工作,这需要围绕可能的操作及其链接方式(脚本语言或可视化设计,例如表单构建工具)划定严格的界限。元数据是一种从编码细节中抽象出运行时配置的重要方式,从而使系统可以支持各种期望的结果。如果对边界进行了布局并坚持(不接受随之而来的每一个扩展请求),您就可以拥有一个长期有效的系统,该系统可以为人们服务,而无需他们成为程序员来完成他们想要的事情。

马丁·福勒(Martin Fowler)撰写了有关此书,该技术几乎与编程本身一样古老。您几乎可以说所有编程语言都是领域特定语言,因此该思想是地方性的,因为它很明显而被忽略了。但是,您仍然可以创建自己的脚本或可视化设计工具来简化生活。有时,将问题泛化会使解决起来容易得多!


0

这是一个非常好的问题,我觉得这里给出的答案还没有道理,所以我会继续说下去。

目的是- 管理软件复杂性。目的不是“使用OO语言”。

引入新范式背后没有“理由”。随着编码变得更加成熟,这是自然发生的事情。编写代码比在链接列表的末尾添加新节点更有意义,因为我们在火车的末尾添加了一个教练(火车正在使用链表进行建模)。


就现实世界实体而言,编码只是当我们对现实世界实体进行编码时显而易见的正确方法。


计算机可以像在火车末尾添加额外的教练一样轻松地将节点添加到链表的末尾。但是,对于人类来说,与火车和教练一起工作比与链表和节点进行合作要容易得多,即使当我们深入研究时,我们发现火车是通过链表进行建模的。

保护或加密文件无法实现封装。加密的反义词是解密。封装的反义词是Decapsulation,它意味着用编程语言分解结构和类以获得更好的性能。通过减少内存流量和避免OOP规则检查而获得的性能。

因此,您可以编写既加密又封装好的代码,因为这两个是不同的概念。

封装通过接近现实来帮助管理复杂性。

因此,在对象中编程是因为,它使您更容易编码,并且对您和其他所有人也更快地理解。


0

要记住的一件事是:
OOP与语言功能无关;它是关于结构代码的方式

OOP是一种思维方式,也是一种设计代码架构的方式,几乎可以使用任何一种语言来完成。这尤其包括那些称为汇编程序和C的低级非OO语言。您可以在汇编程序中完美地进行面向对象的编程,并且用C编写的Linux内核在许多方面都非常面向对象。

就是说,一种语言中的OO功能极大地减少了为达到所需结果而需要编写的样板代码量。在需要显式定义虚拟函数表并在C中用适当的函数指针填充它的地方,您只需在Java中什么都不做,就可以完成。OO语言只是从源代码中删除了所有启用残障的功能,将其隐藏在良好的语言级抽象(如类,方法,成员,基类,隐式构造函数/析构函数调用等)之后。

因此,不,我们不需要 OO语言来进行OOP。只是,使用体面的OO语言可以轻松实现OOP 。



-2

最大的原因是,随着程序变得越来越复杂,您需要使该程序的某些部分与其他部分不可见,否则该应用程序的复杂性和功能数量将使您的大脑从您的耳朵中运出。

假设有一个100个类的系统,每个类可以执行约20个操作;那是2,000个函数 但是,其中只有500项是完整的操作,例如“保存”和“删除”,而1500项是内部功能,它们需要进行一些维护或具有某些实用性。考虑;

// intentionally in a non-specific language!

setName(person, name) {
    nameParts = splitPersonName(name);
    person.firstName = nameParts[0];
    person.lastName = nameParts[1];
    person.modified = true;
}

splitPersonName(name) {
    var result = [];
    result.add(name.substring(0, name.indexOf(" ")));
    result.add(name.substring(name.indexOf(" ") + 1));
    return result;
}

所以SetName是一个函数的人应该做的的人,但SplitPersonName采用的是效用函数人。

直接的过程编程在这两个操作之间没有区别。这意味着您的2,000个功能都在争夺您的注意力。但是,如果我们可以将这些功能标记为“对拥有个人记录的每个人都可用”,并且“仅用作个人记录内部的效用函数”,那么我们现在的注意力是500个“对每个人都可用”功能,以及15个“实用程序”您正在编辑的班级的功能。

这就是publicprivate做;

public class Person {
    public void setName(...) {...}
    private string[] splitPersonName(...) { ...}
}

1
我不确定您是否正确理解我的问题。我已经知道封装和数据隐藏是管理复杂性的一种方式。我只是认为,可以通过将程序划分为执行定义明确的简单任务的模块来轻松地用程序语言来完成这些任务,这些任务的内部工作在单独的受保护源中指定。那么为什么我们可以用过程语言来完成这些事情呢?那是我的问题。
Steakhouseexchange

2
我想我的回答是,如果您想做这种事情,您将不得不编写特殊的工具和语言结构(例如,对包含一个文件的特殊“ include”调用可能不会包含在其他位置)。一旦走上了这条道路,您实际上已经开始以自己的方式实现面向对象的语言。例如,C ++是最初它生产普通的C预处理器,我怀疑,一旦你实现你的系统,它看起来很像C ++对C的顶部
user62575

3
@steakexchange请注意,OOP是在许多程序语言没有这样的模块的时候开发的。有些人甚至不称此类语言为过程语言。确实,没有什么可以说一种过程语言一定不能是面向对象的,反之亦然。注意标签-它们很容易误导您。如果您的过程语言支持具有公共和私有字段和过程的模块,那么这对您有好处:)“传统过程”和“ OOP”之间的主要区别在于,调用分派在OOP中更加灵活-实际上,在严格的OOP中,您永远不知道你叫什么代码。
卢安

2
@steakexchange在ML系列语言中,模块可以很好地工作,并且与lambda结合使用,它们可以为您提供任何OO语言的所有功能(毕竟,一个函数是一个单一方法的接口,这几乎不是建议的) OO中的“好代码”家伙?由于各种原因,它们仍然没有像C ++或Java这样的更具过程性的语言使用少,但它们确实具有吸引力,并且许多人正在尝试教育人们如何简化生活(或多或少获得成功)。
卢安

C有效地拥有这些东西。它具有带有接口(.h文件)的模块(.c文件),并且可以具有公共(外部)方法和非公共(外部)方法(功能)。您甚至可以通过函数指针数组来实现穷人的多态性,我并不是说OO在C语言中很容易(或者理智),但封装起来却很容易,
Nick Keighley
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.