Java设计模式-模板方法
模板(Template Method)方法模式,在23种设计模式中属于类行为型模式。
模板方法模式在一个方法中定义了一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
模式介绍
无论是对后台或者前端开发人员,相信模板这个词应该都不会感到陌生。前端的模板框架template.js,初始学jsp时include指令或者include动作,或者springboot开发中使用的freemarker或者Thymeleaf等,在这些模板代码的使用中,模板的存在主要是为了代码片段的复用。相比较而言,模板方法模式的重点反而并不是为了代码片段的使用,更倾向于架构层面的复用。
模板方法模式是为了创建一个算法模板的,其实这个模板就是一个方法。在该方法中将算法定义成一组步骤,其中的任何步骤都可以是抽象的,由子类负责实现。这可以确保算法结构不变,同时由子类提供部分实现。
模板方法是一种行为型设计模式。一般行为型设计模式分为两种:类行为模式和对象行为模式,类行为模式使用的是继承实现行为分配,对象行为模式使用的是对象组合而非类继承。模板方法属于类行为模式,因此它是一种基于继承的代码复用模式。
适用性
- 一次性实现一个算法的不变部分,将可变的行为留给子类去实现。
- 各子类的公共行为被抽取并集中到一个公共父类中以避免代码重复。
- 控制子类扩展。模板方法只在特定点调用“hook”操作,这样就只允许在这些点进行扩展。
示例介绍
在这里我们借助《Head First Design Pattern》中的泡茶和煮咖啡的示例做介绍。
茶 | 咖啡 |
---|---|
把水煮沸 | 把水煮沸 |
用沸水冲泡茶叶 | 用沸水冲咖啡粉 |
把茶倒进杯子 | 把咖啡倒进杯子 |
加柠檬 | 加糖和牛奶 |
将茶和咖啡的操作步骤泛化。
- 把水煮沸
- 冲泡
- 把饮料倒进杯子
- 加调料
public abstract class CaffeineBeverage { final void prepareRecipe() { boilWater(); //煮沸水 brew(); // 冲泡饮料 pourInCup(); // 将饮料倒入杯子 addCondiments(); // 添加调料 } abstract void brew(); abstract void addCondiments(); void boilWater() { System.out.println("Boiling water"); } void pourInCup() { System.out.println("Pouring into cup"); } } public class Tea extends CaffeineBeverage { public void brew() { System.out.println("Steeping the tea"); } public void addCondiments() { System.out.println("Adding Lemon"); } } Tea tea = new Tea(); Coffee coffee = new Coffee(); System.out.println("\nMaking tea..."); tea.prepareRecipe(); System.out.println("\nMaking coffee..."); coffee.prepareRecipe();
添加调料是消费者自主行为,有些消费者可能不喜欢额外的添加品,这样我们可以将添加调料作为一个可选项,由消费者自己选择是否需要添加调料。
public abstract class CaffeineBeverageWithHook { final void prepareRecipe() { boilWater(); brew(); pourInCup(); if (customerWantsCondiments()) { addCondiments(); } } // 该方法就是一个钩子方法 // 提供一个默认实现,返回值为true // 子类可以选择是否重写该方法 boolean customerWantsCondiments() { return true; } }
模板方法模式的一般示例如下:
public abstract class AbstractClass { final void templateMethod(){ primitiveOperation01(); primitiveOperation02(); concreteMethod(); hook(); } abstract void primitiveOperation01(); abstract void primitiveOperation02(); final void concreteMethod(){ // 具体方法实现 } void hook(){} }
模式分析
模板方法模式类图如下:
- AbstractClass:抽象类。用来定义算法骨架和原语操作,在这个类里面,还可以提供算法中通用的实现。
- ConcreteClass:具体实现类。用来实现算法骨架中的某些步骤,完成子类相关的功能。
模板方法templateMethod()在实现算法的过程中,用到了两个原语操作primitiveMethod()。模板方法本身和原语操作的具体实现之间解耦了。
模板方法为了固定算法框架,一般设计为final类型的,主要是为了防止子类重写算法框架。
模板方法中的调用的方法并不全是抽象方法,有一些conreteMethod(),一般讲具体方法定位为final类型的,这样子类只可以调用该类型方法,却不可以重写这些方法。
模板方法中还有一类方法,这种方法即不是抽象方法,也不会定义为final类型的,被称为钩子方法(Hook Method)或者空方法。子类可以视情况要不要覆盖该类型方法。
模板方法中原语操作要尽可能的少,定义模板方法的目的就是为了减少子类实现算法的步骤。如果原语操作过多,那么子类需要实现的操作也就越多。
模板方法其实就是依赖抽象类实现的一种类型的设计模式,但是它仅仅是抽象类应用中的一个场景。抽象类不仅仅是为了抽象模板方法中的原语操作,抽象类也会抽象许多子类需要完成的共有操作,这些共有操作并不一定是某个算法框架的一个组成部分。
好莱坞原则
通过模板方法设计模式可以很容易关联到软件设计中的一个原则-好莱坞原则。
别调用(打电话给)我们,我们会调用(打电话给)你。
好莱坞原则主要是为了防止环状依赖问题的,环状依赖就是说高层组件依赖低层组件,而低层组件又依赖高层组件。
在好莱坞原则之下,我们允许底层组件将自己挂钩到系统上,但是高层组件会决定什么时候和怎样使用这些底层组件。
好莱坞原则强调高层对低层的主动作用,即低层应该只管好自己的工作(具体实现),而高层自有它自己的工作(这就是管理低层的逻辑们,或者说从client到具体实现的一系列中间逻辑),在不需要到某个低层的时候,高层并不会调用到这个具体低层,低层永远不需要向高层作出表示,说它需要被调用。
模式优点
可以实现代码复用,模板方法通过将不变的共有的行为抽取到父类中,去除了子类中重复的代码。
将不同的代码放入不同的子类中,子类实现算法的细节,有利于算法的扩展。
通过父类调用子类实现的操作,通过子类增加扩展父类的行为,符合“开闭原则”。
模式缺点
模板方法模式体现了设计模式的通病,类的膨胀,大量衍生类的创建。另外,子类的执行结果影响到父类的行为,增加了代码的阅读难度。
与其它模式对比
模板方法模式与工厂方法模式都可以让具体的实现延迟到子类中,但是工厂方法属于创建型模式,它提供了一种创建复杂对象的方式。模板方法属于行为型模式,父类中已经定义了某个算法的框架,子类只需要实现某些特定的方法即可。
模板方法模式与策略模式的意图不一样,模板方法模式是定义一个算法的大纲,而由其子类定义其中某些步骤的内容。策略模式封装可互换的行为,每一种行为都是一种算法,这些算法之间是平等的,可以选择任何一个算法执行某种行为。
策略模式是对象行为模式,采用的是对象组合方式实现,模板方法模式是类行为模式,采用类继承方式实现。
小结
模板方法模式其实是一种很常见的设计模式,在一些框架中尤为常见。一般框架层会控制如何做事情,而使用框架的开发人员只需要实现框架中指定的具体步骤即可。
在学习模板方式模式时,需要注意哪些行为需要抽象为原语方法,哪些行为可以抽象为final的具体方法,什么时候可以使用hook方法。相比较其它设计模式而言,模板方法模式是一种非常基础的设计模式,只需要对抽象类与接口有一定了解很很容易理解。