寂然解读设计模式 - 模板模式

815 阅读9分钟
I walk very slowly, but I never walk backwards 

设计模式 - 模板模式


寂然

大家好,我是寂然,本节课我们进入到篇章五 - 行为型模式的学习,首先第一个,来聊行为模式中的模板模式,这个模式相对而言比较常见,同样,我们通过一个案例需求来引入

案例演示 - 豆浆制作

制作豆浆的流程 选材--->添加配料--->浸泡--->放到豆浆机打碎

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

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

请使用模板模式完成

(因为模板模式,比较简单,很容易想到,因此就直接应用,不再通过传统的方案来引出模板方法模式)

基本介绍

模板模式(Template Pattern),又叫模板方法模式(Template Method Pattern),他的基本介绍如下:

在一个抽象类公开定义了执行它的方法的模板,它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行,简单说,模板方法模式定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构,就可以重定义该算法的某些特定步骤

举例说明

概念听着云里雾里,其实这个模式是大家非常常用的,例如,实际开发中常常会遇到,代码骨架类似甚至相同,只是具体的实现不一样的场景,例如:流程都有开启、编辑、驳回、结束,每个流程都包含这几个步骤,不同的是不同的流程实例它们的内容不一样,再举个生活中的例子,共享单车都是先开锁、骑行、上锁、付款,这些大的步骤固定,不同的是每个实例的具体实现细节不一样。这些类似的业务我们都可以定义一个类,在其中一个方法把执行流程定义出来,例如先执行开锁,再执行骑行,接着,上锁和付款,并把公共的方法进行实现,例如共享单车开锁和关锁的方法,定义好了这样一个类似模板的类,那具体各个子类,也就是各个共享单车去实现业务逻辑就容易多了,其实这种思想,就是模板模式

原理类图


1609748193965.png


角色分析

AbstractClass

抽象类, 类中实现了模板方法(template),定义了算法的大致流程,具体子类需要去实现其它的抽象方法

ConcreteClassA/B

实现父类中的抽象方法,完成算法中特定子类的相关步骤

解决方案 - 模板模式

OK,同样的思路,我们先画出使用模板模式解决案例需求的类图,接着通过代码来实现

类图演示

1609750467616.png


代码演示
//抽象类 豆浆
public abstract class SoyaMilk {
​
 //make 即模板方法,可以做成final,不让子类去覆盖
 final void make(){
​
 select();
 add();
 soak();
 beat();
​
 }
​
 //选材
 void select(){
 System.out.println("选择新鲜的黄豆作为材料");
 }
​
 //添加配料,抽象方法
 abstract void add();
​
 //浸泡
 void soak(){
 System.out.println("将所有的材料浸泡一段时间");
 }
​
 //打碎
 void beat(){
 System.out.println("将所有的材料放到豆浆机里打碎进行制作");
 }
​
}
​
//黑豆豆浆
public class BlackBeanSoyaMilk extends SoyaMilk {
​
 @Override
 void add() {
 System.out.println("添加黑豆,糖等一系列材料");
 }
}
​
//花生豆浆
public class PeanutSoyaMilk extends SoyaMilk {
​
 @Override
 void add() {
 System.out.println("添加花生,糖等一系列材料");
 }
}
​
//客户端
public class Client {
​
 public static void main(String[] args) {
​
 //制作黑豆豆浆
 SoyaMilk blackBeanSoyaMilk = new BlackBeanSoyaMilk();
​
 blackBeanSoyaMilk.make();
​
 //制作花生豆浆
 SoyaMilk peanutSoyaMilk = new PeanutSoyaMilk();
​
 peanutSoyaMilk.make();
 }
}

所以,你可以把一个通用的方法,全部提到抽象类中去写

钩子方法

下面,我们来看下模板模式中的钩子方法

在模板方法模式的父类中,我们可以定义一个方法,它默认不做任何事,子类可以视情况要不要覆盖它,该方法称为 ”钩子方法”

案例需求

希望制作纯豆浆,不添加任何的配料,请使用钩子方法对前面的模板方法进行改造

代码演示
//抽象类 表示豆浆
public abstract class SoyaMilk {
​
 //make 即模板方法,可以做成final,不让子类去覆盖
 final void make(){
​
 select();
 if (needBatching()){
 add();
 }
 soak();
 beat();
​
 }
​
 //选材
 void select(){
 System.out.println("选择新鲜的黄豆作为材料");
 }
​
 //添加配料,抽象方法
 abstract void add();
​
 //浸泡
 void soak(){
 System.out.println("将所有的材料浸泡一段时间");
 }
​
 //打碎
 void beat(){
 System.out.println("将所有的材料放到豆浆机里打碎进行制作");
 }
​
 //钩子方法,决定是否要添加配料
 boolean needBatching(){
 return true;
 }
}
​
//纯豆浆
public class PureSoyaMilk extends SoyaMilk {
​
 @Override
 void add() {
 //空实现
 //TODO...
 }
​
 @Override
 boolean needBatching() {
 return false; //不会再去加配料
 }
}
​
//客户端
public class Client {
​
 public static void main(String[] args) {
​
 //制作纯豆浆
 SoyaMilk pureSoyaMilk = new PureSoyaMilk();
​
 pureSoyaMilk.make();
​
 }
}

它可以很轻松的实现,某一个方法在你的模板里是否要调用,你可以设计这样一个钩子方法去进行调用,如果子类没有覆盖,那默认无实现,如果子类重写了父类的方法,那么执行子类的逻辑,子类处理时提供了很大的灵活性

注意事项

基本思想

算法只存在于一个地方,也就是在父类中,容易修改,需要修改算法时,只要修改父类的模板方 法或者已经实现的某些步骤,子类就会继承这些修改,般模板方法都加上 final 关键字, 防止子类重写模板方法

优势

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

既统一了算法,也提供了很大的灵活性(钩子方法),父类的模板方法确保了算法的结构保持不变,同时由子类提供部分步骤的实现

劣势

每一个不同的实现都需要一个子类实现,导致类的个数增加,使得系统更加庞大

使用场景

当要完成在某个过程,该过程要执行一系列步骤 ,这一系列的步骤基本相同,但其个别步骤在实现时可能不同,通常考虑用模板方法模式来处理,例如上面的举例说明

IOC源码分析

源码分析

Spring IOC 容器初始化时,就会运用到模板方法模式 ,下面我们一起来看下

我们通过 ConfigurableApplicationContext 作为入口,可以看到,它是一个接口,这个接口里面定义了很多方法,其中有一个重要的方法 refresh() ,我们进到其实现来看,这个 refresh() 方法就是一个模板方法

@Override
 public void refresh() throws BeansException, IllegalStateException {
 synchronized (this.startupShutdownMonitor) {
 // Prepare this context for refreshing.
 prepareRefresh();
​
 // 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) {
 if (logger.isWarnEnabled()) {
 logger.warn("Exception encountered during context initialization - " +
 "cancelling refresh attempt: " + ex);
 }
​
 // Destroy already created singletons to avoid dangling resources.
 destroyBeans();
​
 // Reset 'active' flag.
 cancelRefresh(ex);
​
 // Propagate exception to caller.
 throw ex;
 }
​
 finally {
 // Reset common introspection caches in Spring's core, since we
 // might not ever need metadata for singleton beans anymore...
 resetCommonCaches();
 }
 }
 }

可以看到,这个方法内部调了很多很多方法,并且调用的方法,是本类中定义好的,举例两个:

// Prepare this context for refreshing.
 prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
 ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

我们进入 obtainFreshBeanFactory() 方法,里面调用了两个抽象方法,refreshBeanFactory(), getBeanFactory(),同样都是本类中定义好的,让子类去实现,这样具体要取哪种 BeanFactory 容器的决定权交给了子类,当然这里我们主要看模式的应用,不去关注IOC的初始化流程

protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
 refreshBeanFactory();
 return getBeanFactory();
 }

那大家可以看到,和我们案例中的make() 方法很类似对吧,refresh() 里面定义了运行的大致流程


refresh() 里面有没有钩子方法呢?我们接着来看这两个方法

// Allows post-processing of the bean factory in context subclasses.
 postProcessBeanFactory(beanFactory);
​
// Initialize other special beans in specific context subclasses.
 onRefresh();

可以看到,postProcessBeanFactory() 里面都是空实现

protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
 }

前面讲过,在模板方法模式里面,有些方法是空实现,主要价值是在于让子类覆盖他

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

同理,我在 refresh() 方法里面调用了 onRefresh() ,如果子类没有覆盖,那默认无实现,如果子类重写了父类的方法,那么执行子类的逻辑,可以让子类处理的时候更加灵活,大家类比于我们案例中的制作纯豆浆,如果想要制作纯豆浆,那么子类重写方法,实现相应纯豆浆的业务逻辑

当然,ConfigurableApplicationContext 不止一个实现类,我们来看 GenericApplicationContext


1609759535325.png


这个类就对上面我们聊到的 refreshBeanFactory(),getBeanFactory() 都进行了实现

以及类 AbstractRefreshableApplicationContext,同样对 refreshBeanFactory(),getBeanFactory() 进行了实现


1609763028937.png


可能有人会说 AbstractRefreshableApplicationContext 是个抽象类啊,没关系,抽象类下面还有子类,我们在考虑设计模式的时候,主要学习它的一种思想,这个地方实现了不一定就是非抽象类,也可以是个抽象类,它的子类去进行实现

流程梳理

经过上面的分析,我们来进行梳理,首先,我们从接口 ConfigurableApplicationContext 进去,该接口声明了一个模板方法 refresh(),而 AbstractApplicationContext 对模板方法 refresh() 进行了实现,定义了运行的大致流程

同时,里面也存在钩子方法,钩子方法我们上面提到,如果子类没有覆盖,那默认无实现,如果子类重写了父类的方法,那么执行子类的逻辑,钩子方法的使用可以让子类处理的时候更加灵活,当然AbstractApplicationContext是一个抽象类,所以他下面还有具体的子类,下面我们再来画下相应的类图

类图演示

1609763204435.png


refreshBeanFactory(),getBeanFactory() 是抽象方法 postProcessBeanFactory(beanFactory),onRefresh()是钩子方法,他们都在模板方法 refresh() 中被调用了,相信分析到这,大家应该明白模板模式在 IOC源码中的应用了

下节预告

OK,到这里,模板模式的相关内容就结束了,下一节,我们开启命令模式的学习,希望大家能够一起坚持下去,真正有所收获,就像开篇那句话,我走的很慢,但是我从来不后退,哈哈,那我们下期见~