为什么面向对象糟透了?
又是周末,编程语言“三巨头”Java, Lisp 和C语言在Hello World咖啡馆聚会。
服务员送来咖啡的同时还带来了一张今天的报纸, 三人寒暄了几句, C语言翻开了报纸,突然眼前一亮:“这篇文章的标题写得好啊, 《为什么面向对象糟透了!》”
(这是一篇很早之前的文章,作者是大名鼎鼎的Joe Armstrong , 也就是Erlang之父。)
Java 大吃一惊,居然有人这么骂面向对象?!
他赶紧抢过来,看了一会儿,说到:“虽然我对去世的Joe Armstrong老先生非常尊敬,但是我对他的观点却不敢苟同,你看他说'数据结构和函数不应该绑到一起!'”
C语言说:“他说得很有道理啊,函数是实现算法的,就像一个黑盒子,只要理解了它的输入和输出,就理解了它的功能,而数据结构呢就是单纯的‘声明’,为什么要把他们绑在一起呢?”
“不不不,还是绑在一起好!我给你举个例子,一个栈,如果你把它当成一个完整的对象,那用起来就方便多了。”
Stack s = new Stack();
s.push(100);
s.push(200);
s.pop();
C语言不甘示弱:“把数据结构和函数分开也挺好啊!比如我可以创建一个叫做Stack的数据结构,然后写几个对这个数据结构操作的函数。”
push(stack, 100);
push(stack, 200);
pop(stack);
Java不屑一顾地说:“你看看你这种方式多丑陋啊。”
C语言寸步不让:“本质都是一样的,你是o.f() ,我是f(o), 有啥区别?”
Lisp也插了一嘴:“还有我的(f o) ”
2
Java无语,心说这两个家伙就是胡搅蛮缠。突然,他心中一动:我怎么忘记多态了。
Java说:“本质是不一样的,你要知道,o.f()是可以产生多态行为的,这就带来了巨大的好处,我给你举个例子,你有一段业务逻辑,需要把计算的结果记录到文件中,将来还可能会记录别的地方,你的设计可能是这样的。”
C语言:“难道不应该如此吗?一个函数调用另外一个函数?”
Java 说到:“这里有个依赖的问题,就是businessLogic()不但运行期依赖writeToFile(), 在源码级或者编译期也会依赖。”
C语言:“这不是很正常嘛!”
Java又说道:“不一样, 写入文件是底层的实现细节,不是高层策略,假如用户不想把计算的结果保存到文件中了,而是想通过邮件发送,那你的businessLogic也得修改了,对不对?”
C语言:“那肯定啊!函数调用嘛,一个改了,另外一个也得改。”
“这就是问题了, 编译期/源代码的依赖导致我们没法把系统划分成独立的组件,各自独立开发,独立部署,一个的变化就影响到了另外一个。”
C语言觉得有一定的道理,他说:“那怎么办?”
Java说:“你看看我使用多态以后的设计,我的业务逻辑在编译时只依赖那个接口Writer,而不依赖具体的实现FileWriter和MailWriter。”
“你的意思是只要接口Writer不变化,底层的具体实现如FileWriter ,MailWriter可以随意变化,随意替换,就像插件一样,对吧?” C语言说到。
“对啊,编译期/源代码不依赖,运行期依赖,这就是延迟绑定带来的好处,现在你明白o.f()和 f(o)的本质区别了吧。 ”
Lisp 不失时机又插了一嘴:“你那接口中只有一个函数,就是write(),用什么接口啊,脱裤子放屁,多此一举,在我这儿只要把不同的函数传递过去就可以了。”
Java笑道:“别抬杠,这就是个简单的例子,不管是用接口,还是传递函数,都是延迟绑定嘛, 关键点都是要找到那个稳定的东西(Writer),就是抽象。你找不到这个稳定的东西,做不出抽象,你的系统就没法划分成可以独立开发,独立变化的组件了。”
C语言还想反击,但一直找不到突破口。
Lisp说道:“别听Java在哪里忽悠,C老弟,你也能实现运行期的延迟绑定,这不是Java的专利,你忘了虚函数表了?”
C语言一拍大腿:“是啊,我忘了那一年春节回家,Linus 大神曾经告诉我,虚函数表和函数指针才是实现多态的关键,比如Unix/Linux把设备都当成了文件,有标准的open , read 等方法,对于不同的设备,都能调用对应的方法, 那是怎么实现的?也是通过虚函数表做延迟绑定嘛!”
(码农翻身老刘注:C语言实现面向对象的详情可以移步《C语言:春节回家,就我没有对象!》)
C语言高兴了:“哈哈,Java 老弟,看来我们本质上还是一样的,多态只不过是函数指针的一种应用!”
Java说:“所以编程的关键不在于是否使用了面向对象的语言,这一点你同意吧?”
C语言点头,编程的关键点就是找到、抽象出稳定的接口,针对这个接口编程,这样就可以让各个模块能够独立地变化。
3
“说起来容易,做起来难,这儿有一个例子,你给我用面向对象设计一下?” Lisp 抛出了一道题。
动物可以分为肉食动物,草食动物,水生动物,陆生动物,用类如何表示?
Java 说:这还不简单,看看这名词多明显啊,都可以变成类啊,让他们都继承动物就可以了。
Lisp看到Java 掉入了陷阱,狡黠地一笑:“那有的动物就既是陆生动物,又是肉食动物,怎么表达?”
“那我就加一个陆生肉食动物类。” 虽然觉得不妥,Java还是说了出来。
“那要是再来一个水生肉食动物怎么办?或者来了一个新的概念‘哺乳动物’, 该怎么处理?”
“哈哈哈,我懂了,随着需求的增多,不但类会出现爆炸, 还可能会出现那种怪异的类,这面向对象编程确实是有大问题啊!” C语言说。
Java 低头沉思不语,突然,脑海中想起来了那句话:优先使用组合而不是继承。
怎么使用组合?必须得改变下看待问题的方式,对,应该这样:
Java得意地说:“看看这个图, 动物具备多个特性,如'进食', '移动',将来还可以加上'哺乳方式',每个特性都是一个接口,接口是稳定的, 动物这个概念是可以通过这些接口特性给组合起来的。”
Lisp 赞赏地点头, C语言向Java投去了钦佩的目光,这家伙经常做面向对象的设计,还是有两把刷子的,他通过特性的方式把变化给隔离了, 各个特性可以通过组合的方式,像插件一样随意替换, 嗯,这才是面向对象的真正精髓啊。
夜已深, 最后Java做了个总结,大伙散去。
“编程嘛就是发现变化,并且把它给隔离起来,使用各种语言都可以,面向对象的语言有着直接使用多态的便利,以后不要随随便便就diss它了。”
参考资料:
http://www.cs.otago.ac.nz/staffpriv/ok/Joe-Hates-OO.htm
《敏捷软件开发:原则,模式与实践》
《架构整洁之道》
《面向对象开发参考手册》
关于作者:
码农翻身老刘,15年以上软件开发经验,畅销书《码农翻身》作者,前 IBM 架构师,领导过多个企业应用架构设计和开发工作;洞察技术本质,擅长用故事去讲解复杂技术。
还不过瘾?试试它们
▲面试官逼疯面试者:聊聊多进程场景下Logging的解决方案?