面向对象编程之丑?
扫描二维码
随时随地手机看文章
面向对象的三大特性:继承、封装、多态但其实这个说法有问题。面向对象的思想里没有任何继承和多态的概念,正确的说法是:这三大特性是面向对象语言的特性,而不是面向对象理念本身的。面向对象语言是面向对象设计思想的一种实现,面向对象语言为了能在真实世界使用,其必须经过一些拓展和妥协,而问题也就随着这些拓展和妥协而来。
1. 继承带来的也可能是无以复加的痛苦
在实际开发中,我们无论谁写代码,都要考虑代码的复用性。面向对象的编程语言作为给开发人员使用的工具,它也必须考虑到复用性。所以,在面向对象编程语言里,对面向对象的基础思想做了拓展,搞出了继承这个概念。继承就具体实现来说,就是子类拥有父类的所有非 private 的属性和方法。继承的出现能够最大化的代码复用。当项目里一个类已经有了我们需要的属性和方法,而我们现在的需求只是在这个已有类的基础上有些许的不同,我们只需要继承这个类,仅把这少许的不同在子类中实现即可。但是如果你用了继承,你就引入了问题。继承的出现天然会使得子类和父类紧耦合。也就是说,父类和子类是紧密关联的,牵一发动全身。如果现实世界里,所有业务模型都是有层次的,而且层次井然有序,是一颗天然的树,那这种紧耦合没有什么问题。但是现实的需求可不是吃干饭的!咱们看看这样一种情况。假设现在我们一家只有两口人,即只有父亲和孩子,那么类继承模型很容易模拟这种情况:我们在现实生活里,往往是三口之家:那这就有问题了。就像小时候经常有人会问孩子,你觉得你是爸爸的孩子,还是妈妈的孩子啊?如果你要用 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开发手册》中就有一条:组合和继承的区别如下:其实我认为继承和组合各有优缺点,如果两个类确实非常紧密,就是存在层次关系,用继承没问题。之所以有“组合优于继承”这个说法,我个人感觉是组合更灵活,而且能防止被人滥用,用不好的话轻则类的层次失控,重则很可能就把整个项目的代码质量给腐蚀了。
2. 封装如同带有漏洞的封印,可能会逃逸出魔王
封装,说白了就是把属性、方法,封到一个对象里,这是面向对象的核心理念。嘴上叫封装,却开了个缝儿。我们知道,项目是既要兼顾代码质量,还要兼顾运行性能的。不可能说为了提升什么松耦合、高内聚,就不管不顾性能了。事情就坏在了这个兼顾性能这里。面向对象里,以上帝角度看,系统就是对象和对象之间的关系构造成的网络。就拿咱们上面谈到的组合关系来说,组合关系的实现就是通过把一个对象当成另一个对象的属性来实现的。上面这图就叫做 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 里,而现在则会倾向考虑把信息放在客户端的本地存储上。我举个例子,假设现在有个需求,要在后台系统新增加一个功能:用户信息管理。其中有个需求要求这样操作,录入用户信息分成两步。- 第一步,录入用户的基本信息:姓名、手机号、年龄……
- 第二步,录入额外信息:家庭成员、教育经历、工作经历……