模板方法模式及实际应用场景

242 阅读12分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

a、流程都有开启、编辑、驳回、结束。每个流程都包含这几个步骤,不同的是不同的流程实例它们的内容不一样。

b、出国留学手续一般经过以下流程:索取学校资料,提出入学申请,办理因私出国护照、出境卡和公证,申请签证,体检、订机票、准备行李,抵达目标学校等,其中有些业务对各个学校是一样的,但有些业务因学校不同而不同;

c、去银行办理业务一般要经过以下4个流程:取号、排队、办理具体业务、对银行工作人员进行评分等,其中取号、排队和对银行工作人员进行评分的业务对每个客户是一样的,但是办理具体业务却因人而异,它可能是存款、取款或者转账等;

d、我们平时用的共享单车,使用流程一般为扫码开锁、骑车、上锁、结算,其中前三步基本都一样,结算时有的人选择用支付宝,有的人选择用微信进行支付。

e、豆浆的制作过程:选材、添加配料、浸泡、豆浆机打碎;

这些大的步骤固定,不同的是每个实例的具体实现细节不一样。这些类似的业务我们都可以使用模板模式实现。

1、模板模式概述****

定义:模板模式(Template Pattern):父类定义了骨架(调用哪些方法及顺序),某些特定方法由子类实现。

通俗点的理解就是 :完成一件事情,有固定的数个步骤,但是每个步骤根据对象的不同,而实现细节不同;就可以在父类中定义一个完成该事情的总方法,按照完成事件需要的步骤去调用其每个步骤的实现方法。每个步骤的具体实现,由子类完成。

意图:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以再不改变算法结构的情况下,重新定义算法中的某些步骤。

主要解决:一些方法通用,却在每一个子类都重新写了这一方法。

何时使用:有一些通用的方法。

如何解决:将这些通用算法抽象出来。

网上找到的一个模板模式的类图:

 抽象父类(AbstractClass):实现了模板方法,定义了算法的骨架。

 具体类(ConcreteClass):实现抽象类中的抽象方法,即不同的对象的具体实现细节。

2、举例:豆浆的制作

那么为什么要使用模板模式以及如何使用呢?接下来我们以豆浆制作为例,来具体看一下模板模式是如何应用的

通过添加不同的配料,可以制作出不同口味的豆浆(红豆、花生豆浆……)

但是选材、浸泡和放到豆浆机打碎这几个步骤对于制作每种口味的豆浆都是一样的

package com.nick.pattern;

//抽象类,表示豆浆
public abstract class SoyaMilk {

    //模板方法, make , 模板方法可以做成final , 不让子类去覆盖.
    final void make() {

        select();
        addCondiments();
        soak();
        beat();

    }

    //选材料
    void select() {
        System.out.println("第一步:选择好的新鲜黄豆  ");
    }

    //添加不同的配料, 抽象方法, 子类具体实现
    abstract void addCondiments();

    //浸泡
    void soak() {
        System.out.println("第三步, 黄豆和配料开始浸泡, 需要3小时 ");
    }

    void beat() {
        System.out.println("第四步:黄豆和配料放到豆浆机去打碎  ");
        System.out.println();
    }
}

然后新建两个子类

public class RedBeanSoyaMilk extends SoyaMilk {

	@Override
	void addCondiments() {
		System.out.println("第二步,加入上好的红豆 ");
	}

}
public class PeanutSoyaMilk extends SoyaMilk {

	@Override
	void addCondiments() {
		System.out.println("第二步,加入上好的花生 ");
	}

}

接下来我们写一个客户端来制作豆浆

package com.nick.pattern;

public class Client {

	public static void main(String[] args) {
        
		System.out.println("----制作红豆豆浆----");
		SoyaMilk redBeanSoyaMilk = new RedBeanSoyaMilk();
		redBeanSoyaMilk.make();

		System.out.println("----制作花生豆浆----");
		SoyaMilk peanutSoyaMilk = new PeanutSoyaMilk();
		peanutSoyaMilk.make();
	}

}

从以上实例可以看出,其实模板模式也没什么高深莫测的,简单来说就是三大步骤:

  1. 创建一个抽象类,定义几个抽象方法和一个final修饰的模板方法,而模板方法中设定了抽象方法的执行顺序或逻辑。
  2. 无论子类有多少个,只需要继承该抽象类,实现父类的抽象方法重写自己的业务。
  3. 根据不同的需求创建不同的子类实现,每次调用的地方只需调用模板方法,即可完成特定的模板流程。

刚刚我们通过上面的客户端制作了红豆和花生豆浆,如果现在只是需要制作一杯纯豆浆,不需要添加任何配料。但是加入配料这一步又在我们的模板方法中,也就是说默认你制作豆浆时都需要添加配料。这个时候我们可以通过钩子方法对前面的模板方法进行改造。

3、钩子方法

//抽象类,表示豆浆
public abstract class SoyaMilk {

    //模板方法, make , 模板方法可以做成final , 不让子类去覆盖.
    final void make() {

        select();
        if(customerWantCondiments()) {
            addCondients();
        }
        soak();
        beat();

    }
    //不变的部分省略
    ……

    //钩子方法,决定是否需要添加配料
    boolean customerWantCondiments() {
        return true;
    }
}

抽象类SoyaMilk修改后,对原先的两个子类 RedBeanSoyaMilk和PeanutSoyaMilk 是没有影响的。

下面我们新加一个子类用来制作纯豆浆

public class PureSoyaMilk extends SoyaMilk{

	@Override
	void addCondiments() {
		//空实现
	}

	@Override
	boolean customerWantCondiments() {
		return false;
	}
 
}

然后我们通过客户端看下效果

public class Client {

	public static void main(String[] args) {

		System.out.println("----制作红豆豆浆----");
		SoyaMilk redBeanSoyaMilk = new RedBeanSoyaMilk();
		redBeanSoyaMilk.make();

		System.out.println("----制作花生豆浆----");
		SoyaMilk peanutSoyaMilk = new PeanutSoyaMilk();
		peanutSoyaMilk.make();

		System.out.println("----制作纯豆浆----");
		SoyaMilk pureSoyaMilk = new PureSoyaMilk();
		pureSoyaMilk.make();
	}

}

钩子方法,是对于抽象方法或者接口中定义的方法的一个空实现,在实际中的应用,比如说有一个接口,这个接口里有5个方法,而你只想用其中一个方法,那么这时,你可以写一个抽象类实现这个接口,在这个抽象类里将你要用的那个方法设置为abstract,其它方法进行空实现,然后你再继承这个抽象类,就不需要实现其它不用的方法,这就是钩子方法的作用。

钩子是一种被声明在抽象类中的方法,但只有空的或者默认的实现。钩子的存在,可以让子类有能力对算法的不同点进行挂钩。要不要挂钩,由子类决定。

有了钩子方法的模板方法模式才算完美,使得我们的控制行为更加的主动,更加的灵活。

注意!钩子方法应该是最开始设计就有的,而不是去完善、纠正错误的

钩子方法使用方式还可以更灵活一些,比如像HttpServlet类中的doGet()、doPost()等方法。

//这里是简化后的代码,和源代码有一定出入
//service典型的是模板方法
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String method = req.getMethod();
    if (method.equals(METHOD_GET)) {
        ……
        doGet(req, resp);
        ……
    } else if (method.equals(METHOD_POST)) {
        doPost(req, resp);
    }
    ……
}

注意:很多人会把回调函数和钩子方法搞混,其实很好区分。回调利用的接口,它注重的是对方法的描述——方法名是什么、方法参数有什么、返回值如何。而钩子方法其实就是普通的抽象类多态,它在模板方法模式中提供了改变原始逻辑的空间。

4、Spring中的模板模式

在读Spring源码的时候,发现Spring代码中运用了大量的模板模式,比如根据文件系统目录加载配置文件(FileSystemXmlApplicationContext),类路径加载配置文件(ClassPathXmlApplicationContext),以及根据项目上下文目录(XmlWebApplicationContext)加载配置文件,这个在加载的过程中就使用了模板设计模式。

Spring中几乎所有的扩展,都使用了模板方法模式,这里说下IOC部分的模板方法模式!

下面的代码展示了Spring IOC容器初始化时运用到的模板方法模式。(截取部分关键代码)

1、首先定义一个接口ConfigurableApplicationContext,声明模板方法refresh

public interface ConfigurableApplicationContext extends ApplicationContext, 
Lifecycle, Closeable {
  /**声明了一个模板方法*/
  void refresh() throws BeansException, IllegalStateException;
}

2、抽象类AbstractApplicationContext实现了接口,主要实现了模板方法refresh(这个方法很重要,是各种IOC容器初始化的入口)的逻辑

public abstract class AbstractApplicationContext extends DefaultResourceLoader
        implements ConfigurableApplicationContext, DisposableBean {

   /**模板方法的具体实现*/
    public void refresh() throws BeansException, IllegalStateException {
        synchronized (this.startupShutdownMonitor) {
            // Prepare this context for refreshing.
            prepareRefresh();

        //注意这个方法是,里面调用了两个抽象方法refreshBeanFactory、getBeanFactory
            // Tell the subclass to refresh the internal bean factory.
            ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

            // Prepare the bean factory for use in this context.
            prepareBeanFactory(beanFactory);
            try {

          //注意这个方法是钩子方法
                // Allows post-processing of the bean factory in context subclasses.
                postProcessBeanFactory(beanFactory);

                // Invoke factory processors registered as beans in the context.
                invokeBeanFactoryPostProcessors(beanFactory);
                // Register bean processors that intercept bean creation.
                registerBeanPostProcessors(beanFactory);
                // Initialize message source for this context.
                initMessageSource();
                // Initialize event multicaster for this context.
                initApplicationEventMulticaster();

          //注意这个方法是钩子方法
                // Initialize other special beans in specific context subclasses.
                onRefresh();

                // Check for listener beans and register them.
                registerListeners();
                // Instantiate all remaining (non-lazy-init) singletons.
                finishBeanFactoryInitialization(beanFactory);
                // Last step: publish corresponding event.
                finishRefresh();
            }
            catch (BeansException ex) {
                // Destroy already created singletons to avoid dangling resources.
                destroyBeans();
                // Reset 'active' flag.
                cancelRefresh(ex);
                // Propagate exception to caller.
                throw ex;
            }
        }
    }

这里最主要有一个抽象方法obtainFreshBeanFactory、两个钩子方法postProcessBeanFactory和onRefresh,看看他们在类中的定义两个钩子方法:

protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
    }

protected void onRefresh() throws BeansException {
        // For subclasses: do nothing by default.
    }

再看看获取Spring容器的抽象方法:

/**其实他内部只调用了两个抽象方法**/    
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
        refreshBeanFactory();
        ConfigurableListableBeanFactory beanFactory = getBeanFactory();
        if (logger.isDebugEnabled()) {
            logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory);
        }
        return beanFactory;
    }
protected abstract void refreshBeanFactory() throws BeansException, IllegalStateException;
public abstract ConfigurableListableBeanFactory getBeanFactory() throws IllegalStateException;

具体要取那种BeanFactory容器的决定权交给了子类!

3、具体实现的子类,实现了抽象方法getBeanFactory的子类有:

AbstractRefreshableApplicationContext:

public abstract class AbstractRefreshableApplicationContext extends AbstractApplicationContext {
    @Override
    public final ConfigurableListableBeanFactory getBeanFactory() {
        synchronized (this.beanFactoryMonitor) {
            if (this.beanFactory == null) {
                throw new IllegalStateException("BeanFactory not initialized or already closed - " +
                        "call 'refresh' before accessing beans via the ApplicationContext");
            }
            //这里的this.beanFactory在另一个抽象方法refreshBeanFactory的设置的
            return this.beanFactory;
        }
    }
}    
public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry {
    @Override
    public final ConfigurableListableBeanFactory getBeanFactory() {
    //同样这里的this.beanFactory在另一个抽象方法中设置        
    return this.beanFactory;
    }
}

其实这里的差别还不是很大,我们可以看看另一个抽象方法refreshBeanFactory的实现,两个抽象方法的配合使用。

除了IOC,Spring JdbcTemplate、Spring Transaction、Java IO、Hibernate中也用到了模板模式,感兴趣的同学可以看一下相关的源码。

6、ERP相关代码重构

做过ERP采购、仓储的同学应该知道,目前存在一些越库、下拨、代发的业务。而这些业务有一个类似的操作。

生成业务单据、冻结库存、预出库、出库确认、扣减库存、预入库、入库确认、增加库存。

private void autoInnerOrder(){
	//生成调拨单
   addInnerOrder();
    //冻结库存
   freezeInvetory();
    //生成预出库单
   addOuterVoucherInfo();
    //出库确认
   updateExecuteAmountByConfirmPage();
   //扣减库存
   reduceInvetory();
    //预入库
   savePreInStoreVoucher();
   // 入库确认
   inStoreSpareVoucher();
   //增加库存
   increaseInventory();
}

private void autoInnerAllocate(){
	//生成调货单
   addInnerAllocate();
    //冻结库存
   freezeInvetory();
    //生成预出库单
   addOuterVoucherInfo();
    //出库确认
   updateExecuteAmountByConfirmPage();
    //扣减库存
   reduceInvetory();
    //预入库
   savePreInStoreVoucher();
   // 入库确认
   inStoreSpareVoucher();
    //增加库存
   increaseInventory();
}

其实可以通过模板模式进行重构,下面是伪代码

public abstract class abstractInOutStock{
    //模板方法
    void inOutStock() {
       //生成业务单
	   generateOrder();
        //冻结库存
   	   freezeInvetory();
       //生成预出库单
   	   addOuterVoucherInfo();
        //出库确认
       updateExecuteAmountByConfirmPage();
        //扣减库存
       reduceInvetory();
        //预入库
       savePreInStoreVoucher();
       // 入库确认
       inStoreSpareVoucher();
        //增加库存
       increaseInventory();

    }
    
	//生成业务单据,抽象方法, 子类具体实现
    abstract void generateOrder();	
    //钩子方法
    protected void freezeInvetory(){
    };	
    //钩子方法
    protected void reduceInvetory(){
    };		
    //钩子方法
    protected void increaseInventory(){
    };
    
     //生成预出库单
    void addOuterVoucherInfo() {
        System.out.println("创建预出库单 ");
    }
	//出库确认
    void updateExecuteAmountByConfirmPage() {
        System.out.println("执行出库确认操作 ");
    }	
    //生成预入库单
    void savePreInStoreVoucher() {
        System.out.println("执行出库确认操作 ");
    }
    //入库确认
    void inStoreSpareVoucher() {
        System.out.println("创建预入库单 ");
    }
}

思考:其实项目里面可能用到模板的地方还是很多的,比如

1、商品系统,创建商品时(一个spu对应多个sku,并且spu和sku其实处理方式都差不多)。

2、订单系统,创建线上或者线下订单(大致流程差不多,具体实现有些不同,或者省去某些步骤)

例子还是很多的,只要有差不多的流程都可能用到模板方法模式。

5、总结

模板方法(Template method):父类定义了骨架(调用哪些方法及顺序),某些特定方法由子类实现。

父类模板方法中有两类方法:

1、共同的方法:所有子类都会用到的代码

2、不同的方法:子类要覆盖的方法,分为两种:

  A、抽象方法:父类中的是抽象方法,子类必须覆盖

  B、钩子方法:父类中是一个空方法,子类继承了默认也是空的

模板方法模式的注意事项和细节

  1.  基本思想是:算法只存在于一个地方,也就是在父类中,容易修改。需要修改算法时,只要修改父类的模板方 法或者已经实现的某些步骤,子类就会继承这些修改

  2.  实现了最大化代码复用。父类的模板方法和已实现的某些步骤会被子类继承而直接使用。

  3.  既统一了算法,也提供了很大的灵活性。父类的模板方法确保了算法的结构保持不变,同时由子类提供部分步骤的实现,符合开闭原则。 

  4.  该模式的不足之处:每一个不同的实现都需要一个子类实现,导致类的个数增加,使得系统更加庞大

  5.  一般模板方法都加上final关键字,防止子类重写模板方法.

  6.  模板方法模式使用场景:当要完成在某个过程,该过程要执行一系列步骤,这一系列的步骤基本相同,但其个别步骤在实现时可能不同,通常考虑用模板方法模式来处理。