面向对象编程的弊端是什么?

当前很多资料都在讲述面向对象的优点,可是在软件开发或者大型软件项目中,面向对象却带来了巨大的类关系设计开销,但具体有何弊端却没有详尽的介绍。 相关问题…
关注者
10,647
被浏览
2,135,679

476 个回答

从我个人来说,经常遇到的一个问题是oo和并行的冲突。oo要封装,并行要开放。oo要array of struct/class,并行要struct of array。以至于很多时候为了上CUDA等大规模并行,得把原有数据结构做大规模修改。

不过如果习惯了并行优先,那会把程序设计成数据和操作分离的模式。不那么oo但结构仍然清晰。

面向对象编程是一种处理复杂问题的设计工具,本身没有什么好坏之分,只有用的好坏之分。但面向对象的问题在于长期以来的技术环境、编程语言、一些工具的推广、培训和教育都大大的过分乐观的强调了面向对象编程本身可以带来的好处。以至于很多学习编程的人都深深的相信“只要用了面向对象编程(以及基于其基础之上的的一系列设计模式、规范、工具、框架),就能得到非常容易维护、可以复用、明晰可理解的代码“。

但,这并不是真的

如果你经历过很多,就会发现“只要如何如何,就一定能如何如何”这个提法一旦出现,基本上就不靠谱,不管是编程还是别的什么事情。

在大量的场景中,可以偏执的认为“万物皆对象”(或者万物皆别的什么),但是哲学上的单纯并不一定能让现实中的工程变得更“好”。如果说非得有个“万物皆XX”,那么这个XX八成就是根据众多需求综合到一起的“折衷”。

简单从工程讲的话,如果程序(或者说工作)是一次性的,那么怎么写得快,能work就怎么来。这个相对好理解。但是,如果程序是要长期维护的,那么如何管理其复杂性是核心的问题。而管理复杂性的要点在于

  • 让事情本身变得简单。这说白了就是砍需求,研发和PM之间要经常沟通去避免nice to have的需求变动带来的程序复杂性的剧烈变化(比如一个1对1的实体关系,需求变动一点就变成了麻烦的多的“有时1对1,有时1对多”的混合关系)。
  • 运用隔离的手段将复杂性拆解为互相影响很小的单元。一个单元对外只暴露一个简单的“接口”,隐藏内部复杂性。这就是“抽象”或者“封装“的力量。但是问题在于,这个抽象本身是否做的合适是由于问题决定的,而不是代码本身决定的。

即便是抽象,也有很多种做法。可以定义一组接口,这个接口是一组函数、一组服务的RPC还是一个class的public method都可以根据实际情况商讨。面向对象只是这里面其中一种做法而已。一个想要把程序编好的人,需要注重的是理解问题,然后尝试做出几种不同的抽象,评估各自优缺点后得到一个当时可行解的能力。而现有的大环境、教育体系,没有那么多真实的、复杂的案例,只能用一些简单的sample code来教授。并且在说明问题本身时,简化问题本身,而突出代码设计的“模式”。这就好像是在用视频教人游泳一样。学习者自己需要认识到这些培训只是个参考,玩真的还是要到项目里去体会。

即便是用面向对象做抽象也会有问题。很多时候,面向对象编程并不是一种好的“抽象”。如果抽象做得好,透过抽象出来的“接口”就可以轻易的使用这个系统。这时“大量的复杂性”被隐藏到接口后的实现里。这就像是你看电视从来都不需要拆开壳子看里面液晶屏幕和视频信号的转换,只需要知道【电源】、【调台】、【调音量】就能用。一个抽象做得好,往往要“deep”,隐藏足够的复杂度。而面向对象的文化/教育往往会鼓励程序员做很多无意义的,无性价比的抽象。看看有些代码里完全不知所云的adaptor,factory,builder等就是这种做法的产物。

此外,在大量使用继承作为设计方法时,也没有起到任何实质的隔离作用。如果你尝试扩展一个继承体系,往往需要了解整个继承体系才能写对代码——这时,复杂性并没有被隐藏起来。你也许只是代码写的少了而已。对于这种复杂度没有降低,编写代码只是写的少,但是要看懂还是得结合整个体系才能做到的方式,不是抽象,是“压缩”。压缩只能少写代码,却会让系统更难以理解了。

也许不太容易理解压缩在这里意思。比如在一段被压缩的数据中有3个bytes是“A”,“1”, “8”。但是他们的意思可能是A连续出现18次,也许是A1连续出现8次。至于到底是哪个意思,必须从头读所有的数据才能弄明白。编码也是这个道理。

再说说类型本身。一些面向对象编码对类型的定义要求的比较严格。其本质假设是“如果一个Object的类型是XXXX”,则其行为模式必然是“YYYY”。但现实当中,一个Object的行为模式不光与他的类型有关,还与这个Object“如何被使用”有关。比方说,一个User的Object,如果是用户自己看自己,就可以登陆、登出,修改昵称;如果是其他普通用户看,就只能看看看昵称和头像;如果是管理员来操作,可以reset密码、注销或者踢出登陆。这时就得界定一个Scope,来说明现在的User到底是哪个scope的User。DDD的一些理念就源自于此——找到某个上下文的某个实体概念,不能有歧义。但是即便不用DDD,也必须用各种变通的手段,把“如何用”的信息与类型信息结合到一起来实现逻辑。很郁闷的是,这个“如何用”完全没有章法,可能是“iOS App登陆“,也可能是“第一次下单时”,或者是“系统处于降级状态”时。你永远也猜不到下一次可能会有个什么条件是要纳入到上下文的。大家都知道大量用if不好,容易让代码变成麻花,无法维护。但面向对象编程本身没解决这个问题。很多文章提出面向对象某个模式可以少写if,让代码容易维护。但是这其实是建立在那个问题的上下文已经明确的基础之上。上下文易变的问题没有解决,换一个上下文,招数便不灵了,到时还得处理一坨“模式代码”,非常恶心。

最后,面向对象会倾向于将不同的代码抽象为不同相互作用的Object,但是有一些现实因素会让这么面向对象得到非常不理想的效果:

  • 安全 - 如果你的代码要求非常安全,那么所有的Object都要耦合安全控制的代码;要不就是在一层对外的接口之前拦截一道处理安全问题,内部Object都无视安全问题。这也就相当于放弃了一部分的安全性。
  • 性能 - 如果强调性能的话,是要尽量减少隔离的层次的。无论抽象如何做,只要隔离发生,就要经历一次转换以及相应的性能损耗。比如早期的Hibernate不支持“bulk insert”和“bulk update”,只能逼着程序员做for loop IO;而native的sql却可以轻易办到。在每多一次IO都很伤的场景下,这种隔离只能把事情做的更糟。
  • 数据为中心 - 很多业务场景都是以数据为中心。也就是说DB里的那坨数据是唯一的truth。在代码层面做的只是为处理数据更加方便。这时做的很多抽象意义不大。比如你可以在ORM层强制声明读取出来的一个数据少了某个字段是invalid的。但是你没法阻止你的第三方数据提供商源给你invalid的数据。对Invalid数据的处理远不是一个Annotation就能搞定的,必须引入复杂的业务流程。
  • 灵活性和成本 - 每次做某种抽象都意味着对一个系统“要做某种变化的能力做出优化”,但是同时,也就意味着或多或少对其他种变化适应性做“劣化“。如果系统变化的方向和预期的不一致,那么浪费掉的工作不说,为了再次调整设计方向的代价也会相当的大。这种情况比比皆是。

总结下,我希望所有的程序员都要理解自己的工作的最终目的是干什么的,并且活用自己所能用到的一切工具来达成自己的目标。不要在各种编程范式里迷了路。如果是初学编程的人,我衷心的希望你的编程课程讲授的是解决一些实际的问题,多了解业务,多尝试对业务的变动作出合理和准确的预。不要过早的接触高层的思想和哲学层面的问题——一个小孩看《红楼梦》又能真的看懂多少呢。

P.S. 回到面向对象编程的本身,我这里有一篇回答比较详细的解释了一下