万字解析设计模式之模板方法与解释器模式

一、模板方法模式

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 {@Overridepublic void pourVegetable() {System.out.println("下锅的蔬菜是包菜");}@Overridepublic void pourSauce() {System.out.println("下锅的酱料是辣椒");}
}
package com.yanyu.Template;public class ConcreteClass_CaiXin extends AbstractClass {@Overridepublic void pourVegetable() {System.out.println("下锅的蔬菜是菜心");}@Overridepublic 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方法用于解释上下文中的表达式,这里是返回变量对应的值@Overridepublic int interpret(Context ctx) {return ctx.getValue(this);}// 重写toString方法,返回变量名的字符串表示@Overridepublic 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方法用于解释上下文中的表达式,这里是返回值本身@Overridepublic int interpret(Context context) {return value;}// 重写toString方法,返回值的字符串表示@Overridepublic 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方法用于解释上下文中的表达式,这里是返回左右操作数的解释结果相加的值@Overridepublic int interpret(Context context) {return left.interpret(context) + right.interpret(context);}// 重写toString方法,返回加法表达式的字符串表示,形式为 (左操作数 + 右操作数)@Overridepublic 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;}@Overridepublic int interpret(Context context) {return left.interpret(context) - right.interpret(context);}@Overridepublic 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");  // 创建变量aVariable b = new Variable("b");  // 创建变量bVariable c = new Variable("c");  // 创建变量cVariable d = new Variable("d");  // 创建变量dVariable e = new Variable("e");  // 创建变量econtext.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)为例,模拟实现申请单处理流程。

实现方式

  1. 分析目标算法, 确定能否将其分解为多个步骤。 从所有子类的角度出发, 考虑哪些步骤能够通用, 哪些步骤各不相同;

  2. 创建抽象基类并声明一个模板方法和代表算法步骤的一系列抽象方法。 在模板方法中根据算法结构依次调用相应步骤。 可用 final 最终修饰模板方法以防止子类对其进行重写;

  3. 虽然可将所有步骤全都设为抽象类型, 但默认实现可能会给部分步骤带来好处, 因为子类无需实现那些方法;

  4. 可考虑在算法的关键步骤之间添加钩子;

  5. 为每个算法变体新建一个具体子类, 它必须实现所有的抽象步骤, 也可以重写部分可选步骤。

编程要求

根据提示,补充右侧编辑器文件Client.javaBegin-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{@Overridepublic void dataAnalysis(String data) {System.out.println("教职工请假申请单数据解析");}
}
public class VenueApplication extends ApplicationTemplate{@Overridepublic 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 *********/}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/215495.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HTTP/2:多路复用、服务器推送和首部压缩的革命

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

系统优化软件Bitsum Process Lasso Pro v12.4,供大家学习研究参考

1、自动或手动调整进程优先级;将不需要抑制的进程添加到排除列表; 2、设置动态提升前台运行的进程/线程的优先级 3、设置进程黑名单,禁止无用进程(机制为启动即结束,而非拦截其启动)。 4、优化I/O优先级以及电源模式自动化。 5、ProBalance功能。翻译成中文是“进程平衡…

【从删库到跑路】MySQL数据库 — E-R图 | 关系模型

&#x1f38a;专栏【MySQL】 &#x1f354;喜欢的诗句&#xff1a;更喜岷山千里雪 三军过后尽开颜。 &#x1f386;音乐分享【如愿】 大一同学小吉&#xff0c;欢迎并且感谢大家指出我的问题&#x1f970; 文章目录 &#x1f339;简述什么是E-R图⭐核心概念 &#x1f339;E-R图…

Windows power shell for循环

有时候需要重复执行某个shell命令 for($i1;$i -lt 10;$i$i1){echo $i}如果是cmd for /l %i in (1,1,5) do echo %i

typora中的快捷键shift enter 和 enter的交换

1 问题&#xff1a; 我最近在用 typora 进行写作&#xff0c;但是在合格 typora 的 markdown 编辑器很奇怪&#xff0c;它的一个回车符是两次换行&#xff0c;而用 shfit ent 找了半天都不知道怎么解决的这个问题&#xff0c;然后我就去了这个 typora 在 github 开源的问题仓库…

vitis AIE graph

https://zhuanlan.zhihu.com/p/661255763 在前文中,我们首先认识了 Vitis™ 2022.1 统一软件平台内适用于 Versal™ 的 AI 引擎 (AIE) 应用。 我们认识了 Vitis IDE 2022.1 中 AIE 应用工程的结构,还了解了用于计算图初始化、运行和终止的一些 API。在本文中,我们将进一步深…

忘记跟进客户?CRM系统来帮您

销售人员要同时跟进多个客户&#xff0c;经常容易忘记跟进客户&#xff0c;导致客户流失。这就代表您的企业需要使用CRM系统了。下面我们就来对这个问题聊聊&#xff0c;销售总忘记跟进客户&#xff1f;CRM客户管理了解一下。 CRM如何帮助销售跟进转化客户&#xff1a; 管理客…

【华为数通HCIP | 网络工程师】821-IGP高频题、易错题之OSPF(6)

个人名片&#xff1a; &#x1f43c;作者简介&#xff1a;一名大三在校生&#xff0c;喜欢AI编程&#x1f38b; &#x1f43b;‍❄️个人主页&#x1f947;&#xff1a;落798. &#x1f43c;个人WeChat&#xff1a;hmmwx53 &#x1f54a;️系列专栏&#xff1a;&#x1f5bc;️…

0基础能不能转行做网络安全?网络安全人才发展路线

最近有同学在后台留言&#xff0c;0基础怎么学网络安全&#xff1f;0基础可以转行做网络安全吗&#xff1f;以前也碰到过类似的问题&#xff0c;想了想&#xff0c;今天简单写一下。 我的回答是先了解&#xff0c;再入行。 具体怎么做呢&#xff1f; 首先&#xff0c;你要确…

单链表的反转?太细了哥们!细到离谱!

单链表的反转&#xff08;面试常出&#xff09;&#xff1a; ​ 单链表的反转&#xff0c;可以通过很多种方法实现。包括迭代法&#xff0c;递归法&#xff0c; 迭代法&#xff1a; 定义三个指针&#xff1a;prev、current和next&#xff0c;它们分别表示前一个节点、当前节点…

【开源】基于Vue.js的城市桥梁道路管理系统的设计和实现

项目编号&#xff1a; S 025 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S025&#xff0c;文末获取源码。} 项目编号&#xff1a;S025&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块三、系统展示四、核心代码4.1 查询城市桥…

Linux基本命令操作 —— 文件夹/文件的创建,删除,查看,重命名......(简单理解 快速上手)

目录 1. 基础命令 1.1 显示当前目录&#xff1a;pwd 1.2 改变当前目录&#xff1a;cd 2. 文件夹的操作命令 2.1 创建文件夹&#xff1a;mkdir 2.2 查看文件夹&#xff1a;ls 2.3 删除文件夹&#xff1a;rmdir &#xff08;不推荐&#xff09; 2.4 复制文件夹&#xff1…