一文搞清楚模板模式的定义及其在MyBatis中的使用

标签: 设计模式, Java

保留所有版权,请引用而不是转载本文(原文地址 https://yeecode.top/blog/52/ )。

说起模板大家应该都熟悉。一般情况下,模板中规定了大体的框架,只留下一些细节供使用者来修改和完善。使用同一模板做出的不同产品都具有一致的框架。

设计模式中的模板模式与上述模板的概念相同。在模板模式中,需要使用一个抽象类定义一套操作的整体步骤(即,模板),而抽象类的子类则完成每个步骤的具体实现。这样,抽象类的不同子类总是遵循了同样的一套模板。

例如我们定义一套打扫卫生的模板,如下面代码所示。它为所有的打扫卫生工作定义了四个大的步骤:准备(prepare)、实施(implement)、善后(windup)、汇报(report)。

public abstract class Cleaning {

    public void clean(){
        prepare();
        implement();
        windup();
        report();
    }

    abstract void prepare();

    abstract void implement();

    abstract void windup();

    void report() {
        System.out.println("告诉别人已经打扫完成。");
    }
}

这样,我们可以基于该模板完成一些具体的打扫卫生工作。下面分别给出擦玻璃和擦黑板的实现:

public class WipeGlass extends Cleaning{
    @Override
    void prepare() {
        System.out.println("找到抹布。");
        System.out.println("浸湿和清洗抹布。");
    }

    @Override
    void implement() {
        System.out.println("擦玻璃。");
    }

    @Override
    void windup() {
        System.out.println("清理窗台。");
    }
}
public class WipeBlackboard extends Cleaning {
    @Override
    void prepare() {
        System.out.println("找到黑板檫。");
    }

    @Override
    void implement() {
        System.out.println("用力擦黑板。");
    }

    @Override
    void windup() {
        System.out.println("清理粉笔屑。");
    }
}

可以看到,虽然具体的行为不同,但是CleanGlass和WipeBlackboard两者遵循的模板是一样的,都是由Cleaning类给定的。这就是模板模式的作用,即确定了一套操作的框架,而子类只需要在此框架的基础上定义具体的实现。

MyBatis中很多地方使用了模板模式。作为一个ORM框架,处理Java对象和数据库关系之间的映射是MyBatis工作中的重要部分。然而在Java中存在Integer、String、Data等各种类型的数据,在数据库中也存在varchar、longvarchar、tinyint等各种类型的数据。不同类型的字段所需的读、写方法各不相同,因此需要对不同类型的字段采取相应的处理方式。

例如,User中有下面几个属性。

private Integer id;
private String name;
private Integer sex;
private String schoolName;

则在对User对象的属性进行读取和赋值时,需要用Integer的相关处理方式来操作idsex属性,而要用String的相关处理方式来操作nameschoolName属性。

在type包中,将每种类型对应的处理方式封装在了对应的类型处理器TypeHandler中。例如IntegerTypeHandler负责完成对Integer类型的处理。

type包一共有43个类型处理器,这些类型处理器的名称也均以“TypeHandler”结尾。而TypeHandler和BaseTypeHandler则分别是类型处理器接口和类型处理器基类。

下图展示了type包中类型处理器相关的类图。

图 8-1 类型处理器类图

TypeHandler<T>是一个接口,其中定义了进行数据处理操作的几个抽象方法。而BaseTypeHandler<T>继承了TypeHandler<T>接口,并实现了TypeHandler<T>中的接口。

在类型处理器相关类的设计中采用了模板模式,BaseTypeHandler<T>作为所有类型处理器的基类,定义了模板的框架。而在各个具体的实现类中则实现了具体的细节。(均参考自书籍《通用源码阅读指导书——MyBatis源码详解》)

以下面代码中BaseTypeHandlergetResult(ResultSet, String)方法为例。该方法完成了异常处理等统一的工作,而与具体类型相关的getNullableResult(ResultSet, String)操作则通过抽象方法交给具体的类型处理器实现。这就是典型的模板模式。

/**
 * 从结果集中读出一个结果
 * @param rs 结果集
 * @param columnName 要读取的结果的列名称
 * @return 结果值
 * @throws SQLException
 */
public T getResult(ResultSet rs, String columnName) throws SQLException {
  try {
    return getNullableResult(rs, columnName);
  } catch (Exception e) {
    throw new ResultMapException("Error attempting to get column '" + columnName + "' from result set.  Cause: " + e, e);
  }
}

BaseTypeHandler<T>交给具体的类型处理器实现的抽象方法一共只有4个。在每个类型处理器都需要实现这四个方法:

因为上面的抽象方法跟具体的类型相关,因此存在泛型参数T。在每种类型处理器中,都给出了泛型参数的值。以IntegerTypeHandler为例,它设置泛型参数值为IntegerIntegerTypeHandler类的源码如下所示。

public class IntegerTypeHandler extends BaseTypeHandler<Integer> {

  /**
   * 从结果集中读出一个可能为null的结果
   * @param rs 结果集
   * @param columnName 要读取结果的列名称
   * @return 结果值
   * @throws SQLException
   */
  @Override
  public Integer getNullableResult(ResultSet rs, String columnName)
      throws SQLException {
    int result = rs.getInt(columnName);
    return result == 0 && rs.wasNull() ? null : result;
  }

  // 省略其他方法
}

上述代码中,IntegerTypeHandler处理的类型是Integer,这表明其getNullableResult方法给出的就是一个Integer结果。

通过模板模式,MyBatis为不同的数据类型提供了相同的处理流程,从而能够完成各种类型的属性的处理。可见,阅读源码对于深入理解设计模式的实现大有裨益。这里,推荐大家阅读《通用源码阅读指导书——MyBatis源码详解》。

通用源码阅读指导书-京东自营

《通用源码阅读指导书》

《通用源码阅读指导书——MyBatis源码详解》是一本以MyBatis的源码为实例讲述源码阅读方法的书籍,并且附带有示例项目源码,MyBatis的全中文注解。书籍还总结了大量的编程知识和架构经验,对提升编程和架构能力十分有用,非常推荐。

本文首发于个人知乎:易哥(https://www.zhihu.com/people/yeecode),欢迎关注。

作者书籍推荐