前言
对于设计模式里创建型模式和结构型模式的学习完成了
现在开始行为型模式 - 模板方法模式的学习
行为型模式
思考行为型模式是什么?为什么要这么划分?
前面的创建型模式和结构型模式:
- 创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”
- 结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象
而行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配
行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为
行为型模式包括:模板方法模式、策略模式、命令模式、职责链模式、状态模式、观察者模式、中介者模式、迭代器模式、访问者模式、备忘录模式、解释器模式 11种设计模式
这些会在后来的学习中了解,现在先学习模板方法模式
现实中的问题
前面说了行为型模式是设计描述程序中复杂的流程控制
一个情景:
一个请客流程抽象成3个步骤:点单 - 》 吃东西 - 》买单
对于吃不同的东西,这个流程是一样的,只是第二个步骤:吃东西不一样
我们可以把这些规定了流程或格式的实例定义成模板,允许根据需求设计更改模板中,如吃东西可以吃面条,也可以修改成吃满汉全席
模板方法模式
模板方法模式(Template Method Pattern):定义一个操作中算法的骨架,而将一些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤
模板方法模式是基于继承的代码复用基本技术,模板方法模式的结构和用法也是面向对象设计的核心之一。在模板方法模式中,可以将相同的代码放在父类中,而将不同的方法实现放在不同的子类中
在模板方法模式中,我们需要准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法来让子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现,这就是模板方法模式的用意。模板方法模式体现了面向对象的诸多重要思想,是一种使用频率较高的模式
模板方法模式结构:
模板方法模式的角色:
(1) 抽象类(AbstractClass):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。这些方法的定义如下。
① 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
② 基本方法:是整个算法中的一个步骤,包含以下几种类型。
- 抽象方法:在抽象类中申明,由具体子类实现。
- 具体方法:在抽象类中已经实现,在具体子类中可以继承或重写它。
- 钩子方法:在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。
(2) 具体子类(ConcreteClass):实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的一个组成步骤。
可以看出结构图中只有类之间的继承关系,没有对象关联关系
模板方法模式实现
使用模板方法模式实现上面的请客案例
其中需要定义一个抽象类:请客过程
定义两种具体的请客:请吃面条、请吃满汉全席
package com.company.Behavioral;
//抽象类:定义模板
abstract class Treat {
//请客模板
final public void treatEat(){
//请客的具体流程:点单 - 》 吃菜 - 》 买单
order();
eat();
pay();
}
//具体流程:点单方法
private void order(){
System.out.println(" 服务员,点菜。。。");
}
//具体流程:抽象方法,由子类具体实现
abstract void eat();
//具体流程:买单方法
private void pay(){
System.out.println(" 这餐我买单。。。 ");
}
}
//具体实现类:请吃面条
class TreatNoodles extends Treat{
private String food = "noodles";
@Override
void eat() {
System.out.println(" 今天,我请大家吃 "+food+"。。。");
}
}
//具体实现类:请吃满汉全席
class TreatFull extends Treat{
private String food = "满汉全席";
@Override
void eat() {
System.out.println(" 今天,我请大家吃 "+food+"。。。");
}
}
class Client{
public static void main(String[] args) {
Treat treatNoodles = new TreatNoodles();
treatNoodles.treatEat();
}
}
模板方法模式很简单,主要就是通过抽象类给出模板的大致流程,再由子类去实现一下特定的方法
钩子方法扩展模板方法模式
钩子方法:默认不做任何事,子类可以根据情况选择是否重写这个方法,通过这个方法控制父类的流程
前面抽象类中定义好了具体的流程,如果对流程有一定的改动,可以使用钩子方法约束,通过在子类中实现的钩子方法对父类方法的执行进行约束,实现子类对父类行为的反向控制
例如:在吃饭之前加一个流程喝酒
就可以通过一个判断方法(钩子方法),决定是否喝酒
package com.company.Behavioral;
//抽象类:定义模板
abstract class Treat {
//请客模板
final public void treatEat(){
//请客的具体流程:点单 - 》 吃菜 - 》喝酒- 》 买单
order();
//钩子方法决定是否喝酒
if (isDrink()){
drink();
}
eat();
pay();
}
//具体流程:点单方法
private void order(){
System.out.println(" 服务员,点菜。。。");
}
//具体流程:喝酒
private void drink(){
System.out.println(" 来来来,干了这杯82年的老白干。。。");
}
//具体流程:抽象方法,由子类具体实现
abstract void eat();
//具体流程:买单方法
private void pay(){
System.out.println(" 这餐我买单。。。 ");
}
//钩子方法
public boolean isDrink(){
return true;
}
}
//具体实现类:请吃面条
class TreatNoodles extends Treat{
private String food = "noodles";
@Override
void eat() {
System.out.println(" 今天,我请大家吃 "+food+"。。。");
}
@Override
public boolean isDrink(){
return false;
}
}
//具体实现类:请吃满汉全席
class TreatFull extends Treat{
private String food = "满汉全席";
@Override
void eat() {
System.out.println(" 今天,我请大家吃 "+food+"。。。");
}
}
class Client{
public static void main(String[] args) {
//吃面条就别喝酒了
Treat treatNoodles = new TreatNoodles();
treatNoodles.treatEat();
System.out.println("-------------------");
//吃满汉全席一定要喝酒
Treat treatFull = new TreatFull();
treatFull.treatEat();
}
}
钩子方法就这么简单
模板方法模式的优缺点
优点
- 模板方法模式在一个类中形式化地定义算法,而由它的子类实现细节的处理。
- 模板方法模式是一种代码复用的基本技术。
- 模板方法模式导致一种反向的控制结构,通过一个父类调用其子类的操作,通过对子类的扩展增加新的行为,符合“开闭原则”
缺点
- 每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象,但是更加符合“单一职责原则”,使得类的内聚性得以提高
- 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度
- 由于是通过继承重写父类实现,违背了“里氏替换原则”,且因为是继承,违背了“合成复用原则”的思想
适用环境
以下场景适用模板方法模式:
- 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现
- 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复
- 对一些复杂的算法进行分割,将其算法中固定不变的部分设计为模板方法和父类具体方法,而一些可以改变的细节由其子类来实现
- 控制子类的扩展
具体使用
模板方法模式很简单,也很实用,这种模式频繁的使用与诸多框架中(Spring、Struts等)
只要复杂的算法中有公共部分,就可以使用模块方法模式,其实在实际编程中经常用到模块方法模式的思想
Spring IOC容器初始化时运用到的模板方法模式
模式扩展知识
- 继承
经常说里氏替换原则:继承不应该重写父类方法
合成复用原则:应该先考虑聚合、组合,再考虑继承
模板方法模式鼓励我们恰当使用继承,此模式可以用来改写一些拥有相同功能的相关类,将可复用的一般性的行为代码移到父类里面,而将特殊化的行为代码移到子类里面。这也进一步说明,虽然继承复用存在一些问题,但是在某些情况下还是可以给开发人员带来方便,模板方法模式就是体现继承优势的模式之一
- 好莱坞原则
好莱坞原则的定义为:“不要给我们打电话,我们会给你打电话(Don‘t call us, we’ll call you)”
在模板方法模式中,子类通过重写父类的方法来实现某些具体的业务逻辑
具体的流程是在父类中控制的,父类控制对子类的调用
好莱坞原则体现在:子类不需要调用父类,而通过父类来调用子类;由父类控制整个过程
模式细节
- 基本思想是:算法只存在于一个地方,也就是在父类中,容易修改。需要修改算法时,只要修改父类的模板方法或者已经实现的某些步骤,子类就会继承这些修改
- 实现了最大化代码复用。父类的模板方法和已实现的某些步骤会被子类继承而直接使用。
- 既统一了算法,也提供了很大的灵活性。父类的模板方法确保了算法的结构保持不变,同时由子类提供部分步骤的实现。
- 该模式的不足之处:每一个不同的实现都需要一个子类实现,导致类的个数增加,使得系统更加庞大
- 一般模板方法都加上final关键字, 防止子类重写模板方法.
- 模板方法模式使用场景:当要完成在某个过程,该过程要执行一系列步骤 ,这一系列的步骤基本相同,但其个别步骤在实现时 可能不同,通常考虑用模板方法模式来处理
总结
- 行为型模式有11中具体设计模式,主要是描述程序运行中复杂的流程控制
- 模板方法模式是定义一个算法骨架、流程,具体在子类中实现某些特定的步骤
- 模板方法模式有两个角色:抽象类:负责给出算法的骨架,由一个final模板方法和具体方法组成,具体方法可以定义钩子方法(用于子类控制父类流程),定义抽象方法(由子类完成的特定的步骤);具体实现类:实现一些具体步骤
- 模板方法模式的优点是子类定义详细的处理算法时不会改变算法的结构,实现了代码的复用,通过对子类的扩展可以增加新的行为,符合“开闭原则”
- 模板方法模式的缺点是每个不同的实现都定义一个子类,使得系统复杂、庞大,设计更抽象,且因为是继承重写实现,违背了里氏替换原则、合成复用原则;模板方法模式应当在特定场景下使用
- 模板方法模式适用场景:算法复杂对算法的分割;有公共的行为提取集中到公共父类避免代码重复;控制子类的扩展
- 在很多框架中都使用了模板方法模式,且我们实际编程中也或多或少的使用到了模板方法模式的思想