2.3 引入回调(Callback)

    模板方法模式的应用很广泛,但过分地使用模板方法(Template Method)模式,往往会引起子类的泛滥。比如,我们有如下需求:查询数据库里的记录。

    1.首先我们需要得到数据库连接Connection对象;

    2.然后创建Statement实例并执行相关的查询语句;

    3.最后处理查询出来的结果并在整个执行过程中处理异常。

    研究这些步骤,不难发现,整个过程中第一步、第二步以及异常处理的逻辑对于每次查询来说,都是相同的,发生变化的部分主要是在对查询结果的处理上。根据分析,模板方法模式很适合处理这个问题,我们只需要抽象出这个处理查询结果的方法供不同的子类去延迟实现即可。

    如果你正是这样做的,你就会慢慢发现,由于各种各样的查询太多,导致我们需要创建很多的子类来处理这些查询结果,引起了子类的泛滥。为了解决此问题,通常我们结合使用回调来处理这种问题。

    回调表示一段可执行逻辑的引用(或者指针),我们把该引用(或者指针)传递到另外一段逻辑(或者方法)里供这段逻辑适时调用。

    回调在不同语言有不同的实现,例如,在C语言里经常使用函数指针实现回调,在C#语言里使用代理(delegate)实现,而在Java语言里使用内部匿名类实现回调。这里还是以Java语言为例进行说明。

    1.类图

    为了实现上述数据库的查询,我们设计了SimpleJdbcQueryTemplate为模板方法类,ResultSetHandler为回调接口,UML静态类图如图2-2所示。

    figure_0041_0021

    图2-2

    客户对象使用SimpleJdbcQueryTemplate类的query(String queryString, ResultSetHandler rsHandler)方法时,需要把回调作为第二个参数传递给这个方法,query(……)方法会在做完查询后执行该回调的handle(ResultSet rs)方法处理查询,返回最后的处理结果。

    2.代码实现

    定义ResultSetHandler接口,如下所示。

    注意这里我们使用了Java新语法——泛型(Generics[1])。

    figure_0041_0022

    于是,SimpleJdbcQueryTemplate类的代码片段如下所示.

    figure_0041_0023

    1.SimpleJdbcQueryTemplate的query(……)方法首先会获得一个数据库的连接,即这句:connection=getConnection();

    2.接着创建了一个PreparedStatement实例,即stmt=connection.prepare Statement(queryString)准备查询;

    3.这句ResultSet rs=stmt.executeQuery(queryString)表示stmt对象去数据库执行该查询并返回结果;

    4.最后调用回调rsHandler的handle(ResultSet rs)方法来处理处理查询结果并返回,即;return rsHandler.handle(rs)。

    为了演示测试代码的执行,我们使用了EasyMock框架,EasyMock是一款非常流行的运行时生成Mock对象的软件,在单元测试中使用非常广泛。我们用它生成java.sql.Connection的Mock实例和java.sql.PreparedStatement实例去模拟真实的查询,这里仅给出测试的那部分代码片段,此例的详细代码请参见示例代码,在这里就不再赘述。

    3.测试代码

    figure_0043_0025

    我们使用了Java的匿名类来回调类处理查询结果,如上加粗斜体的部分所示。这样,即使有一千种不同的查询,也不需要增加一个专门的文件。

    这里结合使用了模板方法模式和回调,避免了类的泛滥,此模式在Spring框架里使用十分广泛,有兴趣的读者可以参看其关于ORM(Object-Relational Mapping)框架和Jdbc框架的源码以做进一步深入研究。

    [1] 泛型是指能够在运行时动态得到对象类型的一种能力。这样,我们就没有必要每次都写强制类型转换语句了。其实Java是在编译时为生成的二进制代码加入强制类型转换的语句,并非真正在运行时得到对象的类型。