“我是旧时代的残党,新时代没有承载我的船。”

如果面向对象编程是一个人,我猜他自己在不断被非议的今天,一定会这样感慨。

说实话,我用面向对象方式编程已经十几年了,我做架构设计离不开它,做系统分析离不开它,编码的时候更是严重依赖它,我对面向对象无论是思想上还是写代码上都对它是有很深的感情。

刚学 Java 的时候,我觉得面向对象编程(OOP)真牛逼,用面向对象方式写出来的代码是最好的代码。但是随着项目越做越多,代码越写越多,我发现 OOP 不是万能的,盲目的迷信追求 OOP 会有代价。

今天这篇文章我不是说面向对象不好,只是希望大家不要过度神话它,更不要人云亦云。

大家都听说过

面向对象的三大特性:继承、封装、多态

但其实这个说法有问题。面向对象的思想里没有任何继承和多态的概念,正确的说法是:

这三大特性是面向对象语言的特性,而不是面向对象理念本身的。

面向对象语言是面向对象设计思想的一种实现,面向对象语言为了能在真实世界使用,其必须经过一些拓展和妥协,而问题也就随着这些拓展和妥协而来。

1. 继承带来的也可能是无以复加的痛苦

在实际开发中,我们无论谁写代码,都要考虑代码的复用性。面向对象的编程语言作为给开发人员使用的工具,它也必须考虑到复用性。

所以,在面向对象编程语言里,对面向对象的基础思想做了拓展,搞出了继承这个概念。

继承就具体实现来说,就是子类拥有父类的所有非 private 的属性和方法。继承的出现能够最大化的代码复用。

当项目里一个类已经有了我们需要的属性和方法,而我们现在的需求只是在这个已有类的基础上有些许的不同,我们只需要继承这个类,仅把这少许的不同在子类中实现即可。

但是如果你用了继承,你就引入了问题。

继承的出现天然会使得子类和父类紧耦合。也就是说,父类和子类是紧密关联的,牵一发动全身。

如果现实世界里,所有业务模型都是有层次的,而且层次井然有序,是一颗天然的树,那这种紧耦合没有什么问题。

但是现实的需求可不是吃干饭的!

咱们看看这样一种情况。假设现在我们一家只有两口人,即只有父亲和孩子,那么类继承模型很容易模拟这种情况:

面向对象编程,不香了吗?_面向对象

我们在现实生活里,往往是三口之家:

面向对象编程,不香了吗?_Java_02

那这就有问题了。就像小时候经常有人会问孩子,你觉得你是爸爸的孩子,还是妈妈的孩子啊?如果你要用 Java 的规矩回答,只能从是爸爸或者妈妈里选一个,那么完蛋了。回答爸爸的孩子,妈妈不高兴;回答妈妈的孩子,问题更严重。

但是,如果像 C++ 那样,你说我既是爸爸的孩子也是妈妈的孩子,也有问题。

假设爸爸类里有个方法叫说话,妈妈类也有个方法叫说话,你作为继承了他们的孩子类,自然也会拥有说话这个方法。问题来了,你所拥有的的说话这个方法到底来源于谁?

另外咱们说了,继承会把子类和父类紧耦合,一旦业务模型失配,就会造成问题。

这里给出一个维基百科举的经典例子,来说明一下:

class Super {

private int counter = 0;

void inc1() {
counter++;
}

void inc2() {
counter++;
}

}

class Sub extends Super {

@Override
void inc2() {
inc1();
}

}

你看,子类覆盖了父类的 inc2 方法,但是这个 inc2 方法依赖于父类 inc1 的实现。

如果父类的 inc1 逻辑发生变化了,变成下面这样

class Super {

private int counter = 0;

void inc1() {
inc2();
}

void inc2() {
counter++;
}
}

这就会出现 stack overflow 的异常,因为出现了无限递归。

所以,当我们在子类里,依赖了父类方法作为子类业务逻辑的一个关键步骤的时候,当父类的逻辑修改的时候,必须联动修改所有依赖父类相关逻辑的子类,否则就可能引发严重的问题。

用继承,本来是想少写点代码少加点班,结果……用网上看到的一句话说就是:

一日为父,终生是祖宗。

像这种情况该怎么办?

现在只要是个正经的介绍面向对象的技术文章或者书籍里,只要是涉及到继承的,都会加这么句话:

尽量选择对象组合的设计方式。

在《阿里巴巴Java开发手册》中就有一条:

面向对象编程,不香了吗?_Java_03

组合和继承的区别如下:

面向对象编程,不香了吗?_面向对象编程_04

其实我认为继承和组合各有优缺点,如果两个类确实非常紧密,就是存在层次关系,用继承没问题。

之所以有“组合优于继承”这个说法,我个人感觉是组合更灵活,而且能防止被人滥用,用不好的话轻则类的层次失控,重则很可能就把整个项目的代码质量给腐蚀了。

2. 封装如同带有裂缝的封印,可能会逃逸出魔王

封装,说白了就是把属性、方法,封到一个对象里,这是面向对象的核心理念。

嘴上叫封装,却开了个缝儿。

我们知道,项目是既要兼顾代码质量,还要兼顾运行性能的。不可能说为了提升什么松耦合、高内聚,就不管不顾性能了。

事情就坏在了这个兼顾性能这里。面向对象里,以上帝角度看,系统就是对象和对象之间的关系构造成的网络。

就拿咱们上面谈到的组合关系来说,组合关系的实现就是通过把一个对象当成另一个对象的属性来实现的。

面向对象编程,不香了吗?_封装_05

上面这图就叫做 A 和 B 之间是组合关系。想用 A 对象里的 B 对象,代码这么写:

A a = new A();
B b = a.getB();

好,我们要问了,这个从 A 中获取的 B,是 B 对象的实例还是实例的一个引用指针呢?

必然是引用指针吧,这是最基础的知识。诺,问题来了,引用指针是可以修改的。

b.getS(); //原来是Hello World
b.setS("World");//直接改成World

原来 B 中有个字段 s,值是个 “Hello World”,我直接可以用代码改成“World”。

如果这次修改随意在个犄角旮旯里,A 能知道吗?A 蒙在鼓里,还以为一切尽在把控当中呢。

你看,封装的缝儿出来了吧。说句实话,就这种鬼操作,是非常难以排查的。

像这种封装了,但是又没封装的问题,我只想说“封装的挺好的,下次别封装了”。

3. 多态好,但可能是面向对象的贪天之功

再说说多态。

其实,面向对象中的多态使用,才是面向对象语言最被认可的地方。因为有了多态,代码才能保证在业务需求多变的情况下,保证了项目的相对稳定。

可是,多态不是面向对象独有的啊。面向过程,函数式编程也可以:面向过程里,C 语言可以靠虚函数去在运行时加载对应的函数实现去实现多态。函数式编程也可以通过组合函数去实现多态。

所以,面向对象连多态这种优势都不独特了。

4. 服务端业务变了,人们的观点发生变化了

在说服务端业务的变化之前,我想先普及两个概念,即有状态的服务和无状态的服务。

有状态的服务就是说,服务需要暂时存一些和客户端相关的数据,以便客户端后续发来的请求可以和客户端前面发的请求通过服务器端关联起来,从而共同完成一项业务。

无状态服务是说,服务端不存储任何和客户端相关的数据,客户端每次请求,服务端都认为这是个新客户端,和以前的请求无任何关系。

用现实生活举例的话,有状态服务就是你去一家健身房,第一次去的时候花了一笔钱办了一张健身卡,你以后每次去健身,有卡就不用再掏钱了。

无状态服务就是,你没办卡,每次去都和第一次去一样现掏钱。

那么,无状态服务和有状态服务和面向对象的衰落又有什么关系呢?在如今的年代,分布式、微服务大行其道。一个有状态的服务是不容易做分布式和做弹性伸缩的。

当年,大家做有多个步骤的业务的时候,为了保证业务数据不会因为用户偶然的关闭浏览器或者浏览器崩溃等问题而丢失,往往会把上一个步骤的信息存在服务端的 session 里,而现在则会倾向考虑把信息放在客户端的本地存储上。

我举个例子,假设现在有个需求,要在后台系统新增加一个功能:用户信息管理。其中有个需求要求这样操作,录入用户信息分成两步。

  • 第一步,录入用户的基本信息:姓名、手机号、年龄……
  • 第二步,录入额外信息:家庭成员、教育经历、工作经历……

出于信息完整度的考虑,业务要求这两步应该是一个完整的事务。要么都成功,要么都失败。

从技术实现上讲,如果是多年以前,我们会在第一步的时候,把商户的基本信息做成表单提交,然后为了保证不会因为用户误关闭浏览器等意外问题丢失中间的数据,保存在对应的 session 中后,在第二步信息提交后,合并起来一起存入到数据库中。

但是,现在的技术趋势是,做任何事情,尽量让服务器端无状态,也就是不存储客户端相关数据。

此时,这个需求的解决方案就是,当第一步填写商户信息完成后,直接把数据存储在客户端的本地存储里又或者直接就存在 cookie 里,在第二步填写内容完毕后,联合存在客户端的信息一起提交到服务器端,然后存入数据库。

所以,你看到了,现在大家的趋势就是服务器端都在转向无状态服务,哪怕以前是有状态的服务,也会通过一些增加客户端参数等手段,去改造为无状态服务。

说了这么多,那这种技术趋势的变化对我们的面向对象有什么影响呢?

影响在于,服务端现在越来越变得往单纯的处理数据这个方向发展。当仅处理数据的时候,服务器端真正的需求其实就是计算,然后就是为了大幅度提升计算速度,而带来的并行化需求。

而面向对象这种方式和我们当今的技术趋势是有一些冲突的。

首先就是确定性的冲突。

我们的首要需求从以前重度处理业务状态加业务数据变成了业务数据的计算,而计算是需要确定性的:即给定相同的输入,经过服务器端相同的逻辑处理后,应该给定相同的输出。

而面向对象这种方式,出身在有状态服务大行其道的年代,它会优先考虑业务逻辑的调度,其次才是计算,所以,面向对象是拥有状态的。面向对象的状态就是它的字段值。这些字段值,如果单纯的从计算数据角度看,他们不仅无意义了,反而还引入了风险。

比如,我们不小心把一个对象的状态给共享出去了,那当我们用同样的输入计算的时候,很可能由于状态的变化,导致了不同的输出结果,最后就是项目出了问题。

其次,由于计算我们对性能更加看重了,又由于无状态服务的大量使用,所以,并行的重要性也远远超出了以前。而并行,要求的是结构的开放,和更加严格的无状态化,而面向对象,恰恰严重依赖于状态,并且,他还把这种状态依赖封装在了复杂的对象关系里。

A 状态依赖于 B 的状态,B 的状态又依赖于 C,而这些依赖,全部被封装在了 D 对象的实现细节里,这种严重的反并行也是现在越来越多人开始反感面向对象的重要原因。

结尾

说了这么多面向对象的坏话,其实真的是面向对象自身的问题吗?并不是。

首先,面向对象其实就是我们程序员试图简化这个世界,提高对这个世界的认知的一种美好愿望而已。愿望来自于人自身认知的局限性,所以本身就不可能完美。

其次,面向对象编程语言只是一种工具,工具的使用的好坏还是要靠人的,不可能每个人能把一套工具用的完美无缺。

如上所说,面向对象的问题本质还是人的问题,而人可能永远都需要通过组合使用越来越多的类似面向对象的这种并不完美的工具去解决自己的问题。

所以,我们不能一味的依靠面向对象,认为面向对象就是最棒的,也不能发现面向对象可能应付不了某些业务场景了,就开始极端地摒弃它。

我们要灵活地,合理地使用任何我们可以使用的编程思想、编程工具,积极地去拥抱变化。

不要忘了我们写代码的初衷。

欢迎关注我的公众号

面向对象编程,不香了吗?_面向对象_06