最近又有点空,翻出了《Linux多线程服务端编程:使用muduo C++网络库》随便看看,又看到了一些而之前没有注意过的问题,这要从我之前写Java的时候,一直存在的一个疑问开始说起。
在Java多线程中,如果要是实现一个线程,有两种方式:
- 继承自
Thread
类,重载Thread
类中的run()
方法 - 使用一个
Runnable
的对象new一个Thread
,直接调用start()启动线程
于是,问题来了:
为什么Java要设计这两种方式?在什么场景下会有不同?
对于以上这个问题,这次在书中,我看到了一定的答案。
库的接口设计
要回答上面的问题,首先要从库的接口设计说起。库之所以提供接口,就是希望用户逻辑能够以某种方式被注入到库的逻辑中,因此,合理的设计接口可以让这种“注入”变得更加自然易用。传统上,库的接口通过虚函数的形式给出。库的用户只需要重在这些虚函数,就将业务逻辑注入到库中,达到利用库的目的。
这对于习惯使用面向对象的开发人员来说,是非常常规的思路,即使用了“多态”。但是继承本身就是一种非常强的耦合,如果现在的需求在未来需要改变,那么很可能面临着一种牵一发动全身的窘境。
摆脱继承和虚函数
如果对这个问题稍加分析,不难看出,其实我们需要的是“多态”,虚函数仅仅是一种手段。更深一点说,“多态”代表着一种动态绑定的手法,在不同情况下,需要绑定到不同的业务逻辑中。那么问题来了,我们能不能在不使用继承的同时,实现我们需要的“多态”功能呢。下面就要提到我们熟悉的另一个概念接口(interface)。(要注意,这里的接口和库的接口是两个概念,不可混淆)
熟悉Java的人都知道,接口是一种非常常用的手段,用来描述一个类的对外表现,而忽略它到底是什么,也不管这个类是如何实现这个接口的。在Java8中,由于闭包概念的引入,将接口和闭包组合在一起,又形成只有在函数编程中才能看到的强大的模块封装的效果。
下面用一个具体点的例子来说明:
传统方法中,我们可以用如下方法,实现一个线程:
1 2 3 4 5 6 7 8 9 10 11 |
|
而如果我们使用接口+闭包的话,就可以用下面的方式:
1 2 3 4 5 6 7 8 9 |
|
从上面的例子,我们可以看出,对于Thread这样一个库,它只需要一个实现了Runnalbe接口的类,就可以了,所以我们不再需要专门写一个类,去覆写Thread的run方法。同时,按照传统的做法,我们需要一个专门的类来同时实现Runnable接口,又包含一个Data
型的对象,来实现业务逻辑。有了闭包之后,我们的编码就变得灵活了许多,我们不在需要把Data
和run
方法写在一个类里面,而是可以随这个具体业务的需求,把它们灵活的组合在一起,也就是说它们完全没有关系。
这样一来,我们的程序就可以很“轻”,我们可以轻而易举的重构任何一个组件,而不需要付出高昂的代价。这样我想起了ruby中的duck type,和go中的interface。二者都是强调的接口的灵活性。“duck type”的哲学是“像鸭子的东西就是鸭子”;而go的interface则更直接,只要符合一个interface的描述,一个类就是某个interface,而不需要像Java一样使用implements关键字指定。
小结
在Java中,这两种方式看似稀松平常,但是背后却是两种不同的设计思想:一种是以继承的方式将用户的逻辑注入库中,比较重一些;另一种则是以接口(或者说duck type、抑或tag)的形式将用户的逻辑注入库中,比较轻一些。无论是哪种,都是在软件发展史上经过前人的思考,并最终沉淀下来的经典方式。只不过是随着软件行业的发展,越来越多的需求在变化,因此,我们需要越来越灵活的编码方式来帮助我们开发罢了。
最后引用人家的一句话做结:
代码重用不是目的,目的是减少重复劳动、提高工作效率