设计模式
目录
一.模板方法模式
二.策略模式
三.命令模式
四.职责链模式
五.状态模式
六.观察者模式
七.中介者模式
八.迭代器模式
九.访问者模式
十.备忘录模式
十一.解释器模式
注:学习视频:黑马程序员Java设计模式
行为型模式
行为型模式用于描述在运行时复杂的控制流程,即描述了多个类或对象之间互相协调共同完成单个对象无法完成的任务,涉及到算法与对象之间的职责。
行为型模式分为类行为性模式和对象行为型模式,前者采用继承的机制在对象和类之间分配行为,后者采用组合或聚合的方式分配行为,后者采用组合与聚合的方式,所以的满足“合成服用原则”,耦合度低。
行为型模式分为:
- 模板方法模式
- 策略模式
- 命令模式
- 职责链模式
- 状态模式
- 观察者模式
- 中介者模式
- 迭代器模式
- 访问者模式
- 备忘录模式
- 解释器模式
以上 11 种行为型模式,除了模板方法模式和解释器模式是类行为型模式,其他的全部属于对象行为型模式。
一、模板方法模式
1.1 概述
在面向对象的设计过程中,设置一个系统关键步骤,并且已经知道了这些步骤的执行顺序,但有些步骤的具体实现位置,有些步骤的实现是固定的。
例如我们去医院看病,到了医院要进行排队、挂号、看诊、取药等操作,而排队、挂号是大家做的都是一样的行为,但是看诊、取药是根据每个人不同去不同的地方看诊,取不同的药,这样的问题就可以使用模板方法模式来设计
定义:
定义一个操作中的算法股价,而将算法的一些步骤延迟到子类,使得子类在不改变算法的结构情况下重写该算法的特定步骤。
1.2 结构
模板方法(Template)模式包含以下角色:
-
抽象类(Abstract Class): 负责给定一个算法的轮廓和骨架,并且定义若干基本方法。
- 基本方法分为三种
抽象方法: 一个抽象方法由抽象类声明,这些方法往往是需要子类的实现其特定功能的方法。
具体方法: 一个具体方法由抽象类声明并且实现,是模板方法里面所有人都需要执行的基本方法。
钩子方法: 在抽象类中已经实现,包括用于判断逻辑方法和需要子类重写的空方法两种。
-
具体子类: 实现抽象类中的抽象方法和钩子方法,它们是一个顶级逻辑组成步骤。实际上就是实现一些特定的功能。
1.3 案例
【例】炒菜
炒菜的步骤是固定的,分为倒油、热油、倒蔬菜、倒调味料等步骤,使用模板方法实现。
类图如下:
代码如下:
/**
* 抽象类
*/
public abstract class AbstractClass {
// 定义炒菜流程算法
public void cookProcess(){
// 倒油
pourOil();
// 热油
heatOil();
// 倒蔬菜
pourVegetable();
// 倒调料
pourSource();
}
// 倒油
public void pourOil(){
System.out.println("倒油");
}
// 热油
public void heatOil(){
System.out.println("热油");
}
// 倒蔬菜(抽象)
public abstract void pourVegetable();
// 倒调料(抽象)
public abstract void pourSource();
}
/**
* 炒卷心菜(子类)
*/
public class Cook_Cabbage extends AbstractClass{
@Override
public void pourVegetable() {
System.out.println("倒卷心菜");
}
@Override
public void pourSource() {
System.out.println("倒五香粉");
}
}
/**
* 炒胡萝卜(子类)
*/
public class Cook_Carrot extends AbstractClass{
@Override
public void pourVegetable() {
System.out.println("倒胡萝卜");
}
@Override
public void pourSource() {
System.out.println("倒辣椒");
}
}
// 测试类
public class Client {
public static void main(String[] args) {
Cook_Cabbage cook_cabbage = new Cook_Cabbage();
// 调用父类的炒菜
cook_cabbage.cookProcess();
}
}
// 测试结果
倒油
热油
倒卷心菜
倒五香粉
为了防止恶意破坏,模板方法一般都加final关键字
1.4 优缺点
优点:
- 提高代码的复用性: 将相同的代码抽取倒父类中,不同的代码在子类编写
- 实现反向控制: 通过一个父类调用其子类实现的方法,通过子类实现方法的不同,实现了反向控制,符合“开闭原则”
缺点:
- 对每个不同的实现都需要定义一个子类,会造成类数量增大
- 父类的抽象方法由子类来实现,使代码的阅读难度增大
1.5 使用场景
- 算法步骤固定,其中的某些步骤是一样的,某些步骤是不一样的,就可以使用模板方法模式,将一样的步骤抽取到父类中,不同的步骤由子类来实现。
- 需要子类决定某些父类的步骤,可以实现反向控制。
3.6 JDK源码分析
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)
方法,所以在此处重点看的方法是带三个参数的方法。
总结如下:
在InputStream父类中已经定义好了读取一个字节数组数据的方法是每次读取一个字节,并将其存储到数组的第一个索引位置,读取len个字节数据。具体如何读取一个字节数据呢?由子类实现。
二、策略模式
2.1 概述
先看下面的图片,我们去旅游选择出行模式有很多种,可以骑自行车、可以坐汽车、可以坐火车、可以坐飞机。
作为一个程序猿,开发需要选择一款开发工具,当然可以进行代码开发的工具有很多,可以选择Idea进行开发,也可以使用eclipse进行开发,也可以使用其他的一些开发工具。
定义:
该模式定义了一系列算法,并且每个算法封装起来,使他们可以互相替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对象算法的封装,把算法职责和算法的具体实现分离(其实就是将算法抽取到接口中,调用直接调用接口就可以,而算法的具体实现就交给具体实现类来处理),并委派给不同对象对这些算法进行管理。
2.2 结构
策略模式的主要角色如下:
- 抽象(Strategy)策略类: 这是一个抽象角色,通常用接口或抽象类来实现,此角色给出了所有具体策略所需的接口。
- 具体(Concrete Strategy)策略类: 实现了抽象策略定义的接口,提供具体的算法实现。
- 环境(Context)类: 持有一个策略类的引用给客户端调用。
2.3 案例
【例】促销活动
一家百货公司在定年度的促销活动。针对不同的节日(春节、中秋节、圣诞节)推出不同的促销活动,由促销员将促销活动展示给客户。
类图如下:
代码如下:
抽象策略类
/**
* 抽象策略类
*/
public interface Strategy {
void show();
}
具体策略类(实现抽象策略类)
// 策略A (具体策略类)
public class StrategyA implements Strategy{
@Override
public void show() {
System.out.println("买一送一");
}
}
// 策略B (具体策略类)
public class StrategyB implements Strategy{
@Override
public void show() {
System.out.println("满两百送洗衣液");
}
}
// 策略C (具体策略类)
public class StrategyC implements Strategy{
@Override
public void show() {
System.out.println("满1000打八折");
}
}
环境类(*管理抽象类)
// 售货员 环境类,管理具体策略类,供客户调用
public class SaleMan {
// 聚合所有策略
private Strategy strategy;
public SaleMan(Strategy strategy) {
this.strategy = strategy;
}
public Strategy getStrategy() {
return strategy;
}
public void setStrategy(Strategy strategy) {
this.strategy = strategy;
}
// 提供策略调用方法
public void useStrategy(){
this.strategy.show();
}
}
测试类
// 测试类
public class Client {
public static void main(String[] args) {
// 通过环境类调用策略
SaleMan saleMan = new SaleMan(new StrategyA());
// 使用策略
saleMan.useStrategy();
System.out.println("===================");
// 通过环境类调用策略
saleMan.setStrategy(new StrategyB());
// 使用策略
saleMan.useStrategy();
}
}
// 测试结果
买一送一
===================
满两百送洗衣液
策略可能会被创建多次,采用享元模式改进
// 策略工厂类
public class StrategyFactory {
// 创建享元对象集合
private Map<String ,Strategy> strategyMap = new HashMap<>();
// 初始化享元对象集合
private StrategyFactory() {
strategyMap.put("A",new StrategyA());
strategyMap.put("B",new StrategyB());
strategyMap.put("C",new StrategyC());
}
// 提供添加
public void add(String strategyName,Strategy strategy){
strategyMap.put(strategyName,strategy);
}
// 移除
public void remove(String strategyName){
strategyMap.remove(strategyName);
}
// 提供获取策略的方法
public Strategy getStrategy(String strategyName){
return strategyMap.get(strategyName);
}
// 将工厂设置成单例模式
public static StrategyFactory getInstance(){
return StrategyFactoryHolder.strategyFactory;
}
public static class StrategyFactoryHolder{
// 静态内部类可以调用外部的私有方法
private static final StrategyFactory strategyFactory = new StrategyFactory();
}
}
// 测试类
public class Client {
public static void main(String[] args) {
StrategyFactory instance = StrategyFactory.getInstance();
// 通过环境类调用策略
SaleMan saleMan = new SaleMan(instance.getStrategy("A"));
// 使用策略
saleMan.useStrategy();
System.out.println("===================");
// 通过环境类调用策略
saleMan.setStrategy(instance.getStrategy("B"));
// 使用策略
saleMan.useStrategy();
}
}
//测试结果和上面一致
买一送一
===================
满两百送洗衣液
2.4 优缺点
优点:
- 策略类之间可以互相替换: 策略类实现了同一个接口,所以他们之间可以互相替换。
- 易于扩展: 如果需要增加策略,只需要实现抽象策略类接口,原来代码无需改变,符合”开闭原则“。
- 避免多重选择条件语句,充分体现面向对象设计思想。
缺点:
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
- 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量(上面已经实现)。
2.5 使用场景
- 一个系统在几个算法中选择一个时,可以将每个算法封装成策略,使用策略模式
- 一个类中定义了多种行为,且每个行为是以条件判断的形式来选择执行的,这个时候就可以将每个条件分支封装到策略中,用策略类来代替条件分支。
- 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时。
- 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。
2.6 JDK 源码分析
Comparator
中的策略模式。在Arrays类中有一个sort()
方法,如下:
public class Arrays{
public static <T> void sort(T[] a, Comparator<? super T> c) {
if (c == null) {
sort(a);
} else {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a, c);
else
TimSort.sort(a, 0, a.length, c, null, 0, 0);
}
}
}
Arrays就是一个环境角色类,这个sort方法传入一个新策略让Arrrays根据新策略进行排序,这里抽象策略类就是
Comparator
,而我们定义的匿名内部类就是具体策略类。
三、命令模式
3.1 概述
日常生活中,我们出去吃饭都会遇到下面的场景。
定义:
讲一个请求封装成一个对象,是发出请求的责任和执行请求的责任分离,这样两者之间就通过命令对象进行沟通,这样方便命令对象进行存储、传递、调用、增加与管理。
3.2 结构
命令模式包含以下主要角色:
- 抽象命令角色: 定义命令的接口,声明命令的执行方法。
- 具体命角色: 实现命令接口,真正执行命令的对象,通常会持有接受者,并调用接受者的方法来完成命令。
- 实现者/接收者角色: 接收者,真正执行命令的对象,任何类都可能称为接受者,只要能够实现命令要求实现的相应功能。
- 调用者角色: 调用命令的对象,通常持有命令角色,并且执行命令完成任务,相当于使用命令对象的入口。
3.3 案例
实现上面图中的命令
服务员:调用者角色,发出命令
资深大厨:接受者,真正执行命令的对象
订单: 命令中包含订单
类图如下:
代码如下:
抽象命令类和具体命令类
/**
* 抽象命令角色
*/
public interface Command {
// 定义调用命令的方法
void exec();
}
/**
* 具体命令类
*/
public class ConCreateCommand implements Command{
// 持有接受者
private SeniorChef chef;
// 聚合订单
private Order order;
public ConCreateCommand(SeniorChef chef, Order order) {
this.chef = chef;
this.order = order;
}
// 让接受者来执行命令
@Override
public void exec() {
Set<String> foodNames = order.getMap().keySet();
System.out.println(order.getTableNum() + "号桌订单");
for (String foodName : foodNames) {
Integer foodNum = order.getMap().get(foodName);
chef.cook(foodName,foodNum);
}
}
}
调用者和接收者
/**
* 服务员 调用者
*/
public class Waitor {
// 持有多个命令对象
private List<Command> commandList =new ArrayList<>();
// 添加命令
public void addCommand(Command command){
commandList.add(command);
}
// 执行多个命令
public void orderUp(){
System.out.println("有人下单了..:");
for (Command command : commandList) {
System.out.println("====================");
command.exec();
}
}
}
/**
* 资深厨师类
*/
public class SeniorChef {
public void cook(String foodName,Integer foodNum){
System.out.println(foodNum + "份" + foodName);
}
}
订单类和测试类
/**
* 订单信息
*/
public class Order {
// 桌号
private Integer tableNum;
// 菜单
private Map<String, Integer> map;
public Order(Integer tableNum, Map<String,Integer> map) {
this.tableNum = tableNum;
this.map = map;
}
public Integer getTableNum() {
return tableNum;
}
public void setTableNum(Integer tableNum) {
this.tableNum = tableNum;
}
public Map<String,Integer> getMap() {
return map;
}
public void setMap( Map<String,Integer> map) {
this.map = map;
}
}
// 测试类
public class Client {
public static void main(String[] args) {
// 创建接收者 厨师类,作为命令对象的参数
SeniorChef seniorChef = new SeniorChef();
Map<String, Integer> map = new HashMap<>();
map.put("黄焖鸡米饭",1);
map.put("可乐",2);
// 菜单1,作为命令对象的参数
Order order = new Order(1, map);
Map<String, Integer> map1 = new HashMap<>();
map1.put("黄焖鸡米饭",1);
map1.put("可乐",2);
// 菜单2,作为命令对象的参数
Order order1 = new Order(2, map);
// 创建两个命令
ConCreateCommand command = new ConCreateCommand(seniorChef, order);
ConCreateCommand command1 = new ConCreateCommand(seniorChef, order1);
// 创建调用者 服务员
Waitor waitor = new Waitor();
waitor.addCommand(command);
waitor.addCommand(command1);
// 调用者执行命令
waitor.orderUp();
}
}
// 测试结果
有人下单了..:
====================
1号桌订单
2份可乐
1份黄焖鸡米饭
====================
2号桌订单
2份可乐
1份黄焖鸡米饭
3.4 优缺点
优点:
- 降低了系统的耦合度: 命令模式将调用操作和执行者解耦
- 增加和删除命令方便: 最简单的实现就是将命令存储到集合中,如果方便管理,需要增加命令只需增加实现类,符合“开闭原则”
- 可以实现宏命令: 命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
- 方便实现 Undo 和 Redo 操作: 命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。
缺点:
- 使用命令模式可能会导致某些系统有过多的具体命令类。
- 系统结构复杂。
3.5 使用场景
- 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
- 系统需要在不同的时间指定请求、将请求排队和执行请求。
- 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作
3.6 JDK源码分析
Runable是一个典型命令模式,Runnable担当命令的角色,Thread充当的是调用者,start方法就是其执行方法
//命令接口(抽象命令角色)
public interface Runnable {
public abstract void run();
}
//调用者 Thread
public class Thread implements Runnable {
private Runnable target;
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
private native void start0();
}
会调用一个native方法start0(),调用系统方法,开启一个线程。而接收者是对程序员开放的,可以自己定义接收者。
/**
* jdk Runnable 命令模式
* TurnOffThread : 属于具体命令类
*/
public class TurnOffThread implements Runnable{
// 持有接受者
private Receiver receiver;
public TurnOffThread(Receiver receiver) {
this.receiver = receiver;
}
// 重写抽象命令方法的执行接口
public void run() {
receiver.turnOFF();
}
}
接收者和测试类
// 接收者
public class Receiver {
public void run(){
System.out.println("线程执行了");
}
}
/**
* 测试类
*/
public class Demo {
public static void main(String[] args) {
Receiver receiver = new Receiver();
TurnOffThread turnOffThread = new TurnOffThread(receiver);
Thread thread = new Thread(turnOffThread);
thread.start();
}
}
// 测试结果
线程执行了
四、责任链模式
4.1 概述
在现实生活中,常常会出现这样的事例:一个请求有多个对象可以处理,但每个对象的处理条件或权限不同。例如,公司员工请假,可批假的领导有部门负责人、副总经理、总经理等,但每个领导能批准的天数不同,员工必须根据自己要请假的天数去找不同的领导签名,也就是说员工必须记住每个领导的姓名、电话和地址等信息,这增加了难度。这样的例子还有很多,如找领导出差报销、生活中的“击鼓传花”游戏等。
定义:
又名职责链模式,为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
4.2 结构
- 抽象处理者类: 定义一个处理请求的接口,包含抽象处理方法和一个后续连接。
- 具体处理者:实现抽象处理者的方法,在方法中判断是否能够处理本次请求,如果可以后处理则处理,不能处理就交给后继者处理。
- 客户类角色:创建处理链,并向链头的具体处理者对象提交请求,它不关系处理袭击和请求的传递过程。
4.3 案例
现需要开发一个请假流程控制系统。请假一天以下的假只需要小组长同意即可;请假1天到3天的假还需要部门经理同意;请求3天到7天还需要总经理同意才行
类图如下:
代码如下:
抽象处理类和请求信息类
/**
* 抽象处理类
*/
public abstract class Handler {
// 请假天数常量
protected final static Integer NUM_ONE = 1;
protected final static Integer NUM_THREE = 3;
protected final static Integer NUM_SEVEN = 7;
// 定义请假天数
private Integer startNum;
private Integer endNum;
public Handler(Integer endNum) {
this.endNum = endNum;
}
public Handler(Integer startNum, Integer endNum) {
this.startNum = startNum;
this.endNum = endNum;
}
// 后续者
private Handler nextHandler;
// 提供设置后继者的方法
public void setNextHandler(Handler handler){
this.nextHandler = handler;
}
// 处理请假方法
protected abstract void handlerLeave(LeaveRequest leaveRequest);
// 提供处理方法
public void dealLeave(LeaveRequest leaveRequest){
if(leaveRequest.getNum() > this.startNum){
// 如果自己能处理就自己处理
this.handlerLeave(leaveRequest);
if(leaveRequest.getNum() > this.endNum && this.nextHandler !=null){
// 不能处理交给后继者
this.nextHandler.dealLeave(leaveRequest);
}else {
System.out.println("流程结束!!");
}
}
}
public Integer getEndNum() {
return endNum;
}
}
/**
* 请求信息
*/
public class LeaveRequest {
private String name;//姓名
private int num;//请假天数
private String content;//请假内容
public LeaveRequest(String name, int num, String content) {
this.name = name;
this.num = num;
this.content = content;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
具体处理类
/**
* 小组长处理类 (具体处理类)
*/
public class GroupLeader extends Handler{
public GroupLeader() {
super(0, NUM_ONE);
}
@Override
protected void handlerLeave(LeaveRequest leaveRequest) {
if (leaveRequest.getNum() <= this.getEndNum()){
System.out.println(leaveRequest.getName() + "因" + leaveRequest.getContent() + "请假" + leaveRequest.getNum()+"天");
System.out.println("小组长审批:同意");
}
}
}
/**
* 经理处理类(具体处理类)
*/
public class Manager extends Handler{
public Manager() {
super(NUM_ONE, NUM_THREE);
}
@Override
protected void handlerLeave(LeaveRequest leaveRequest) {
if (leaveRequest.getNum() <= this.getEndNum()){
System.out.println(leaveRequest.getName() + "因" + leaveRequest.getContent() + "请假" + leaveRequest.getNum()+"天");
System.out.println("经理审批:同意");
}
}
}
/**
* 总经理处理类 (具体处理类)
*/
public class GeneralManager extends Handler{
public GeneralManager() {
super(NUM_THREE, NUM_SEVEN);
}
@Override
protected void handlerLeave(LeaveRequest leaveRequest) {
if (leaveRequest.getNum() <= this.getEndNum()){
System.out.println(leaveRequest.getName() + "因" + leaveRequest.getContent() + "请假" + leaveRequest.getNum() +"天");
System.out.println("总经理审批:同意");
}
}
}
客户类
/**
* 客户类
*/
public class Client {
public static void main(String[] args) {
// 创建请求
LeaveRequest leaveRequest = new LeaveRequest("张三", 1, "感冒发烧");
// 创建责任链
GroupLeader groupLeader = new GroupLeader();
Manager manager = new Manager();
GeneralManager generalManager = new GeneralManager();
generalManager.setNextHandler(manager);
manager.setNextHandler(generalManager);
groupLeader.dealLeave(leaveRequest);
}
}
// 测试结果
张三因感冒发烧请假1天
小组长审批:同意
流程结束!!
4.4 优缺点
优点:
- 降低对象之间的耦合度: 该模式降低了发送请求者和接受者的耦合度
- 增强系统可扩展性: 如果需要添加具体处理类,只需要调整责任链顺序或直接添加。
- 增强了给对象指派任务的灵活性: 当工作流程发生变化,可以动态地改变链内的成员或者修改它们的次序,也可动态地新增或者删除责任。
- 责任链简化了对象之间的连接: 一个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。
- 责任分担: 每个类只需要处理自己该处理的工作,不能处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。
缺点:
- 不能保证每个请求一定被处理: 由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。
- 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
- 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。
4.5 源码解析
在javaWeb应用开发中,FilterChain是职责链(过滤器)模式的典型应用,以下是Filter的模拟实现分析:
-
模拟web请求Request以及web响应Response
public interface Request{ } public interface Response{ }
-
模拟web过滤器Filter
public interface Filter { public void doFilter(Request req,Response res,FilterChain c); }
-
模拟实现具体过滤器
public class FirstFilter implements Filter { @Override public void doFilter(Request request, Response response, FilterChain chain) { System.out.println("过滤器1 前置处理"); // 先执行所有request再倒序执行所有response chain.doFilter(request, response); System.out.println("过滤器1 后置处理"); } } public class SecondFilter implements Filter { @Override public void doFilter(Request request, Response response, FilterChain chain) { System.out.println("过滤器2 前置处理"); // 先执行所有request再倒序执行所有response chain.doFilter(request, response); System.out.println("过滤器2 后置处理"); } }
-
模拟实现过滤器链FilterChain
public class FilterChain { private List<Filter> filters = new ArrayList<Filter>(); private int index = 0; // 链式调用 public FilterChain addFilter(Filter filter) { this.filters.add(filter); return this; } public void doFilter(Request request, Response response) { if (index == filters.size()) { return; } Filter filter = filters.get(index); index++; filter.doFilter(request, response, this); } }
-
测试类
public class Client { public static void main(String[] args) { Request req = null; Response res = null ; FilterChain filterChain = new FilterChain(); filterChain.addFilter(new FirstFilter()).addFilter(new SecondFilter()); filterChain.doFilter(req,res); } }
总结:
对于一个请求根据不同权限给不同的类处理,并且这些类之间存在着类似的功能,例如这里的过滤器,需要通过多个过滤器,就可以将它们连成链,按照顺序执行,我们只负责将请求发送给链头,并不关心里面是怎么实现的。
五、状态模式
5.1 概述
【例】通过按钮来控制一个电梯的状态,一个电梯有开门状态,关门状态,停止状态,运行状态。每一种状态改变,都有可能要根据其他状态来更新处理。例如,如果电梯门现在处于运行时状态,就不能进行开门操作,而如果电梯门是停止状态,就可以执行开门操作。
类图如下:
代码如下:
public interface ILift {
//电梯的4个状态
//开门状态
public final static int OPENING_STATE = 1;
//关门状态
public final static int CLOSING_STATE = 2;
//运行状态
public final static int RUNNING_STATE = 3;
//停止状态
public final static int STOPPING_STATE = 4;
//设置电梯的状态
public void setState(int state);
//电梯的动作
public void open();
public void close();
public void run();
public void stop();
}
public class Lift implements ILift {
private int state;
@Override
public void setState(int state) {
this.state = state;
}
//执行关门动作
@Override
public void close() {
switch (this.state) {
case OPENING_STATE:
System.out.println("电梯关门了。。。");//只有开门状态可以关闭电梯门,可以对应电梯状态表来看
this.setState(CLOSING_STATE);//关门之后电梯就是关闭状态了
break;
case CLOSING_STATE:
//do nothing //已经是关门状态,不能关门
break;
case RUNNING_STATE:
//do nothing //运行时电梯门是关着的,不能关门
break;
case STOPPING_STATE:
//do nothing //停止时电梯也是关着的,不能关门
break;
}
}
//执行开门动作
@Override
public void open() {
switch (this.state) {
case OPENING_STATE://门已经开了,不能再开门了
//do nothing
break;
case CLOSING_STATE://关门状态,门打开:
System.out.println("电梯门打开了。。。");
this.setState(OPENING_STATE);
break;
case RUNNING_STATE:
//do nothing 运行时电梯不能开门
break;
case STOPPING_STATE:
System.out.println("电梯门开了。。。");//电梯停了,可以开门了
this.setState(OPENING_STATE);
break;
}
}
//执行运行动作
@Override
public void run() {
switch (this.state) {
case OPENING_STATE://电梯不能开着门就走
//do nothing
break;
case CLOSING_STATE://门关了,可以运行了
System.out.println("电梯开始运行了。。。");
this.setState(RUNNING_STATE);//现在是运行状态
break;
case RUNNING_STATE:
//do nothing 已经是运行状态了
break;
case STOPPING_STATE:
System.out.println("电梯开始运行了。。。");
this.setState(RUNNING_STATE);
break;
}
}
//执行停止动作
@Override
public void stop() {
switch (this.state) {
case OPENING_STATE: //开门的电梯已经是是停止的了(正常情况下)
//do nothing
break;
case CLOSING_STATE://关门时才可以停止
System.out.println("电梯停止了。。。");
this.setState(STOPPING_STATE);
break;
case RUNNING_STATE://运行时当然可以停止了
System.out.println("电梯停止了。。。");
this.setState(STOPPING_STATE);
break;
case STOPPING_STATE:
//do nothing
break;
}
}
}
public class Client {
public static void main(String[] args) {
Lift lift = new Lift();
lift.setState(ILift.STOPPING_STATE);//电梯是停止的
lift.open();//开门
lift.close();//关门
lift.run();//运行
lift.stop();//停止
}
}
问题分析:
- 使用了大量的switch…case这样的判断(if…else也是一样),使程序的可阅读性变差。
- 扩展性很差。如果新加了断电的状态,我们需要修改上面判断逻辑
定义:
对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。
5.2 结构
状态模式包含以下主要角色:
- 环境(Context)角色: 也称为上下文,它定义了客户程序需要的接口,维护一个当前状态,并将与状态相关的操作委托给当前状态对象来处理。
- 抽象状态(State)角色: 定义一个接口,用以封装环境对象中的特定状态所对应的行为。
- 具体状态(Concrete State)角色: 实现抽象状态所对应的行为。
5.3 案例实现
对上述电梯的案例使用状态模式进行改进。类图如下:
代码如下:
//抽象状态类
public abstract class LiftState {
//定义一个环境角色,也就是封装状态的变化引起的功能变化
protected Context context;
public void setContext(Context context) {
this.context = context;
}
//电梯开门动作
public abstract void open();
//电梯关门动作
public abstract void close();
//电梯运行动作
public abstract void run();
//电梯停止动作
public abstract void stop();
}
//开启状态
public class OpenningState extends LiftState {
//开启当然可以关闭了,我就想测试一下电梯门开关功能
@Override
public void open() {
System.out.println("电梯门开启...");
}
@Override
public void close() {
//状态修改
super.context.setLiftState(Context.closeingState);
//动作委托为CloseState来执行,也就是委托给了ClosingState子类执行这个动作
super.context.getLiftState().close();
}
//电梯门不能开着就跑,这里什么也不做
@Override
public void run() {
//do nothing
}
//开门状态已经是停止的了
@Override
public void stop() {
//do nothing
}
}
//运行状态
public class RunningState extends LiftState {
//运行的时候开电梯门?你疯了!电梯不会给你开的
@Override
public void open() {
//do nothing
}
//电梯门关闭?这是肯定了
@Override
public void close() {//虽然可以关门,但这个动作不归我执行
//do nothing
}
//这是在运行状态下要实现的方法
@Override
public void run() {
System.out.println("电梯正在运行...");
}
//这个事绝对是合理的,光运行不停止还有谁敢做这个电梯?!估计只有上帝了
@Override
public void stop() {
super.context.setLiftState(Context.stoppingState);
super.context.stop();
}
}
//停止状态
public class StoppingState extends LiftState {
//停止状态,开门,那是要的!
@Override
public void open() {
//状态修改
super.context.setLiftState(Context.openningState);
//动作委托为CloseState来执行,也就是委托给了ClosingState子类执行这个动作
super.context.getLiftState().open();
}
@Override
public void close() {//虽然可以关门,但这个动作不归我执行
//状态修改
super.context.setLiftState(Context.closeingState);
//动作委托为CloseState来执行,也就是委托给了ClosingState子类执行这个动作
super.context.getLiftState().close();
}
//停止状态再跑起来,正常的很
@Override
public void run() {
//状态修改
super.context.setLiftState(Context.runningState);
//动作委托为CloseState来执行,也就是委托给了ClosingState子类执行这个动作
super.context.getLiftState().run();
}
//停止状态是怎么发生的呢?当然是停止方法执行了
@Override
public void stop() {
System.out.println("电梯停止了...");
}
}
//关闭状态
public class ClosingState extends LiftState {
@Override
//电梯门关闭,这是关闭状态要实现的动作
public void close() {
System.out.println("电梯门关闭...");
}
//电梯门关了再打开,逗你玩呢,那这个允许呀
@Override
public void open() {
super.context.setLiftState(Context.openningState);
super.context.open();
}
//电梯门关了就跑,这是再正常不过了
@Override
public void run() {
super.context.setLiftState(Context.runningState);
super.context.run();
}
//电梯门关着,我就不按楼层
@Override
public void stop() {
super.context.setLiftState(Context.stoppingState);
super.context.stop();
}
}
//环境角色
public class Context {
//定义出所有的电梯状态
public final static OpenningState openningState = new OpenningState();//开门状态,这时候电梯只能关闭
public final static ClosingState closeingState = new ClosingState();//关闭状态,这时候电梯可以运行、停止和开门
public final static RunningState runningState = new RunningState();//运行状态,这时候电梯只能停止
public final static StoppingState stoppingState = new StoppingState();//停止状态,这时候电梯可以开门、运行
//定义一个当前电梯状态
private LiftState liftState;
public LiftState getLiftState() {
return this.liftState;
}
public void setLiftState(LiftState liftState) {
//当前环境改变
this.liftState = liftState;
//把当前的环境通知到各个实现类中
this.liftState.setContext(this);
}
public void open() {
this.liftState.open();
}
public void close() {
this.liftState.close();
}
public void run() {
this.liftState.run();
}
public void stop() {
this.liftState.stop();
}
}
//测试类
public class Client {
public static void main(String[] args) {
Context context = new Context();
context.setLiftState(new ClosingState());
context.open();
context.close();
context.run();
context.stop();
}
}
5.4 优缺点
优点:
- 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
- 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
缺点:
- 状态模式的使用必然会增加系统类和对象的个数。
- 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
- 状态模式对"开闭原则"的支持并不太好。
5.5 使用场景
- 当一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为时,就可以考虑使用状态模式。
- 一个操作中含有庞大的分支结构,并且这些分支决定于对象的状态时。
六、观察者模式
6.1 概述
定义:
又称为发布-订阅模式,它定了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知说有的观察者对象,使它们可以自动的更新自己。
6.2 结构
在观察者模式中有如下角色:
- 抽象主题角色(Subject): 抽象被观察者,抽象主题角色把所有的观察者对象保存到一个集合中,每个主题都可以有任意数量观察者,抽象主题提供一个接口,可以增加、删除、通知观察者对象。
- 具体主题角色(ConcreteSubject): 具体主题角色,实现抽象主题角色接口方法,该角色将有关状态存入到具体观察者对象中,在具体主题内部状态发生改变的时候,给所有观察者发送通知。
- 抽象观察者(Observer): 是观察者的抽象类,它定义了一个更新接口,使得到主题更改通知后,改变自己的状态。
- 具体观察者类(ConcreteObserver): 实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身状态。
6.3 案例
【例】微信公众号
在使用微信公众号时,我们会发现当我们订阅了某个公众号的时候,当公众号状态发生改变的时候,也就是推送新的文章的时候,所有订阅了的用户都可以接受到,而这里公众号就是主题角色,用户就是观察者角色。
类图如下:
代码如下:
抽象主题类和具体主题类(微信公众号)
/**
* 抽象主题类
*/
public abstract class Subject {
// 聚合所有的用户(观察者对象)
List<Observer> userList = new ArrayList<>();
// 提供添加用户
public void attach(Observer observer){
this.userList.add(observer);
}
// 删除用户
public void detach(Observer observer){
this.userList.remove(observer);
}
// 更新信息通知观察者方法
public abstract void notify(String message);
}
/**
* 微信公众号类(具体主题类)
*/
public class WeChatSubject extends Subject{
@Override
public void notify(String message) {
for (Observer observer : this.userList) {
observer.update(message);
}
}
}
抽象观察者和具体观察者类(微信用户)
/**
* 抽象观察者类
*/
public abstract class Observer {
// 定义更新本身信息方法
abstract void update(String message);
}
/**
* 微信用户(具体观察者类)
*/
public class UserObserver extends Observer{
private String name;
public UserObserver(String name) {
this.name = name;
}
// 定义改变自己状态的方法,即接收到主题类的状态更新,改变自己的状态
@Override
void update(String message) {
System.out.println(name +"-" + message);
}
}
测试类
public class Client {
public static void main(String[] args) {
// 创建主题类,即创建微信公众号
WeChatSubject weChatSubject = new WeChatSubject();
// 创建观察者,即微信用户,并订阅公众号
weChatSubject.attach( new UserObserver("张三"));
weChatSubject.attach( new UserObserver("李四"));
weChatSubject.attach( new UserObserver("王五"));
// 主题类状态改变,即公众号推送消息
weChatSubject.notify("该吃饭了,我的宝!!!");
}
}
// 测试结果
张三-该吃饭了,我的宝!!!
李四-该吃饭了,我的宝!!!
王五-该吃饭了,我的宝!!!
6.4 优缺点
优点:
- 降低了目标(主题类)和观察者之间的耦合关系,两者之间变成了抽象耦合关系。
- 被观察者(主题类)发送通知,所有注册的观察者都会收到消息(可以实现广播功能)
缺点:
- 如果观察者非常多的话,那么所有观察者收到被观察者信息会有一定的时差,因为我们是遍历集合通知的。
- 如果观察者有循环依赖的话,那么被观察者发送通知会使观察者循环调用,会导致系统崩溃。
6.5 使用场景
- 对象之间存在一对多关系,一个对象状态发生改变会影响其他对象。
- 当一个抽象模型有两个方面。其中一个方面依赖于另一个方面时。
6.6 JDK中提供实现
在Java中,通过
java.util.Observable
类和java.util.Observer
接口定义了观察者模式,只要实现它们的子类都可以编写观察者模式。
- Observable类
Observable
类是抽象目标类(被观察者),它有一个Vector
集合成员变量,用于保存所有要通知的观察者对象,下面来介绍它最重要的 3 个方法。
void addObserver(Observer o)
方法:用于将新的观察者对象添加到集合中。void notifyObservers(Object arg)
方法:调用集合中的所有观察者对象的 update方法,通知它们数据发生改变。通常越晚加入集合的观察者越先得到通知。void setChange()
方法:用来设置一个 boolean 类型的内部标志,注明目标对象发生了变化。当它为true时,notifyObservers()
才会通知观察者。
- Observer接口
Observer接口是抽象观察者,它监视目标对象发生变化时,观察者得到通知,并调用update方法,更新自身状态.
【例】警察抓小偷
警察作为观察者类,观察小偷对象,小偷对象作为被观察者类,如果小偷偷了东西,就会通过
notifyObservers
方法通知观察者类(警察)
代码如下:
小偷(被观察者类) 继承
Observable
类
/**
* 被观察类(小偷) 继承 Observable类
*/
public class Thief extends Observable {
private String name;
public Thief(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void steal(){
System.out.println("小偷:我在偷东西啊");
// 改变状态
setChanged();
// 通知观察者,这里会直接调用观察者对象的update方法
notifyObservers();
}
}
警察(观察者类) 实现
Observer接口
重写update
方法
/**
* 观察者(警察) 实现 Observer
*/
public class Police implements Observer {
private String name;
public Police(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public void update(Observable o, Object arg) {
System.out.println("警察" + name + "抓到小偷" + ((Thief)o).getName() +"正在偷东西!");
}
}
测试类
public class Client {
public static void main(String[] args) {
// 创建被观察者类
Thief thief= new Thief("张三");
// 创建观察者类,添加到被观察类中的观察者集合当中
thief.addObserver(new Police("公安李四"));
// 被观察者类状态发生变化
thief.steal();
}
}
// 测试结果
小偷:我在偷东西啊
警察公安李四抓到小偷张三正在偷东西!
七、中介者模式
7.1 概述
一般来说,同事类之间的关系是比较复杂的,多个同事类之间互相关联时,他们之间的关系会呈现为复杂的网状结构,这是一种过度耦合的架构,即不利于类的复用,也不稳定。例如在下左图中,有六个同事类对象,假如对象1发生变化,那么将会有4个对象受到影响。如果对象2发生变化,那么将会有5个对象受到影响。也就是说,同事类之间直接关联的设计是不好的。
如果引入中介者模式,那么同事类之间的关系将变为星型结构,从下右图中可以看到,任何一个类的变动,只会影响的类本身,以及中介者,这样就减小了系统的耦合。一个好的设计,必定不会把所有的对象关系处理逻辑封装在本类中,而是使用一个专门的类来管理那些不属于自己的行为。
定义:
又及哦啊做调停模式,定义一个中介者模式来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互
7.2 结构
中介者模式包含一下角色:
- 抽象中介者角色: 它是中介者的接口,提供了同时对象注册和转发同时对象信息的抽象方法。
- 具体中介者角色: 实现了抽象中介者角色,定义一个list集合来管理同事对象,协调各个同事之间的关系,因此它依赖于同时角色。
- 抽象同事类角色: 定义同事类接口,保存中介者对象,提供同事交互抽象方法,实现所有相互影响的同事类的公共功能。
- 具体同时类对象: 实现抽象类角色,当需要与其他同事交互的时,由中介者独享负责后续的交互。
7.3 案例
【例】租房
现在租房基本上都是通过房屋中介,租房者从房屋中介获取房屋信息,房主将房屋信息提供给的中介,中介通过对租房的不同要求,返回给租房者具体房屋信息。
类图如下:
代码如下:
抽象中介(提供交流方法)
/**
* 抽象中介者
*/
public abstract class Mediator {
// 提供交互方法
abstract void constact(String message,Person person);
}
抽象同事类
/**
* 抽象同事类
*/
public abstract class Person {
// 姓名
protected String name;
// 中介者类
protected Mediator mediator;
public Person(String name, Mediator mediator) {
this.name = name;
this.mediator = mediator;
}
}
具体同事类
/**
* 房主类(具体同事类)
*/
public class HouseOwner extends Person{
public HouseOwner(String name, Mediator mediator) {
super(name, mediator);
}
// 房主与中介交互
public void constact(String message){
// 自己和中介交互message信息
mediator.constact(message,this);
}
// 房主获取来自租房者的信息
public void getMessageFromTenant(String message){
System.out.println("房主" + name + "获取到了租房信息 : " + message);
}
}
/**
* 租房者(具体同事类)
*/
public class Tenant extends Person{
public Tenant(String name, Mediator mediator) {
super(name, mediator);
}
// 房主与中介交互
public void constact(String message){
// 自己和中介交互message信息
mediator.constact(message,this);
}
// 房主获取来自租房者的信息
public void getMessageFromHouseOwner(String message){
System.out.println("租房者" + name + "获取到了房屋信息 : " + message);
}
}
具体中介类(聚合同事类,实现它们之间的交互方法)
/**
*具体抽象类,聚合同事类,并且实现它们之间的交互方法
*/
public class MediatorStructure extends Mediator{
// 定义需要交互的同事类
private HouseOwner houseOwner;
private Tenant tenant;
public void setHouseOwner(HouseOwner houseOwner) {
this.houseOwner = houseOwner;
}
public void setTenant(Tenant tenant) {
this.tenant = tenant;
}
@Override
void constact(String message, Person person) {
if(person == houseOwner){
tenant.getMessageFromHouseOwner(message);
}else if (person == tenant){
houseOwner.getMessageFromTenant(message);
}
}
}
7.4 优缺点
优点:
- 松散耦合: 中介者模式通过把多个同事对象之间的交互封装到中介者对象里面,从而使得同事对象之间松散耦合,基本上可以做到互补依赖。这样一来,同事对象就可以独立地变化和复用,而不再像以前那样“牵一处而动全身”了。
- 集中控制交互: 多个同事类之间交互,都被封装到了具体中介类之中,如果同事类发生了变化,也只需要修改中介类就可以了。
- 一对多关联转变为一对一关联: 没有使用中介者模式的时候,同事对象之间的关系通常是一对多的,引入中介者对象以后,中介者对象和同事对象的关系通常变成双向的一对一,这会让对象的关系更容易理解和实现。
缺点:
- 当同事类太多时,中介者的职责将很大,它会变得复杂而庞大,以至于系统难以维护。
7.5 使用场景
- 系统中对象之间存在复杂的引用关系,系统结构混乱且难以理解。
- 当想创建一个运行于多个类之间的对象,又不想生成新的子类时。
八、迭代器模式
8.1 概述
提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。
8.2 结构
迭代器模式主要包含以下角色:
- 抽象聚合类角色: 定义存储、添加、删除、聚合元素以及创建迭代器对象的接口。
- 具体聚合类角色:实现抽象聚合类,返回一个具体的迭代器对象
- 抽象迭代器: 定义访问和遍历聚合元素的接口,通常包含
hasNext()
、next()
方法。 - 具体迭代器角色: 实现抽象迭代器接口中所定义的方法,完成聚合对象的遍历,记录遍历的当前位置。
8.3 案例
【例】定义一个可以存储学生对象的容器对象,将遍历该容器的功能交由迭代器实现
类图如下:
代码如下:
定义迭代器接口,声明hasNext() 和 next() 方法,定义遍历类
/**
* 抽象迭代器接口
*/
public interface StudentInterator {
boolean hasNext();
Student next();
}
/**
* 遍历对象
*/
public class Student {
private String name;
private String number;
public Student(String name, String number) {
this.name = name;
this.number = number;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", number='" + number + '\'' +
'}';
}
}
具体迭代器接口
/**
* 具体迭代器类
*/
public class StudentInteratorImpl implements StudentInterator{
// 定义需要迭代的集合
private List<Student> list;
private Integer position = 0;
public StudentInteratorImpl(List<Student> list) {
this.list = list;
}
@Override
public boolean hasNext() {
return position < list.size();
}
@Override
public Student next() {
Student student = list.get(position);
position++;
return student;
}
}
抽象聚合对象和具体聚合对象
/**
* 抽象聚合角色
*/
public interface StudentAggregate {
// 添加迭代成员
void addStudent(Student stu);
// 删除迭代成员
void removeStudent(Student stu);
// 获取迭代器对象方法
StudentInterator getInstance();
}
/**
* 具体聚合对象角色
*/
public class StudentAggregateImpl implements StudentAggregate{
// 声明一个聚合类
private List<Student> list = new ArrayList<>();
@Override
public void addStudent(Student stu) {
list.add(stu);
}
@Override
public void removeStudent(Student stu) {
list.remove(stu);
}
@Override
public StudentInterator getInstance() {
return new StudentInteratorImpl(list);
}
}
测试类
// 测试类
public class Client {
public static void main(String[] args) {
StudentAggregateImpl studentAggregate = new StudentAggregateImpl();
studentAggregate.addStudent(new Student("小猪佩奇","001"));
studentAggregate.addStudent(new Student("猪爸爸","002"));
studentAggregate.addStudent(new Student("猪妈妈","003"));
studentAggregate.addStudent(new Student("小黑猪","004"));
StudentInterator interator = studentAggregate.getInstance();
while (interator.hasNext()){
Student student = interator.next();
System.out.println(student);
}
}
}
// 测试结果
Student{name='小猪佩奇', number='001'}
Student{name='猪爸爸', number='002'}
Student{name='猪妈妈', number='003'}
Student{name='小黑猪', number='004'}
8.4 优缺点
优点:
- 它支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式。在迭代器模式中只需要用一个不同的迭代器来替换原有迭代器即可改变遍历算法,我们也可以自己定义迭代器的子类以支持新的遍历方式。
- 迭代器简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据遍历等方法,这样可以简化聚合类的设计。
- 在迭代器模式中,由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改原有代码,满足 “开闭原则” 的要求。
缺点:
- 增加了类的个数,这在一定程度上增加了系统的复杂性。
8.5 使用场景
- 当需要为聚合对象提供多种遍历方式时。
- 当需要为遍历不同的聚合结构提供一个统一的接口时。
- 当访问一个聚合对象的内容而无须暴露其内部细节的表示时。
8.6 JDK 源码分析
迭代器模式在JAVA的很多集合类中被广泛应用,接下来看看JAVA源码中是如何使用迭代器模式的。
List<String> list = new ArrayList<>();
Iterator<String> iterator = list.iterator(); //list.iterator()方法返回的肯定是Iterator接口的子实现类对象
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
采用ArrayList举例说明,单列集合都使用了迭代器
- List: 抽象聚合类
- ArrayList:具体聚合类
- Iterator:抽象迭代器
- list.interator():返回的是实现了
Interator
接口的具体迭代器对象
ArrayList 源码:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
public Iterator<E> iterator() {
// 返回一个迭代器对象
return new Itr();
}
// 这个迭代器对象实现了Interator接口
private class Itr implements Iterator<E> {
int cursor; // 下一个要返回元素的索引
int lastRet = -1; // 上一个返回元素的索引
int expectedModCount = modCount;
Itr() {}
//判断是否还有元素
public boolean hasNext() {
return cursor != size;
}
//获取下一个元素
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
...
}
上面代码可以看到,这里实现方式和我们写的遍历Student的方式不一样,这里将具体迭代器对象(
new Itr()
)直接声明在具体聚合类(ArrayList
)上面,可以通过调用具体聚合类对象的interator
方法直接获得具体迭代器对象。
采用JDK方式实现Student遍历:
当我们在使用JAVA开发的时候,想使用迭代器模式的话,只要让我们自己定义的容器类实现
java.util.Iterable
并实现其中的iterator()方法使其返回一个java.util.Iterator
的实现类就可以了。
代码如下:
具体聚合类
// 实现Iterable接口 实现方法iterator
public class JDKStudentAggregate implements Iterable<Student>{
private List<Student> list = new ArrayList<>();
public void addStudent(Student stu) {
list.add(stu);
}
public void removeStudent(Student stu) {
list.remove(stu);
}
@Override
public Iterator<Student> iterator() {
return new JDKStudentIterator(list);
}
}
具体迭代器类
public class JDKStudentIterator implements Iterator<Student> {
private List<Student> list;
private Integer position = 0;
public JDKStudentIterator(List<Student> list) {
this.list = list;
}
@Override
public boolean hasNext() {
return position < list.size();
}
@Override
public Student next() {
Student student = list.get(position);
position++;
return student;
}
}
测试类
// 测试类
public class Client {
public static void main(String[] args) {
JDKStudentAggregate jdkStudentAggregate = new JDKStudentAggregate();
jdkStudentAggregate.addStudent(new Student("小猪佩奇","001"));
jdkStudentAggregate.addStudent(new Student("猪爸爸","002"));
jdkStudentAggregate.addStudent(new Student("猪妈妈","003"));
jdkStudentAggregate.addStudent(new Student("小黑猪","004"));
Iterator interator = jdkStudentAggregate.iterator();
while (interator.hasNext()){
Student student = (Student) interator.next();
System.out.println(student);
}
}
}
// 测试结果
Student{name='小猪佩奇', number='001'}
Student{name='猪爸爸', number='002'}
Student{name='猪妈妈', number='003'}
Student{name='小黑猪', number='004'}
九、访问者模式
9.1 概述
定义:
封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新操作。
9.2 结构
访问者模式包含一下主要角色:
- 抽象访问者角色: 定义对每个元素访问的行为,它的参数是可以访问的元素,他的方法个数一般来说和元素类个数一致,一个元素就需要一个方法来访问,并且该元素作为参数 ,访问者模式要求元素类个数不能改变。
- 具体访问者角色: 给出对每个元素类访问时产生的具体行为。
- 抽象元素角色:定义了一个接收访问者的方法,意味着每个访问者都可以被访问者访问。
- 具体元素角色: 提供接受访问者方法的具体实现,而这个具体实现通常情况下使用访问者提供的访问元素的方法。
- 对象结构角色: 定义当前所有的对象结构,对象结构是一个抽象表述,具体点可以理解为一个具有容器性质或者复合对象特性的类,它会含有一组元素(
Element
),并且可以迭代这些元素,供访问者访问。
9.3 案例
【例】给宠物喂食
现在养宠物的人特别多,我们就以这个为例,当然宠物还分为狗,猫等,要给宠物喂食的话,主人可以喂,其他人也可以喂食。
- 访问者角色:给宠物喂食的人
- 具体访问者角色:主人、其他人
- 抽象元素角色:动物抽象类
- 具体元素角色:宠物狗、宠物猫
- 结构对象角色:主人家
类图如下:
代码如下:
抽象访问者类和具体访问者类
/**
* 抽象访问者类
*/
public interface Person{
// 操作狗
void operate(Dog dog);
// 操作猫
void operate(Cat cat);
}
/**
* 具体访问类(主人)
*/
public class Owner implements Person{
@Override
public void operate(Dog dog) {
System.out.println("主人给狗狗喂食...");
}
@Override
public void operate(Cat cat) {
System.out.println("主人给狗狗喂食...");
}
}
/**
* 具体访问类(其他人)
*/
public class Other implements Person{
@Override
public void operate(Dog dog) {
System.out.println("其他人给狗狗喂食了...");
}
@Override
public void operate(Cat cat) {
System.out.println("其他人给猫猫喂食了...");
}
}
抽象元素类 和 具体元素类
/**
* 抽象元素类(动物类)
*/
public interface Animal {
// 提供给访问者访问的方法
void execute(Person person);
}
/**
* 具体元素类(狗)
*/
public class Dog implements Animal{
@Override
public void execute(Person person) {
person.operate(this);
System.out.println("小狗狗被喂食了,汪汪汪...");
}
}
/**
* 具体元素类(猫)
*/
public class Cat implements Animal{
@Override
public void execute(Person person) {
person.operate(this);
System.out.println("猫猫被喂食了,喵喵喵...");
}
}
结构对象角色
/**
* 结构对象角色(家)
*/
public class Home {
// 聚合所有元素类
private List<Animal> list = new ArrayList<>();
public void addAnimal(Animal animal){
list.add(animal);
}
// 对所有元素类访问
public void operateAll(Person person){
for (Animal animal : list) {
animal.execute(person);
}
}
}
测试类
public class Client {
public static void main(String[] args) {
Home home = new Home();
home.addAnimal(new Cat());
home.addAnimal(new Dog());
home.operateAll(new Owner());
}
}
// 测试结果
主人给狗狗喂食...
猫猫被喂食了,喵喵喵...
主人给狗狗喂食...
小狗狗被喂食了,汪汪汪...
访问者模式用到了双分派机制
9.4 优缺点
优点:
- 扩展性好: 在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
- 复用性好: 通过访问者来定义整个对象结构通用的功能,从而提高复用程度。
- 分离无关行为: 通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一。
缺点:
- 对象结构变化很困难: 在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
- 违反了依赖倒置原则: 访问者模式依赖了具体类,而没有依赖抽象类。
9.5 使用场景
- 对象结构相对稳定,但其他操作算法经常发生变化的程序。
- 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。
9.6 扩展
访问者模式用到一种双分派技术
分派:
变量被声明时的类型叫做变量的静态类型,有些人又把静态类型叫做明显类型;而变量所引用的对象的真是类型叫变量的实际类型,比如
Map map = new HashMap()
,map变量的静态类型时Map
,实际类型是HashMap
,根据对象类型而对方法进行的选择,就是分派,分派分为两种,静态分派和动态分派。
静态分派(Static Dispatch):
发生在编译时期,分派根据静态类型信息发生,静态分派对于我们并不陌生,方法的重载就是静态分派
动态分派(Dynamic Dispatch):
发生在运行时期,动态分派动态的置换掉某个方法,Java通过方法的重写实现动态分派。
动态分派:
通过方法的重写支持动态分派。
public class Animal {
public void execute() {
System.out.println("Animal");
}
}
public class Dog extends Animal {
@Override
public void execute() {
System.out.println("dog");
}
}
public class Cat extends Animal {
@Override
public void execute() {
System.out.println("cat");
}
}
public class Client {
public static void main(String[] args) {
Animal a = new Dog();
a.execute();
Animal a1 = new Cat();
a1.execute();
}
}
// 测试结果
dog
cat
通过测试结果可以看出,这个是多态,运行时执行时根据实际类型来执行子类的方法
Java编辑器在编译时期并不总是知道代码会被执行,因为编译器仅仅知道静态类型,而不知道对象的真是类型;而方法的调用则时根据对象的真是类型,而不是静态类型。
静态分派:
通过方法的重载实现
public class Animal {
}
public class Dog extends Animal {
}
public class Cat extends Animal {
}
public class Execute {
// 方法重载 根据参数不同区别方法
public void execute(Animal a) {
System.out.println("Animal");
}
public void execute(Dog d) {
System.out.println("dog");
}
public void execute(Cat c) {
System.out.println("cat");
}
}
public class Client {
public static void main(String[] args) {
Animal a = new Animal();
Animal a1 = new Dog();
Animal a2 = new Cat();
// 编译器在编译的时候,只知道静态类型,三个对象的静态类型都是Animal
// 所以将下面三个对象传入的时候,三个对象都被认为是Animal
Execute exe = new Execute();
exe.execute(a);
exe.execute(a1);
exe.execute(a2);
}
}
// 测试结果
Animal
Animal
Animal
从上面结果可以看出,方法的重载是根据参数不同实现的,而编译器在编译的时候,是无法识别它的真实类型,只知道对象的静态类型,而三个对象声明的静态类型都是
Animal
所以作为参数传入的时候,就全都是Animal
出现测试结果内容。
双分派:
所谓双分派技术就是在选择一个方法的时候,不仅仅要根据消息接收者(receiver)的运行时区别,还要根据参数的运行时区别。
public class Animal {
public void accept(Execute exe) {
exe.execute(this);
}
}
public class Dog extends Animal {
public void accept(Execute exe) {
exe.execute(this);
}
}
public class Cat extends Animal {
public void accept(Execute exe) {
exe.execute(this);
}
}
public class Execute {
public void execute(Animal a) {
System.out.println("animal");
}
public void execute(Dog d) {
System.out.println("dog");
}
public void execute(Cat c) {
System.out.println("cat");
}
}
public class Client {
public static void main(String[] args) {
Animal a = new Animal();
Animal d = new Dog();
Animal c = new Cat();
Execute exe = new Execute();
// 根据对象调用方法,调用到的是真实类型的方法
// 再将重载方法传入,调用重载的方法,传入当前类作为参数
// 这也就是访问者类的实现方法
a.accept(exe);
d.accept(exe);
c.accept(exe);
}
}
// 测试结果
animal
dog
cat
在上面代码中,客户端将Execute对象做为参数传递给Animal类型的变量调用的方法,这里完成第一次分派,这里是方法重写,所以是动态分派,也就是执行实际类型中的方法,同时也
将自己this作为参数传递进去,这里就完成了第二次分派
,这里的Execute类中有多个重载的方法,而传递进行的是this,就是具体的实际类型的对象。
双分派实现动态绑定的本质,就是在重载方法委派的前面加上了继承体系中覆盖的环节,由于覆盖是动态的,所以重载就是动态的了。
十、备忘录模式
10.1 概述
备忘录模式提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂时存储起来的备忘录将状态复原,很多软件都提供了撤销(Undo)操作,如 Word、记事本、Photoshop、IDEA等软件在编辑时按 Ctrl+Z 组合键时能撤销当前操作,使文档恢复到之前的状态;还有在 浏览器 中的后退键、数据库事务管理中的回滚操作、玩游戏时的中间结果存档功能、数据库与操作系统的备份操作、棋类游戏中的悔棋功能等都属于这类。
定义:
又叫快照模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。
10.2 结构
- 发起人角色: 记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录的功能,实现其他业务功能,他可以访问备忘录中所有信息。
- 备忘录角色: 负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
- 管理者角色: 对备忘录进行管理,提供保存和获取备忘录的功能,但其不能对备忘录的内容进行访问和修改。
根据除发起者以外的角色能不能访问备忘录里面的状态,可以将备忘录分为两个等效接口:
- 窄接口:管理者对象(和其他发起人人对象之外的任何对象)看到的是备忘录的窄接口,这个窄接口只允许他吧备忘录对象传给其他对象。
- 宽接口:与管理者看到的窄接口相反,发起人对象可以看到一个宽接口,这个宽接口允许它读取所有数据,一遍根据这些数据恢复这个发起人对象的内部状态。
10.3 案例
游戏中的某个场景,一游戏角色有生命力、攻击力、防御力等数据,在打Boss前和后一定会不一样的,我们允许玩家如果感觉与Boss决斗的效果不理想可以让游戏恢复到决斗之前的状态。
两种方式实现:
- “白箱”备忘录模式
- “黑箱”备忘录模式
"白箱"备忘录模式:
备忘录角色对任何对象都提供一个接口,即宽接口,备忘录角色的内部所存储的状态就对所有对象公开,简单来说就是除发起者以外的对象,都可以访问备忘录中的信息。
类图如下:
代码如下:
发起者类
/**
* 游戏角色(发起者角色)
*/
public class GameRole {
// 定义生命值、攻击力、防御力
private int vit;
private int atk;
private int def;
// 提供initState方法
public void initState(){
this.atk = 100;
this.vit = 100;
this.def = 100;
}
// 创建备忘录
public RoleMemento backup(){
return new RoleMemento(vit,atk,def);
}
// 恢复备忘录
public void recover(RoleMemento roleMemento){
this.atk = roleMemento.getAtk();
this.vit = roleMemento.getVit();
this.def = roleMemento.getDef();
}
// 打boss
public void fight(){
this.atk = 50;
this.vit = 70;
this.def = 90;
}
// 显示当前状态
public void display(){
System.out.println("生命值:" + this.vit);
System.out.println("攻击力:" + this.atk);
System.out.println("防御力:" + this.def);
}
public int getVit() {
return vit;
}
public void setVit(int vit) {
this.vit = vit;
}
public int getAtk() {
return atk;
}
public void setAtk(int atk) {
this.atk = atk;
}
public int getDef() {
return def;
}
public void setDef(int def) {
this.def = def;
}
}
备忘录类
public class RoleMemento {
// 和角色状态一样的信息
private int vit;
private int atk;
private int def;
public RoleMemento(int vit, int atk, int def) {
this.vit = vit;
this.atk = atk;
this.def = def;
}
public int getVit() {
return vit;
}
public int getAtk() {
return atk;
}
public int getDef() {
return def;
}
}
备忘录管理类
/**
* 备忘录管理者类
*/
public class RoleMementoManager {
// 聚合备忘录
private RoleMemento roleMemento;
// 提供获取和设置操作
public RoleMemento getRoleMemento() {
return roleMemento;
}
public void setRoleMemento(RoleMemento roleMemento) {
this.roleMemento = roleMemento;
}
}
测试类
// 测试类
public class Client {
public static void main(String[] args) {
GameRole gameRole = new GameRole();
// 初始化状态
gameRole.initState();
// 显示当前状态
System.out.println("=========打boss前状态==========");
gameRole.display();
// 进行备份
RoleMementoManager roleMementoManager = new RoleMementoManager();
roleMementoManager.setRoleMemento(gameRole.backup());
System.out.println("=========打boss后状态==========");
// 打 boss 状态改变
gameRole.fight();
gameRole.display();
System.out.println("=========恢复状态==========");
gameRole.recover(roleMementoManager.getRoleMemento());
gameRole.display();
}
}
// 测试结果
=========打boss前状态==========
生命值:100
攻击力:100
防御力:100
=========打boss后状态==========
生命值:70
攻击力:50
防御力:90
=========恢复状态==========
生命值:100
攻击力:100
防御力:100
“黑箱”备忘录:
备忘录角色对发起人对象提供一个宽接口,而为其他对象提供一个窄接口。在Java语言中,实现双重接口的办法就是将备忘录类设计成发起人类的内部成员类。这样设计,就只有发起者类才能访问备忘录类
将
RoleMemento
设为GameRole
的内部类,从而将RoleMemento
对象封装在GameRole
里面;在外面提供一个标识接口Memento
给RoleMementoManager
及其他对象使用。这样GameRole
类看到的是RoleMemento
所有的接口,而RoleMementoManager
及其他对象看到的仅仅是标识接口Memento
所暴露出来的接口,从而维护了封装型。
类图如下:
代码如下:
声明一个窄接口,对外提供
/**
* 窄接口 只作为一个标识
*/
public interface Memento {
}
发起者类(玩家) 定义内部类继承窄接口
/**
* 游戏角色(发起者角色)
*/
public class GameRole {
// 定义生命值、攻击力、防御力
private int vit;
private int atk;
private int def;
// 提供initState方法
public void initState(){
this.atk = 100;
this.vit = 100;
this.def = 100;
}
// 创建备忘录
public RoleMemento backup(){
return new RoleMemento(vit,atk,def);
}
// 声明备忘录内部类 实现窄接口
public class RoleMemento implements Memento{
private int vit;
private int atk;
private int def;
public RoleMemento(int vit, int atk, int def) {
this.vit = vit;
this.atk = atk;
this.def = def;
}
public int getVit() {
return vit;
}
public int getAtk() {
return atk;
}
public int getDef() {
return def;
}
}
// 恢复备忘录
public void recover(RoleMemento roleMemento){
this.atk = roleMemento.getAtk();
this.vit = roleMemento.getVit();
this.def = roleMemento.getDef();
}
// 打boss
public void fight(){
this.atk = 50;
this.vit = 70;
this.def = 90;
}
// 显示当前状态
public void display(){
System.out.println("生命值:" + this.vit);
System.out.println("攻击力:" + this.atk);
System.out.println("防御力:" + this.def);
}
public int getVit() {
return vit;
}
public void setVit(int vit) {
this.vit = vit;
}
public int getAtk() {
return atk;
}
public void setAtk(int atk) {
this.atk = atk;
}
public int getDef() {
return def;
}
public void setDef(int def) {
this.def = def;
}
}
备忘录管理类 采用接口实现获取和设置备忘录
/**
* 备忘录管理者类
*/
public class RoleMementoManager {
// 聚合备忘录
private Memento memento;
// 提供获取和设置操作
public Memento getRoleMemento() {
return memento;
}
public void setRoleMemento(Memento memento) {
this.memento = memento;
}
}
测试类
// 测试类
public class Client {
public static void main(String[] args) {
GameRole gameRole = new GameRole();
// 初始化状态
gameRole.initState();
// 显示当前状态
System.out.println("=========打boss前状态==========");
gameRole.display();
// 进行备份
RoleMementoManager roleMementoManager = new RoleMementoManager();
roleMementoManager.setRoleMemento(gameRole.backup());
System.out.println("=========打boss后状态==========");
// 打 boss 状态改变
gameRole.fight();
gameRole.display();
System.out.println("=========恢复状态==========");
gameRole.recover(roleMementoManager.getRoleMemento());
gameRole.display();
}
}
// 测试结果
=========打boss前状态==========
生命值:100
攻击力:100
防御力:100
=========打boss后状态==========
生命值:70
攻击力:50
防御力:90
=========恢复状态==========
生命值:100
攻击力:100
防御力:100
10.4 优缺点
优点:
- 提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。
- 实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
- 简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。
缺点:
- 资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。
10.5 使用场景
-
需要保存与恢复数据的场景,如玩游戏时的中间结果的存档功能。
-
需要提供一个可回滚操作的场景,如 Word、记事本、Photoshop,idea等软件在编辑时按 Ctrl+Z 组合键,还有数据库中事务操作。
十一、解释器模式
11.1 概述
设计一个软件用来进行加减计算。我们第一想法就是使用工具类,提供对应的加法和减法的工具方法。
//用于两个整数相加
public static int add(int a,int b){
return a + b;
}
//用于两个整数相加
public static int add(int a,int b,int c){
return a + b + c;
}
//用于n个整数相加
public static int add(Integer ... arr) {
int sum = 0;
for (Integer i : arr) {
sum += i;
}
return sum;
}
上面的形式比较单一、有限,如果形式变化非常多,这就不符合要求,因为加法和减法运算,两个运算符与数值可以有无限种组合方式。比如 1+2+3+4+5、1+2+3-4等等。
显然,现在需要一种翻译识别机器,能够解析由数字以及 + - 符号构成的合法的运算序列。如果把运算符和数字都看作节点的话,能够逐个节点的进行读取解析运算,这就是解释器模式的思维。
定义:
给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。
文法(语法)规则:
文法是用于描述语言的语法结构的形式规则。
expression ::= value | plus | minus
plus ::= expression ‘+’ expression
minus ::= expression ‘-’ expression
value ::= integer
注意: 这里的符号
“::=”
表示“定义为”的意思,竖线 | 表示或,左右的其中一个,引号内为字符本身,引号外为语法。
抽象语法树:
在计算机科学中,抽象语法树(AbstractSyntaxTree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
11.2 结构
解释器模式包含一下角色:
- 抽象表达式角色: 定义解释器的接口,约定解释器的解释操作,主要包含解释方法
interpret()
; - 终结符表达式角色: 是抽象表达式的子类,用来实现文法中与终结符相关的操作,文法中的每一个终结符都有一个具体终结表达式与之相对应。
- 非终结符表达式角色: 也是抽象表达式的子类,用来实现文法中与非终结符表达式相关的操作,文法中的每条规则都对应一个非终结符表达式。
- 环境角色: 通常包含各个解释器需要的数据或是公共的功能,一般用来传递被所有解释器共享的数据,后面解释器可以从这里获取一些值。
- 客户端: 主要任务就是需要分析句子或表达式装换称使用解释器对象描述的语法树,然后调用解释器的解释方法,当然也可以通过环境角色间接访问解释器的解释方法。
11.3 案例
【例】设计实现加减法的软件
代码如下:
抽象表达式类 本例中没有使用到
/**
* 抽象解释器类对象
*/
public abstract class AbstractExpression {
abstract int interpret(Context context);
}
终结符表达式类
/**
* 终结符表达式
*/
public class Value extends AbstractExpression{
private Integer value;
public Value(Integer value) {
this.value = value;
}
// 计算
@Override
int interpret(Context context) {
return value;
}
@Override
public String toString() {
return new Integer(value).toString();
}
}
非终结者表达式类
/**
* 非终结者表达式类(参数)
*/
public class Variable extends AbstractExpression{
private String name;
public Variable(String name) {
this.name = name;
}
@Override
int interpret(Context context) {
return context.getValue(this);
}
@Override
public String toString() {
return name;
}
}
/**
* 非终结符表达式(加法)
* 继承并聚合抽象表达式
*/
public class Plus extends AbstractExpression{
// + 左边的表达式
private AbstractExpression left;
// + 右边的表达式
private AbstractExpression right;
public Plus(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}
@Override
int interpret(Context context) {
return left.interpret(context) + right.interpret(context);
}
@Override
public String toString() {
return "(" + left.toString() + " + " + right.toString() + ")";
}
}
/**
* 非终结符表达式(加法)
* 继承并聚合抽象表达式
*/
public class Minus extends AbstractExpression{
// - 左边的表达式
private AbstractExpression left;
// - 右边的表达式
private AbstractExpression right;
public Minus(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}
@Override
int interpret(Context context) {
return left.interpret(context) - right.interpret(context);
}
@Override
public String toString() {
return "(" + left.toString() + " - " + right.toString() + ")";
}
}
环境类
/**
* 环境类
*/
public class Context {
// 存储表达式参数
private Map<Variable,Integer> valMap = new HashMap();
// 添加参数
public void assign(Variable var,Integer value){
valMap.put(var,value);
}
// 获取参数
public int getValue(Variable var){
return valMap.get(var);
}
}
测试类
public class Client {
public static void main(String[] args) {
// 定义环境类
Context context = new Context();
// 定义参数列表
Variable a = new Variable("a");
Variable b = new Variable("b");
Variable c = new Variable("c");
Variable d = new Variable("d");
// 往环境类里面添加参数
context.assign(a,1);
context.assign(b,3);
context.assign(c,4);
context.assign(d,2);
// 生成表达式树 a - (b + (c - d)) = -4
AbstractExpression expression = new Minus(a,new Plus(b,new Minus(c,d)));
System.out.println(expression + " = " + expression.interpret(context));
}
}
// 测试结果
(a - (b + (c - d))) = -4
11.4 优缺点
优点:
- 易于改变和扩展文法: 由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言。
- 实现文法较为容易: 在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂。
- 增加新的解释表达式较为方便: 如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无须修改,符合 “开闭原则”。
缺点:
- 对于复杂文法难以维护: 在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护。
- 执行效率较低: 由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调试过程也比较麻烦。
11.5 使用场景
-
当语言的文法较为简单,且执行效率不是关键问题时。
-
当问题重复出现,且可以用一种简单的语言来进行表达时。
-
当一个语言需要解释执行,并且语言中的句子可以表示为一个抽象语法树的时候。
更多推荐
设计模式-行为型模式篇
发布评论