面向对象编程(Object-Oriented Programming,简称OOP)是一种广泛应用于软件开发中的编程范式。尽管OOP具有许多优点,如可重复使用性、扩展性和可维护性等,但也存在着一些不足之处。本文将探讨面向对象编程的若干弊端,并深入研究这些弊端对软件开发带来的挑战。
面向对象编程是一个随着科学的发展和软件需求的不断增长,OOP的重要性日益凸显。然而,没有完美的编程方法,OOP也存在一些弊端,这些弊端对软件开发过程和项目成功产生了重要影响。本文将重点关注面向对象编程的几个常见的弊端。
一、封装性带来的复杂性: 封装是OOP的一个基本原则,通过将相关的数据和方法“包装”在一起形成对象,以达到数据隐藏和代码复用的目的。然而,过分的封装可能导致代码的复杂性增加,使得理解和维护代码变得困难。
二、继承的滥用和耦合度: 继承是OOP的另一个核心概念,它允许子类继承父类的属性和方法。尽管继承有助于代码的重用和组织结构的建立,但滥用继承可能导致类之间的高度耦合。当类之间的关系复杂时,维护和修改代码变得困难,并可能引入不必要的复杂性。
三、多态性带来的不确定性: 多态性是OOP中的重允许对象根据其具体类型执行不同的行为。虽然多态性增加了代码的灵活性和可扩展性,但当代码中存在大量的多态性时,可能导致程序行为变得复杂且不确定,使得代码的运行结果更加难以追踪和预测。
四、性能开销: 相较于其他编程方法,面向对象编程通常需要更多的系统资源和内存开销。每个对象都需要分配内存空间,并且对象之间的交互需要额外的计算和存储开销。当应用程序需要处理大量数据和复杂的逻辑时,这种额外的开销可能导致性能。
五、学习和理解成本: 面向对象编程的概念和方法需要开发人员有一定的抽象思维能力和逻辑分析能力。对于初学者来说,学习和理解OOP的成本可能较高,需要花费更多的时间和精力。
面向对象编程是一种强大的编程范式,它在软件开发中有着重要的地位。然而,我们也应该认识到OOP并非完美无缺,其存在着一些弊端和挑战。了解这些弊端有助于我们更好地应用OOP,并寻找解决方案来响,从而提高软件开发的质量和效率。
面向对象编程是一种处理复杂问题的设计工具,本身没有什么好坏之分,只有用的好坏之分。但面向对象的问题在于长期以来的技术环境、编程语言、一些工具的推广、培训和教育都大大的过分乐观的强调了面向对象编程本身可以带来的好处。以至于很多学习编程的人都深深的相信“只要用了面向对象编程(以及基于其基础之上的的一系列设计模式、规范、工具、框架),就能得到非常容易维护、可以复用、明晰可理解的代码“。
但,这并不是真的。
如果你经历过很多,就会发现“只要如何如何,就一定能如何如何”这个提法一旦出现,基本上就不靠谱,不管是编程还是别的什么事情。
在大量的场景中,可以偏执的认为“万物皆对象”(或者万物皆别的什么),但是哲学上的单纯并不一定能让现实中的工程变得更“好”。如果说非得有个“万物皆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就能搞定的,必须引入复杂的业务流程。
- 灵活性和成本 - 每次做某种抽象都意味着对一个系统“要做某种变化的能力做出优化”,但是同时,也就意味着或多或少对其他种变化适应性做“劣化“。如果系统变化的方向和预期的不一致,那么浪费掉的工作不说,为了再次调整设计方向的代价也会相当的大。这种情况比比皆是。
总结下,我希望所有的程序员都要理解自己的工作的最终目的是干什么的,并且活用自己所能用到的一切工具来达成自己的目标。不要在各种编程范式里迷了路。如果是初学编程的人,我衷心的希望你的编程课程讲授的是解决一些实际的问题,多了解业务,多尝试对业务的变动作出合理和准确的预。不要过早的接触高层的思想和哲学层面的问题——一个小孩看《红楼梦》又能真的看懂多少呢。
等我有了对象再来回答你的问题
说一件事的弊端,首先你要了解它,然后才能说到要点。
而近些年见过一些公司的开发人员,你去问:为什么不进行面向对象设计,就套用某种模式?他们经常给的回答是某某模式现在流行!!!
不可否认的一个事实是:现在很多的程序员,没有进行过严格、系统的面向对象的训练。程序员里,很多是快餐式的培训中心出身,或者是自学成才,没有机会经历系统设计的完整过程,特别是产品化的迭代。
事实上,即使现在的人工智能的开发框架:PyTorch、TensorFlow等,你去看源代码,都是面向对象的设计。
先梳理一下编程模式:过程式编程、面向对象、流式编程、函数式编程。
- 过程式编程
早期,程序功能还比较简单,只要画个流程图,按照流程编码即可。少数人维护即可。但随着代码增多、功能需求复杂、历史维护代码的出现,过程式编程的工作量迅速膨胀。
- 面向对象
随着代码规模的扩大,维护代码时,不可能一切重新来,也尽量不能影响已有代码,因此多态、继承等特性被着重提出,后来发展成了面向对象的开发模式。业界还统一意识,推出UML来便于交流协作。随之的还有如何使用UML的统一过程的最佳方法论。
面向对象的开发过程,基本都是基于统一过程的实践。
至于各种Best Practice的开发模式,都是基于面向对象的Trick。
有句口号:一切皆对象。
但就是因为“一切皆对象”被滥用,很多系统被生搬硬套的笨重无比。
- 流式编程
近些年,随着大数据的普及,对数据处理成为中心,流式编程成为热门词。流式编程适于对数据集合的处理,map、filter等集合操作是数据处理的标配,节省了大量迭代编程。可以把流式编程,理解为流水线式处理,都是标准操作。
- 函数式编程
java8后,函数可以作为参数进行传递,还有Lambda等匿名函数、闭包等概念的提出,把函数进行灵活应用。编程不必进行大量的八股文式的操作,编程效率大幅提升,而且有很大的灵活性。
面向对象编程,就整体结构而言,要先建立框架,才能进行具体定义。如果任务简单,规模小,用面向过程的方式完全可以解决。但如果要长期维护,特别是框架类的系统,还是要采取面向对象为好。
流式编程、函数式编程,底下还是面向对象的编程,只是添加的新特性,提高了开发效率与灵活性。
具体的程序里,经常是这些模式在一起使用。
面向对象编程的弊端就是能带来程序员的价值,让他在大龄的时候能不为五斗米折腰!
一个程序员,你觉得怎样评价他的价值?——我认为唯一评价的标准是代码的生命周期。
中国80%程序开发开发人员现在用面向对象的编程工具按面向过程方式在开发软件,开发效率是有了,但:
1,复杂的业务功能要不做不出来,要不就不稳定。因为面向对象的特点是能将复杂问题通过抽象,提炼使其简单化。
2,面向过程简单上手快,会导致职业周期短。因为代码价值不高,35岁门槛就过不去。
3,外部提供的各种类库,有的几十年都在用,这些类库大部分都是面向对象的,用起来简单,内部实现很复杂。能写这些类库的人,您觉得有35岁门槛吗?
4,还在抱着dao模型开发的程序员,如果不想35岁退休,尽早转ddd之类的面向对象开发吧。