万字解析设计模式之模板方法与解释器模式
一、模板方法模式
1.1概述
定义一个操作中算法的框架,而将一些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
例如,去银行办理业务一般要经过以下4个流程:取号、排队、办理具体业务、对银行工作人员进行评分等,其中取号、排队和对银行工作人员进行评分的业务对每个客户是一样的,可以在父类中实现,但是办理具体业务却因人而异,它可能是存款、取款或者转账等,可以延迟到子类中实现。
1.2结构
模板方法(Template Method)模式包含以下主要角色:
抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成,可以是抽象方法和具体方法。
模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
基本方法:是实现算法各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三种:
抽象方法(Abstract Method) :一个抽象方法由抽象类声明、由其具体子类实现。
具体方法(Concrete Method) :一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。
钩子方法(Hook Method) :在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。抽象类中的可选步骤,子类可以选择是否实现。
一般钩子方法是用于判断的逻辑方法,这类方法名一般为isXxx,返回值类型为boolean类型。
具体子类(Concrete Class):实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。
1.3实现
【例】炒菜
炒菜的步骤是固定的,分为倒油、热油、倒蔬菜、倒调料品、翻炒等步骤。现通过模板方法模式来用代码模拟。类图如下:
抽象类(Abstract Class)
package com.yanyu.Template;
public abstract class AbstractClass {
// 模板方法,定义了烹饪的步骤
public final void cookProcess() {
//第一步:倒油
this.pourOil();
//第二步:热油
this.heatOil();
//第三步:倒蔬菜
this.pourVegetable();
//第四步:倒调味料
this.pourSauce();
//第五步:翻炒
this.fry();
}
public void pourOil() {
System.out.println("倒油");
}
// 抽象方法,由子类实现,倒蔬菜的步骤
public abstract void pourVegetable();
// 抽象方法,由子类实现,倒调味料的步骤
public abstract void pourSauce();
// 具体方法,热油的步骤是一样的,直接实现
public void heatOil() {
System.out.println("热油");
}
// 具体方法,翻炒的步骤是一样的,直接实现
public void fry(){
System.out.println("炒啊炒啊炒到熟啊");
}
}
具体子类(Concrete Class)
package com.yanyu.Template;
public class ConcreteClass_BaoCai extends AbstractClass {
@Override
public void pourVegetable() {
System.out.println("下锅的蔬菜是包菜");
}
@Override
public void pourSauce() {
System.out.println("下锅的酱料是辣椒");
}
}
package com.yanyu.Template;
public class ConcreteClass_CaiXin extends AbstractClass {
@Override
public void pourVegetable() {
System.out.println("下锅的蔬菜是菜心");
}
@Override
public void pourSauce() {
System.out.println("下锅的酱料是蒜蓉");
}
}
客户端类
package com.yanyu.Template;
public class Client {
public static void main(String[] args) {
//炒手撕包菜
ConcreteClass_BaoCai baoCai = new ConcreteClass_BaoCai();
baoCai.cookProcess();
//炒蒜蓉菜心
ConcreteClass_CaiXin caiXin = new ConcreteClass_CaiXin();
caiXin.cookProcess();
}
}
注意:为防止恶意操作,一般模板方法都加上 final 关键词。
1.4优缺点
优点:
提高代码复用性
将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中。
实现了反向控制
通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制 ,并符合“开闭原则”。
缺点:
- 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象。
- 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。
1.5应用场景
当你只希望客户端扩展某个特定算法步骤,而不是整个算法或其结构时,可使用模板方法模式;
模板方法将整个算法转换为一系列独立的步骤,以便子类能对其进行扩展,同时还可让超类中所定义的结构保持完整;
当多个类的算法除一些细微不同之外几乎完全一样时,你可使用该模式。但其后果就是, 只要算法发生变化,你就可能需要修改所有的类;
在将算法转换为模板方法时,你可将相似的实现步骤提取到超类中以去除重复代码。子类间各不同的代码可继续保留在子类中。
- 需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。
1.6源码解析
InputStream类就使用了模板方法模式。在InputStream类中定义了多个 read() 方法,如下:
public abstract class InputStream implements Closeable {
//抽象方法,要求子类必须重写
public abstract int read() throws IOException;
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
int c = read(); //调用了无参的read方法,该方法是每次读取一个字节数据
if (c == -1) {
return -1;
}
b[off] = (byte)c;
int i = 1;
try {
for (; i < len ; i++) {
c = read();
if (c == -1) {
break;
}
b[off + i] = (byte)c;
}
} catch (IOException ee) {
}
return i;
}
}
从上面代码可以看到,无参的
read()
方法是抽象方法,要求子类必须实现。而read(byte b[])
方法调用了read(byte b[], int off, int len)
方法,所以在此处重点看的方法是带三个参数的方法。在该方法中第18行、27行,可以看到调用了无参的抽象的
read()
方法。总结如下: 在InputStream父类中已经定义好了读取一个字节数组数据的方法是每次读取一个字节,并将其存储到数组的第一个索引位置,读取len个字节数据。具体如何读取一个字节数据呢?由子类实现。
二、解释器模式
2.1概述
解释器模式是一种行为型设计模式,它定义了一个语言的语法,并用一个解释器来解释该语言中的句子。通常,解释器模式用于将一个复杂的语言拆分成一些简单的语言元素,使它们易于理解和操作。
2.2结构
- 抽象表达式(Abstract Expression)角色:定义解释器的接口,约定解释器的解释操作,主要包含解释方法 interpret()。定义了一个抽象的解释操作,所有具体的表达式都需要实现这个接口。
- 终结符表达式(Terminal Expression)角色:是抽象表达式的子类,用来实现文法中与终结符相关的操作,文法中的每一个终结符都有一个具体终结表达式与之相对应。它实现了抽象表达式的解释方法。
- 非终结符表达式(Nonterminal Expression)角色:也是抽象表达式的子类,用来实现文法中与非终结符相关的操作,文法中的每条规则都对应于一个非终结符表达式。它们通过递归的方式来解释语言。
- 环境(Context)角色:通常包含各个解释器需要的数据或是公共的功能,一般用来传递被所有解释器共享的数据,后面的解释器可以从这里获取这些值。环境保存了要解释的语言,它提供了一个接口给表达式来获取和设置环境的状态。
2.3实现
【例】设计实现加减法的软件
抽象表达式
package com.yanyu.Expressioner;
//抽象角色AbstractExpression
public abstract class AbstractExpression {
//定义了一个解释器方法,接收一个上下文对象,返回解释结果
public abstract int interpret(Context context);
}
终结符表达式(Terminal Expression)角色
package com.yanyu.Expressioner;
// 终结符表达式角色 变量表达式
// 变量表达式是解释器模式中的一种角色,用于表示语言中的变量。在这里,Variable类表示一个变量,它继承自抽象表达式角色AbstractExpression。
public class Variable extends AbstractExpression {
private String name;
// 构造函数,用于初始化变量名
public Variable(String name) {
this.name = name;
}
// interpret方法用于解释上下文中的表达式,这里是返回变量对应的值
@Override
public int interpret(Context ctx) {
return ctx.getValue(this);
}
// 重写toString方法,返回变量名的字符串表示
@Override
public String toString() {
return name;
}
}
package com.yanyu.Expressioner;
// 终结符表达式角色
// 终结符表达式是解释器模式中的一种角色,用于表示语言中的基本元素。在这里,Value类表示一个具体的值,它继承自抽象表达式角色AbstractExpression。
public class Value extends AbstractExpression {
private int value;
// 构造函数,用于初始化值
public Value(int value) {
this.value = value;
}
// interpret方法用于解释上下文中的表达式,这里是返回值本身
@Override
public int interpret(Context context) {
return value;
}
// 重写toString方法,返回值的字符串表示
@Override
public String toString() {
return Integer.valueOf(value).toString();
}
}
非终结符表达式(Nonterminal Expression)角色
package com.yanyu.Expressioner;
// 非终结符表达式角色 加法表达式
// 加法表达式是解释器模式中的一种非终结符表达式角色,用于表示语言中的加法操作。在这里,Plus类表示加法表达式,它继承自抽象表达式角色AbstractExpression。
public class Plus extends AbstractExpression {
private AbstractExpression left; // 左操作数
private AbstractExpression right; // 右操作数
// 构造函数,用于初始化左右操作数
public Plus(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}
// interpret方法用于解释上下文中的表达式,这里是返回左右操作数的解释结果相加的值
@Override
public int interpret(Context context) {
return left.interpret(context) + right.interpret(context);
}
// 重写toString方法,返回加法表达式的字符串表示,形式为 (左操作数 + 右操作数)
@Override
public String toString() {
return "(" + left.toString() + " + " + right.toString() + ")";
}
}
package com.yanyu.Expressioner;
///非终结符表达式角色 减法表达式
public class Minus extends AbstractExpression {
private AbstractExpression left;
private AbstractExpression right;
public Minus(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret(Context context) {
return left.interpret(context) - right.interpret(context);
}
@Override
public String toString() {
return "(" + left.toString() + " - " + right.toString() + ")";
}
}
环境(Context)角色
package com.yanyu.Expressioner;
import java.util.HashMap;
import java.util.Map;
// 环境类
// 环境类用于存储变量和它们的值,在解释器模式中起到承上启下的作用,为解释器提供解释所需的上下文信息。
public class Context {
private Map<Variable, Integer> map = new HashMap<Variable, Integer>();
// 将变量和对应的值存入map中
public void assign(Variable var, Integer value) {
map.put(var, value);
}
// 获取变量对应的值
public int getValue(Variable var) {
Integer value = map.get(var);
return value;
}
}
客户端类
package com.yanyu.Expressioner;
// 测试类
// 客户端类Client用于测试解释器模式的功能。在这里,我们创建了一个上下文对象context,以及五个变量a、b、c、d、e,并为这些变量赋值。
// 然后,我们构造了一个复杂的表达式,包括加法和减法操作,并通过interpret方法解释这个表达式,输出其计算结果。
public class Client {
public static void main(String[] args) {
Context context = new Context(); // 创建上下文对象
Variable a = new Variable("a"); // 创建变量a
Variable b = new Variable("b"); // 创建变量b
Variable c = new Variable("c"); // 创建变量c
Variable d = new Variable("d"); // 创建变量d
Variable e = new Variable("e"); // 创建变量e
context.assign(a, 1); // 为变量a赋值
context.assign(b, 2); // 为变量b赋值
context.assign(c, 3); // 为变量c赋值
context.assign(d, 4); // 为变量d赋值
context.assign(e, 5); // 为变量e赋值
// 构造复杂的表达式,包括加法和减法操作
AbstractExpression expression = new Minus(new Plus(new Plus(new Plus(a, b), c), d), e);
// 解释并输出表达式的计算结果
System.out.println(expression + "= " + expression.interpret(context));
}
}
2.4 优缺点
1,优点:
易于改变和扩展文法。
由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言。
实现文法较为容易。
在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂。
增加新的解释表达式较为方便。
如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无须修改,符合 "开闭原则"。
2,缺点:
对于复杂文法难以维护。
在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护。
执行效率较低。
由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调试过程也比较麻烦
2.5应用场景
- 当语言的文法较为简单,且执行效率不是关键问题时。
- 当问题重复出现,且可以用一种简单的语言来进行表达时。
- 当一个语言需要解释执行,并且语言中的句子可以表示为一个抽象语法树的时候。
三、模板方法模式实验
任务描述
高校网上办事系统中的需要对场地预约、设备维修、教职工请假、车辆登记等申请单进行审核,因此需要与众多子系统进行对接。子系统会将用户填写好的申请单推送到网上办事系统中,处理完后将单号返回给子系统。对接的申请单种类有多个,每种申请单的处理流程是一样的(数据校验-申请单解析-申请单入库-提交审核-自动备份),最大的区别在于不同的申请单,解析方法不同。
本关任务:以场地预约申请单(
VenueApplication
)和教职工请假申请单(LeaveApplication
)为例,模拟实现申请单处理流程。实现方式
分析目标算法, 确定能否将其分解为多个步骤。 从所有子类的角度出发, 考虑哪些步骤能够通用, 哪些步骤各不相同;
创建抽象基类并声明一个模板方法和代表算法步骤的一系列抽象方法。 在模板方法中根据算法结构依次调用相应步骤。 可用
final
最终修饰模板方法以防止子类对其进行重写;虽然可将所有步骤全都设为抽象类型, 但默认实现可能会给部分步骤带来好处, 因为子类无需实现那些方法;
可考虑在算法的关键步骤之间添加钩子;
为每个算法变体新建一个具体子类, 它必须实现所有的抽象步骤, 也可以重写部分可选步骤。
编程要求
根据提示,补充右侧编辑器文件
Client.java
中 Begin-End 内的代码,完成实验。其它文件的代码不需要修改。测试说明
平台会对你编写的代码进行测试:
测试输入:
张三
LeaveApplication
预期输出:张三数据校验
教职工请假申请单数据解析
张三申请单入库
张三提交审核
张三自动存档
测试输入:
报告厅
VenueApplication
预期输出:报告厅数据校验
场地预约申请单数据解析
报告厅申请单入库
报告厅提交审核
报告厅自动存档
抽象类
public abstract class ApplicationTemplate {
public boolean execute(String data){
this.checker(data);
this.dataAnalysis(data);
this.proposalSave(data);
this.submit(data);
this.autoSave(data);
return true;
}
/**
* 数据校验
*/
public void checker(String data){
System.out.println(data+"数据校验");
}
/**
* 数据解析
*/
public abstract void dataAnalysis(String data);
/**
* 数据入库
*/
public void proposalSave(String data){
System.out.println(data+"申请单入库");
}
/**
* 提交审核
*/
public void submit(String data){
System.out.println(data+"提交审核");
}
/**
* 自动存档
*/
public void autoSave(String data){
System.out.println(data+"自动存档");
}
}
具体类
public class LeaveApplication extends ApplicationTemplate{
@Override
public void dataAnalysis(String data) {
System.out.println("教职工请假申请单数据解析");
}
}
public class VenueApplication extends ApplicationTemplate{
@Override
public void dataAnalysis(String data) {
System.out.println("场地预约申请单数据解析");
}
}
客户端类
import java.util.Scanner;
public class Client {
public static void main(String[] args) {
/********** Begin *********/
Scanner scanner = new Scanner(System.in);
String applicant = scanner.nextLine();
String applicationType = scanner.nextLine();
if (applicationType.equals("LeaveApplication")) {
LeaveApplication leaveApplication = new LeaveApplication();
leaveApplication.execute(applicant);
} else if (applicationType.equals("VenueApplication")) {
VenueApplication venueApplication = new VenueApplication();
venueApplication.execute(applicant);
} else {
System.out.println("Invalid application type");
}
/********** End *********/
}
}