【深入设计模式】模板方法模式—让你更科学地复用代码

860 阅读8分钟

我们社畜每天的日程安排几乎都被固定了,起床——早饭——上班——午饭——午休——上班——晚饭——休息——睡觉,大部分人都是这样的一个作息时间。而在这样的固定日程下也许会出现不同的情况,比如晚餐聚个餐、晚饭后继续加班、下班后休息时间逛个街之类的。又比如去银行办理业务的时候,大体流程都是取号——排队——办理,而不同的银行在办理的过程可能存在不同,比如农业银行需要到柜台人工办理,招商银行需要到机器上自己办理等等。在这样的大体流程不变、其中具体环节可能不同的方式可以使用模板方法模式来处理。

1. 模板方法模式

1.1 模板方法模式简介

我们程序员最爱做的一件事就是抽象代码,将会重复调用的代码块提取出来单独封装调用,提高代码复用率。而我们在对业务逻辑进行封装的时候会碰到这一块业务整体逻辑相同,但是其中某些步骤在特定的环境下处理逻辑会不一样,这个时候模板方法模式就能派上用场了。模板方法模式就是把我们处理逻辑的框架定义好,而其中每一步的具体实现由起子类进行实现,这样我们在特定环境下的业务处理就只需要扩展子类,不需要修改整个框架流程。简而言之就是模板方法模式能够封装不变部分,扩展可变部分。

1.2 模板方法模式结构

模板方法模式的结构比较简单,只需要一个抽象类和具体子类即可

  • 抽象类:抽象类需要包含模板方法、抽象方法、具体方法三个部分
    • 模板方法:定义整个逻辑处理框架
    • 抽象方法:用于子类扩展
    • 具体方法:复用的处理逻辑,即不变的部分,子类可以重写但是通常直接写在模板方法中不需要再重新
  • 具体子类:具体子类需要重写抽象类中的抽象方法,来完成可变部分的功能,调用者直接创建子类实例然后调用模板方法即可

image.png

// 抽象类
public abstract class AbstractClass {
    // 模板方法
    public void templateMethod() {
        System.out.println("this is template method.");
        // 调用抽象方法 1
        abstractMethod1();
        // 调用抽象方法 2
        abstractMethod2();
    }
	// 抽象方法 1,需要子类实现
    public abstract void abstractMethod1();
	// 抽象方法 2,需要子类实现
    public abstract void abstractMethod2();
}
// 具体子类 1
public class SubClass1 extends AbstractClass {
    // 重写抽象方法 1
    @Override
    public void abstractMethod1() {
        System.out.println("subclass1 implement abstract method1.");
    }

    // 重写抽象方法 2
    @Override
    public void abstractMethod2() {
        System.out.println("subclass1 implement abstract method2.");

    }
}
// 具体子类 2
public class SubClass2 extends AbstractClass {
    // 重写抽象方法 1
    @Override
    public void abstractMethod1() {
        System.out.println("subclass2 implement abstract method1.");
    }

    // 重写抽象方法 2
    @Override
    public void abstractMethod2() {
        System.out.println("subclass2 implement abstract method2.");

    }
}

调用方只需要通过创建具体子类实例,然后调用模板方法

public static void main(String[] args) {
    // 构建 SubClass1 实例
    AbstractClass class1 = new SubClass1();
    // 调用模板方法
    class1.templateMethod();
    System.out.println("====================");
    // 构建 SubClass2 实例
    AbstractClass class2 = new SubClass2();
    // 调用模板方法
    class2.templateMethod();
}

控制台输出如下

this is template method.
subclass1 implement abstract method1.
subclass1 implement abstract method2.
====================
this is template method.
subclass2 implement abstract method1.
subclass2 implement abstract method2.

从控制台输出可以看到调用模板方法的时候,首先会执行模板方法里面的代码,然后调用抽象方法 1 和抽象方法 2,由于是抽象方法,因此实际会执行到具体实例中重写的方法里和方法2,通过这样的结构就能够达到主流程不变、主流程中某些步骤具体实现由子类进行扩展实现的目的。

1.3 模板方法模式示例

接下来我么还是以场景模拟来加深对模板方法模式的理解。我们还是以社畜的一天为例,通常人们一天生活都是起床——吃早饭——上午——吃午饭——下午——吃完饭——晚上——睡觉。而在工作日和周末时,这样的流程虽然总体不会变,但是工作日的上午和下午都在上班,甚至晚上也在加班,而周末就不一样了,上午也许在睡懒觉,下午和晚上也许在逛街也许在旅游等等。因此我们使用模板方法模式来模拟这样的场景,其中抽象类(HumanDay)中的抽象方法就规定了一个人的一天流程,由于工作日和周末的上午、下午、晚上所做的事可能不同,因此定义三个抽象方法,由具体子类去单独实现。具体的子类包括工作日类(WorkDay)和周末类(Weekend),通过重写抽象方法来完成工作日和周末的上午(morningTime)、下午(afternoonTime)、晚上(eveningTime)所做的事情。

代码如下:

// 抽象类人的一天
public abstract class HumanDay {
    // 每天的生活
    public void dailyLife(){
        System.out.println("wake up.");
        System.out.println("have breakfast.");
        morningTime();
        System.out.println("have lunch.");
        afternoonTime();
        System.out.println("eat dinner.");
        eveningTime();
        System.out.println("good night.");
    }
    // 抽象方法,上午的时光
    public abstract void morningTime();
    // 抽象方法,下午的时光
    public abstract void afternoonTime();
    // 抽象方法,晚上的时光
    public abstract void eveningTime();
}
// 工作日
public class WorkDay extends HumanDay {
    @Override
    public void morningTime() {
        System.out.println("work hard.");
    }

    @Override
    public void afternoonTime() {
        System.out.println("work hard.");
    }

    @Override
    public void eveningTime() {
        System.out.println("work overtime.");
    }
}
// 周末
public class Weekend extends HumanDay {
    @Override
    public void morningTime() {
        System.out.println("fitness.");
    }

    @Override
    public void afternoonTime() {
        System.out.println("go shopping.");
    }

    @Override
    public void eveningTime() {
        System.out.println("watch movie.");
    }
}

从上面的代码可以看到我们在 HumanDay 类中规定了人每天的生活步骤,而上午、下午、晚上的时光由子类决定做什么,其中工作日(WorkDay)就把这三个时间定义为上班和加班,而周末(Weekend)就比较丰富了,上午健身、下午逛街、晚上看电影。

调用方代码如下:

public static void main(String[] args) {
    System.out.println("From Monday to Friday.");
    HumanDay workDay = new WorkDay();
    workDay.dailyLife();
    System.out.println("====================");
    System.out.println("On weekends.");
    HumanDay weekend = new Weekend();
    weekend.dailyLife();
}

控制台输出如下:

From Monday to Friday.
wake up.
have breakfast.
work hard.
have lunch.
work hard.
eat dinner.
work overtime.
good night.
====================
On weekends.
wake up.
have breakfast.
fitness.
have lunch.
go shopping.
eat dinner.
watch movie.
good night.

从控制台中可以看到,虽然我们是在 dailyLife() 这个模板方法中过完了一天,但是由于实例对象的不同,从而抽象方法的实现也不同,在固定的模板下,对其中会改变的部分进行了扩展,交给了子类自己维护。

2. 模板方法模式在框架源码中的应用

2.1 模板方法模式在 Spring 源码中的应用

在使用 XML 定义 Spring 的 Bean 时,我们需要读取 XML 并解析其中定义好的 Bean,在解析的时候会调用 DefaultBeanDefinitionDocumentReader 中的doRegisterBeanDefinitions() 方法解析 XML 中的节点来注册 Bean。该方法源码如下:

protected void doRegisterBeanDefinitions(Element root) {
    BeanDefinitionParserDelegate parent = this.delegate;
    this.delegate = createDelegate(getReaderContext(), root, parent);

    if (this.delegate.isDefaultNamespace(root)) {
        String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
        if (StringUtils.hasText(profileSpec)) {
            String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
                profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
            // We cannot use Profiles.of(...) since profile expressions are not supported
            // in XML config. See SPR-12458 for details.
            if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec +
                                 "] not matching: " + getReaderContext().getResource());
                }
                return;
            }
        }
    }

    preProcessXml(root);
    parseBeanDefinitions(root, this.delegate);
    postProcessXml(root);

    this.delegate = parent;
}

……
    
protected void preProcessXml(Element root) {
}

protected void postProcessXml(Element root) {
}

在该方法中调用了 preProcessXml 和 postProcessXml 两个方法,用于在解析 XML 前后添加处理逻辑,而进入这两个方法后可以看到方法时空的,因此我们可以通过对 DefaultBeanDefinitionDocumentReader 进行继承并重写 preProcessXml 和 postProcessXml 这两个方法在处理 XML 前后自定义添加新的功能逻辑,而这个地方便是使用的模板方法模式。

2.2 模板方法模式在 MyBatis 源码中的应用

在 MyBatis 中有一个类叫 BaseExecutor,该类定义了增删改查等基本操作。在 BaseExecutor 类中有 update()、flushStatements()、queryFromDatabase()、queryCursorqueryCursor() 四个方法,而在四个方法中又分别调用了doUpdate()、doFlushStatements()、doQuery()、doQueryCursor() 这四个最终执行的 do 方法,可以看到在该类中这几个 do 方法是空的,真正的实现交给了其子类,因此这里也是一个典型的模板方法模式。

public abstract class BaseExecutor implements Executor {
  	@Override
    public int update(MappedStatement ms, Object parameter) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
        if (closed) {
            throw new ExecutorException("Executor was closed.");
        }
        clearLocalCache();
        return doUpdate(ms, parameter);
    }  
    
    public List<BatchResult> flushStatements(boolean isRollBack) throws SQLException {
        if (closed) {
            throw new ExecutorException("Executor was closed.");
        }
        return doFlushStatements(isRollBack);
    }
    
    private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        List<E> list;
        localCache.putObject(key, EXECUTION_PLACEHOLDER);
        try {
            list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        } finally {
            localCache.removeObject(key);
        }
        localCache.putObject(key, list);
        if (ms.getStatementType() == StatementType.CALLABLE) {
            localOutputParameterCache.putObject(key, parameter);
        }
        return list;
    }
    @Override
    public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameter);
        return doQueryCursor(ms, parameter, rowBounds, boundSql);
    }
    
	// 抽象方法
    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;

    protected void closeStatement(Statement statement) {
        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
                // ignore
            }
        }
    }
}

image.png

3. 总结

模板方法模式也叫钩子函数,在是源码和实际开发中,模板方法模式的使用也是比较常见的,其核心就是将不变的行为进行封装,将其中改变的部分交给子类进行实现,这样不同的子类就维护了自己特有的逻辑,并且改变这部分代码不会对不变的部分造成影响。通过使用模板方法模式可以提高我们代码的复用性和扩展性,但是其缺点也比较明显,就是每有一种实现都会构建一个新的子类,并且如果对抽象类的模板方法进行修改,所有的子类都有可能会受到影响。

image.png