面向对象语言真有那么棒嘛?

在我的整个编程生涯中,我一直反复思考关于面向对象编程的问题:用还是不用。不过,去年我终于确定下来,决定不再使用面向对象编程,下面我会说明具体原因。

先讲一个小故事:

起初都是面条式代码(译注:spaghetti code指代码控制结构复杂、混乱而难以理解,尤其用了很多GOTO、例外、线程、或其他无组织的分歧架构)。

Dijkstra说:“要有结构式编程!应当考虑到goto的危害性,用恰当的控制流机制来组织代码、构建功能。”

程序员说:“没问题,当然要这样做。”

但之后Dijkstra发现代码还是面条式的,他呼吁:“停止乱七八糟的状态共用!应当避免使用全局变量,借调用图来传达所有的状态。”

程序员又说:“呃,嗯,等一下,真要这么做吗?我们还没真正想出来怎么使用这个函数式编程的玩意儿,也不想为如今机器的不可变数据支付开销,因此你提出的这个做法对于重要的项目来说,太过不切实际,也不方便。”

不过程序员确实承认:共用状态是有问题的,也许可以减少一些全局变量。

因此,面向对象编程诞生了,而全局变量伪装成单例对象字段之后,继续幸福地存在下去。

面向对象编程是什么?

在驳斥面向对象编程(OOP)之前,我先对它下个定义。OOP可归结为三样东西:多态、继承和封装。

无论是优是劣,多态无需真正与继承体系绑在一起,因此不应被认为是OOP独有的。

继承是(大多数)人从中惊醒的一个噩梦。

然后是封装;令人困惑的是,封装至少有两个不同的含义,我会分开来解释:

封装=管理状态

尽管在80年代末和90年代初,在可复用代码的旗帜下OOP十分流行;不过在最初70年代时,人们是希望按照所谓的“对象”将状态分类并导入单独盒来分类并处理状态的。

在最初的版本中,面向对象编程由对象图构成,每个对象都是指向自身的一个状态岛。各个对象不会直接读取或写入其他对象的状态,而是彼此发送消息。虽然消息可能触发接收对象的状态更新,接收对象也可能返回状态信息,但严格来讲,状态自身不会从一个对象发到另一个对象。简而言之,对象不会共用或分发引用给状态,而只会通过有状态数据的副本向彼此发送报告。这样一来,每个对象都保留着自己状态的独有控制权,就像网络上的计算机可能会共享存储的内容,但保留对自身存储的完全控制权一样。

到目前为止,这都是一个相当简单的故事。但如果我们认真思考“状态岛”这个概念,最终面对的不是一张状态对象图,而是层次严格的结构组成。程序状态均以单独的根对象为结束,而根对象本身又由有状态的对象组成,因此反过来都是由有状态的对象组成。这个层级的对象可以将消息传递给最近的子对象,而不是发给ancestor、sibling或further descendant。

在结构化方面,什么都比不上一个干净的层次结构。然而问题在于,尽管将程序状态呈现为结构层次非常容易,但OOP需要我们在同样的层次结构中将程序状态操作结构化,在面对数量可观的状态时情况会完全不同。只要我们有横切关注点,相应操作就会涉及到多个非直接相关的对象,而该操作应当留存在那些对象的共同ancestor中。这不仅意味着相应操作通常结束在不够直观的地方,还代表这些操作需要一系列相关操作,才能达到相关的状态。

当然在实践中,程序员一般会通过自认为更简单的办法——随意共用对象引用来避免这种麻烦的模式,不过这样一来包含状态的严格层级结构就会完全被打破。真实情况下,面向对象代码往往充斥着一堆乱七八糟的泄漏封装对象,其中每个对象都可能与其他对象有串联。当猫狗对象共处一室时,混乱就成为主基调;状态封装就在程序员遇到真实成本的那一刻就不翼而飞。

即便没有并发性问题,在构建可变对象的大型对象图时,大型OOP程序也会遇到日益复杂的问题。需要了解并牢记在心:在调用方法时会发生什么事情,负面效应是什么?——Rich Hickey(Clojure之父)

现在得出正确结论:

在层级结构中的封装状态并非不好,相反在某种程度上只要有状态,所有状态理论上应当是封装的,而有状态的对象理论上应当由严格的层次构成,而不是自由形态的图表。但要注意,这里的关键在于:“在某种程度上”。想要不留后患地成功管理状态,应当保持对象层次尽可能单薄。

要想做到这一点,我们需要尽可能减少状态的使用,而其中的典范显然是函数式编程。因此在状态管理的问题上,选择由“所有东西都是对象”所构成的OOP是个错误的解决方案,但OOP至少(本来)尝试解决的问题方向是正确的。

面向对象的编程通过封装移动的部分来令代码可被理解,而函数式编程通过将移动的部分最小化来令代码可被理解。——Michael Feathers @mfeathers

封装=数据的相关行为

下面是封装的另一个问题:

在OOP从业者中,有一定程度的思想流派之争:程序的行为应当表示为类方法还是独立功能?例如很多JS代码使用很少使用方法,而另一方面很多Java代码广泛使用方法。然而,前者的风格仅仅是毫无意义,后者却成了一场灾难。

就如历史所述,在我们的问题领域面向对象设计首先识别出数据实体,然后识别出每个实体的相关行为。举个例子,确定需要一个消息类型,定位其字段,然后应当想到该如何利用消息,并通过该类中的方法来实现。

不过在考虑到代码所需要的功能时,许多行为本质上是横切关注点的,因此不真正属于某个具体的数据类型。但这些行为必须存在于某处,因此我们最终用一堆混杂的无意义Doer类来存放它们。通过封装功能到更多对象中,不仅导致结构很不直观,还增加了需要管理的状态,从而产生负担。(而这些无意义的实体习惯于产生更多无意义的实体:在有无数个Manager对象时,就需要一个管理Manager的管理器。)

思考这个基础问题:一个Message应当发送自身么?“发送”是我们希望Message执行的关键任务,因此Message对象肯定有一个“发送”方法,没错吧?如果Message不发送自身,那么必须由其他对象来完成发送任务,比如某些尚未创建的Sender对象。且慢,每个发送的Message需要一个Recipient,因此Recipient对象应当包含“接收”方法。

这是对象分解的核心难题。每个行为都可以围绕主语、动作和对象来重新语境化。Sender可以发送消息给Recipient;Message可以发送自身给Recipient;而Recipient可以接收消息。

现在你可能还认为其中一个方案是最自然的。也许如此。但消息发送是一个相对具体的动作。我们关于代码所做的大多事情都是高度精炼的,几乎没有任何“自然”责任分工,这就解释了为什么针对同一个重大问题,从来没有两个程序员给出两个相同的对象设计。

当然在功能分解上,也没有两个程序员能以相同的方式将工作划分为功能。然而:

  • 与对象不同,普通函数不需要管理,也不需要安置。
  • 较之在类中移动方法所需要的,重组功能所需的数据重组更少。

面向对象编程应当协助我们管理熟悉对象的状态与模型问题,但的确很容易将行为放入错误的对象,从而产生Frankenstein实体这样没有自然意义的实体。实际上造成了状态扩散与杂乱的状态共用),结果很快就以过于复杂和混乱的代码而告终。

因为很容易错置责任,面向对象设计无法很好的兼容增量式设计。没错,理论上完全分解类在补充额外类上十分简单,但事实上类分解并不完美,每个新类和新方法会导致更多混乱,为稍后的重组带来更大工作量。

即便在功能的责任分工上只是次佳选择的面向过程代码(特别是纯功能代码),都会有效地增加新功能和数据类型,而不会导致现有的代码变得混乱。相比之下,在采用面向对象设计时,程序员更容易缺乏前瞻性思维而受到惩罚。

设计瘫痪

多年来,我一直将面向对象编程奉为金科玉律、正确的编程方式,尽管我一直觉得自己编写的每个类和方法都会给自己带来问题,每个可能的对象分解也很可能引起争议、需要重构。将每个问题与类型相匹配就像在玩没有答案的骗人游戏。

现在我已经不再盲目追逐“合适”的对象分解,结果却更开心也更有效率了。当然在面向过程编程中,也没有合适的分解方案。不过在面向过程编程中,我不再感觉自己只是为结构增加层次,结果却没什么回报,只是增加复杂度和代码混乱性了。

原文链接:Object-Oriented Programming: A Disaster Story(译者/Vera 责编/钱曙光)

原文发布于微信公众号 - 程序员互动联盟(coder_online)

原文发表时间:2016-02-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏带你撸出一手好代码

细说10月24号为什么是程序员的节日?

今天是10曰24日,有人把这个日子定为程序员的节日,因为1024这个数字和程序员密切相关。 下面我就为大家解密,1024跟程序员有什么关系,程序员写程序又到底是...

3024
来自专栏CSDN技术头条

6个编写优质干净代码的技巧

编写干净的代码并不是一件容易的事情,这需要尝试不同的技巧和实践。问题是,在这个问题上有太多的实践和技巧,因此开发人员很难进行选择,所以要把这个问题简化一下。在本...

19710
来自专栏Java架构

一个三年Java工程师的面试总结

15年毕业到现在也近3年了,最近面试了阿里集团(菜鸟网络,蚂蚁金服)、网易、滴滴、点我达,最终收到点我达和网易offer,蚂蚁金服二面挂掉,菜鸟网络一个月了还在...

1743
来自专栏企鹅号快讯

学习C语言你所必须要了解的知识

C 语言的发展方向 ? 20世纪80年代初,C 在 UNIX系统的小型机世界中已经是主导语言了,从那时开始,它已经扩展到个人计算机和大型机, 大部分软件开发商公...

3908
来自专栏编程

老丁独家!前方高能,与“程序崩溃”的第一次邂逅!

本阶段课程适合2年级以上刚开始接触EV3机器人的同学们。 本篇请下载 LEGO Mindstorms教育版软件 有条件的话,请为孩子添置一套EV3套装,让课程内...

1959
来自专栏风中追风

敏捷软件开发学习笔记

敏捷设计:敏捷设计是一个过程,不是一个事件,它是一个持续的应用原则、模式以及实践来改进软件的结构和可读性的过程,它致力于保持系统设计在任何实践都尽可能得简单,干...

3889
来自专栏Java程序员的架构之路

一个两年的程序员,面5家斩获点我达,网易offer的面试总结

毕业到现在也近两年了,最近面试了阿里集团(菜鸟网络,蚂蚁金服),网易,滴滴,点我达,最终收到点我达,网易offer,蚂蚁金服二面挂掉,菜鸟网络一个月了还在流程中...

3074
来自专栏Python小屋

Python线性代数扩展库numpy.linalg中几个常用函数

本文内容节选自董付国老师2000页Python系列课件第17章“数据分析、科学计算、可视化、机器学习”(本章PPT共410页)。

1713
来自专栏思考的代码世界

Python网络数据采集之数据清洗|第06天

记得之前我在爬去boss网站的招聘信息的时候,抓取的数据并非我们所理想的样式,后面经过处理后,成为一个自己想要的样子,这个过程可以理解为数据清洗。这里的处理是在...

5357
来自专栏斑斓

剖析响应式编程的本质

基于Actor的响应式编程计划分为三部分,第一部分剖析响应式编程的本质思想,为大家介绍何谓响应式编程(Reactive Programming)。第二部分则结合...

4686

扫码关注云+社区

领取腾讯云代金券