gof23——模板方法模式

    技术2026-03-28  13

    gof23——模板方法模式

    一、模板方法模式的使用场景二、模板方法模式的应用场景三、模板方法模式在源码中的体现四、模板方法模式的优缺点 模板模式又叫模板方法模式(Template Method Pattern),是指定一个算法的骨架,并允许子类为一个或者多个步骤提供实现。模板模式使得子类在可以不改变算法结构的情况下,重新定义算法的某些步骤,属于行为型设计模式。

    一、模板方法模式的使用场景

    一次性实现一个算法的不变部分,并将可变的行为留给子类来实现各个子类中的公共的行为被提取出来并集中到一个公共的父类中

    二、模板方法模式的应用场景

    这里以我们常用的数据库访问操作来举例。在最简单的访问数据库的方式中,我们需要做获取连接,将sql语句传入statement对象,执行sql语句并处理结果这三步,其中的前两部实际上是固定的。那么我们先创建一个BaseDao来提供模板方法的模板。

    public abstract class BaseDao { private static final String DRIVER="com.mysql.cj.jdbc.Driver"; private static final String URL="jdbc:mysql://localhost:3306/db_orm?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf-8&"; private static final String USERNAME="root"; private static final String PASSWORD="123456"; protected boolean ifUpdate=false; static{ try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException exception) { exception.printStackTrace(); } } public Object execute(String sql) throws SQLException { //数据库操作都需要的准备工作,公共操作部分 Connection connection= DriverManager.getConnection(URL,USERNAME,PASSWORD); PreparedStatement preparedStatement=connection.prepareStatement(sql); //可能存在差异的部分 if(isUpdate()){ return doUpdate(preparedStatement); }else{ return doQuery(preparedStatement); } } /** * 执行增删改操作 * @return 受影响的行数 */ abstract int doUpdate(PreparedStatement preparedStatement) throws SQLException; /** * 执行查询操作 * @return 结果集 */ abstract Object doQuery(PreparedStatement preparedStatement) throws SQLException; /** * 钩子方法 是否为增删改操作 * @return default false */ protected boolean isUpdate(){ return ifUpdate; } public void isIfUpdate(boolean ifUpdate){ this.ifUpdate=ifUpdate; } }

    可以看到执行sql语句并处理结果这一步操作中,我们提供了两个抽象方法,需要到子类中进行实现,提供了一个钩子方法,用来干预执行流程,使得控制行为更加的灵活,更符合实际业务的需求。钩子方法的返回值一般作为boolean值为条件分支语句提供判断。

    具体的实现类:

    public class PigDao extends BaseDao{ @Override int doUpdate(PreparedStatement preparedStatement) throws SQLException { System.out.println("代表进行修改猪只信息的预处理操作"); System.out.println("执行修改"+preparedStatement.toString()); return 0; } @Override Object doQuery(PreparedStatement preparedStatement) throws SQLException { System.out.println("代表进行猪只信息查询的预处理操作"); System.out.println("执行查询"+preparedStatement.toString()); return null; } } public class UserDao extends BaseDao{ @Override int doUpdate(PreparedStatement preparedStatement) throws SQLException { System.out.println("代表进行修改用户信息的预处理操作"); System.out.println("执行修改"+preparedStatement.toString()); return 0; } @Override Object doQuery(PreparedStatement preparedStatement) throws SQLException { System.out.println("代表进行用户信息查询的预处理操作"); System.out.println("执行查询"+preparedStatement.toString()); return null; } }

    测试类:

    public class Client { public static void main(String[] args) throws SQLException { BaseDao dao=new PigDao(); String sql="这是一条查询猪只语句"; String sql2="这是一条猪只DML语句"; //执行查询操作 dao.execute(sql); //执行增删改操作 dao.isIfUpdate(true); dao.execute(sql2); System.out.println("=============================="); BaseDao userDao=new UserDao(); String sql3="这是一条User查询语句"; String sql4="这是一条UserDML语句"; //执行查询操作 userDao.execute(sql3); //执行增删改操作 userDao.isIfUpdate(true); userDao.execute(sql4); } }

    可以看到由于PigDao和UserDao对BaseDao内方法的重载,使得他们的execute方法执行效果完全不同。

    然而这里我们模板方法抽取的粒度只到了类,如果我们的pigDao中有多个查询方法的化,那么以上的模板是无法满足的。这里我们可以使用回调接口的方式,为每一个方法提供模板。

    package template; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.SQLException; /** * @author xxbb */ public abstract class AbstractDao { private static final String DRIVER="com.mysql.cj.jdbc.Driver"; private static final String URL="jdbc:mysql://localhost:3306/db_orm?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf-8&"; private static final String USERNAME="root"; private static final String PASSWORD="123456"; protected boolean ifUpdate=false; static{ try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException exception) { exception.printStackTrace(); } } public Object execute(String sql,DaoCallback daoCallback) throws SQLException { //数据库操作都需要的准备工作,公共操作部分 Connection connection= DriverManager.getConnection(URL,USERNAME,PASSWORD); PreparedStatement preparedStatement=connection.prepareStatement(sql); //可能存在差异的部分 if(daoCallback.isUpdate()){ return daoCallback.doUpdate(preparedStatement); }else{ return daoCallback.doQuery(preparedStatement); } } } package template; import javax.security.auth.callback.Callback; import java.sql.PreparedStatement; /** * @author xxbb */ public interface DaoCallback extends Callback { default Object doQuery(PreparedStatement preparedStatement){ return null; } default Object doUpdate(PreparedStatement preparedStatement){ return null; } default boolean isUpdate(){ return false; } }

    不同与BaseDao的在类的内部去定义需要被重载的方法,我们将表示差异化的方法放进了一个接口中,再在execute方法中传入接口对象,并在此时实现接口方法,从而使得我们类中的每一个方法都能使用模板且有具体的可变行为。如一下的日志类,其中的方法在使用了抽象父类AbstractDao的execute方法同时,通过回调接口对其中的可变行为进行定义,使得模板方法的起到效果的粒度更小。

    package template; import java.sql.PreparedStatement; import java.sql.SQLException; /** * @author xxbb */ public class LogDao extends AbstractDao{ public Object doErrorQuery(String sql) throws SQLException { return execute(sql, new DaoCallback() { @Override public Object doQuery(PreparedStatement preparedStatement) { System.out.println("代表进行错误日志查询的预处理"); System.out.println("执行错误日志查询"+preparedStatement.toString()); return null; } }); } public Object doLoginQuery(String sql) throws SQLException { return execute(sql, new DaoCallback() { @Override public Object doQuery(PreparedStatement preparedStatement) { System.out.println("代表进行登录日志查询的预处理"); System.out.println("执行登录日志查询"+preparedStatement.toString()); return null; } }); } public Object doLoginUpdate(String sql) throws SQLException{ return execute(sql, new DaoCallback() { @Override public Object doUpdate(PreparedStatement preparedStatement) { System.out.println("代表进行登录日志修改的预处理"); System.out.println("修改登录日志"+preparedStatement.toString()); return null; } @Override public boolean isUpdate() { return true; } }); } }

    三、模板方法模式在源码中的体现

    在JDK中的AbstractList:

    public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> { abstract public E get(int index); public E next() { checkForComodification(); try { int i = cursor; E next = get(i); lastRet = i; cursor = i + 1; return next; } catch (IndexOutOfBoundsException e) { checkForComodification(); throw new NoSuchElementException(); } } }<code></code>

    可以看到next()方法中使用了getget()方法,而get()方法交给子类来实现,如ArrayList。同理还有AbstractMap和AbstractSet。在我们编写Web项目时频繁使用到的HttpServlet中,doGet()、doPost()和service()都是模板方法的抽象实现。

    在MyBatis框架中也有一些经典的应用,它有一个基础的执行类BaseExecutor,实现了大部分SQL的执行逻辑,然后把几个方法交给子类定制化完成。

    public abstract class BaseExecutor implements Executor { protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException; protected abstract List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException; protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException; protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException; }

    这些方法都是由它的子类来实现

    我们可以查看下SimpleExecutor的doUpdate()方法

    @Override public int doUpdate(MappedStatement ms, Object parameter) throws SQLException { Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null); stmt = prepareStatement(handler, ms.getStatementLog()); return handler.update(stmt); } finally { closeStatement(stmt); } }

    BatchExecutor的doUpdate()方法

    @Override public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException { final Configuration configuration = ms.getConfiguration(); final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null); final BoundSql boundSql = handler.getBoundSql(); final String sql = boundSql.getSql(); final Statement stmt; if (sql.equals(currentSql) && ms.equals(currentStatement)) { int last = statementList.size() - 1; stmt = statementList.get(last); applyTransactionTimeout(stmt); handler.parameterize(stmt);//fix Issues 322 BatchResult batchResult = batchResultList.get(last); batchResult.addParameterObject(parameterObject); } else { Connection connection = getConnection(ms.getStatementLog()); stmt = handler.prepare(connection, transaction.getTimeout()); handler.parameterize(stmt); //fix Issues 322 currentSql = sql; currentStatement = ms; statementList.add(stmt); batchResultList.add(new BatchResult(ms, sql, parameterObject)); } handler.batch(stmt); return BATCH_UPDATE_RETURN_VALUE; }

    可以发现区别还是挺大的。

    四、模板方法模式的优缺点

    优点:

    利用模板方法模式将相同处理逻辑的代码放到抽象父类中,可以提高代码的复用性将不同的代码放到不同的子类中,可以通过对子类的扩展增加新的行为,提高代码的扩展性把不变的行为写在父类中,去除子类的重复代码,提供了一个很好的代码复用平台,符合开闭原则

    缺点:

    每个抽象类都不要一个子类来实现,导致了类数量的增加类数量的增加间接地增加了系统的复杂性因为继承关系自生的缺点,如果父类添加了新的抽象方法,所有的子类都需要进行修改(如果使用回调方法来定义可变的抽象方法则没有这个问题)
    Processed: 0.057, SQL: 10