2.1 控制反转

所谓控制反转,是针对程序正常的控制流程而言的。一般情况下,正在运行的函数或对象的方法调用另一个函数或对象的方法,控制也就从调用方转移到被调用方,直到被调用方运行完毕,才返回给调用方。但是某些情况下,需要被调用方中途将控制传递回调用方,这种控制转移的方向与正常方向相反的现象就称为控制反转。最常见的有以下几种情况。

(1)被调用方需要一直运行,无法返回,而在不确定的时间又要运行调用方的逻辑。图形用户界面程序的开发就是很好的例子。程序员使用图形用户界面的通用类库里的控件创建视图,视图一直运行,收听用户操作触发的事件。用户什么时候输入文本框、单击按钮是不确定的。当这些事件发生时,视图则要通过事件的处理程序,执行项目特定的业务逻辑。

(2)被调用方运行时间较长,调用方不愿或者不能等待被调用方执行完成。在正常的控制流程下,在被调用方执行完毕返回前,调用方一直等待,即处于所谓阻塞状态。假如采用控制反转的模式,将调用方等待被调用方返回后要运行的逻辑以某种方式传递给被调用方,然后新开一个线程,让被调用方在其中运行,调用方就可以保有控制,去做其他事情。函数的异步调用就是这种情况。

(3)被调用方是提取多个特定程序中重用的公共的逻辑,被调用时还需要补充原来程序中特定的逻辑。例如,JavaScript中Array的forEach()、map()等方法,将对一个列表数据结构的遍历逻辑提取出来,被调用时需要传入一个函数,以实现循环中特定的逻辑。

控制反转发生的共同前提是:调用方是项目特定的代码,被调用方是具有某种功能的通用程序,在开发中无法也不应该被修改。否则若被调用方也是一般的项目(ad hoc)代码,当它需要访问调用方的功能时,就可以直接在代码中加入,控制的转移也就是正常的。

比如对于以上第一种情况,假如图形用户界面的类库不是通用的,而是程序员每开发一个从头写出的项目,每个控件都是独一无二的,那就可以直接在一个按钮的实现代码内部添加它要处理的事件的响应程序。应用程序运行时,控件执行事件处理程序时也仅仅是调用自己的一个方法。这么极端的情况当然不会发生,一种缓和的变体却是可能的,并且实实在在地存在。在这种情况下,控件仍然来自现成的类库,向视图上添加的却不是它们的实例,而是实例化自它们的继承类,在继承类中添加了事件处理程序。这样控件执行事件处理程序时,也没有将控制返回给它的调用方。理论上,开发图形用户界面程序时,确实可以采用这种方式,实际上Android的用户界面框架还特意提供了这种途径,作为控件基类的View有若干公开的方法,例如onTouchEvent(),当一个按钮被单击时,这个方法就会被系统调用。所以要为按钮添加响应该事件的逻辑,可以在按钮的继承类中实现这个方法。然而现实中没有多少程序员会采用这种方法,因为采用事件发布者和订阅者的模式,只需使用现成的控件,添加事件处理程序和调用一个方法一样简单,而为每个控件实例都创建一个继承类就烦琐得多。由这些讨论也可以从反面看出,事件实现的控制反转对图形用户界面程序开发来说,是一种多么有效和重要的模式。

对于第二种情况,假如被调用方是普通的项目代码,调用方不愿等待它运行完毕后返回,仍然要创建新的线程,但是不必将被调用方返回后要运行的逻辑再传递给它,因为此时被调用方和调用方一样,也在程序员的控制之下,直接将这些逻辑写在被调用方中就可以了。