本文章笔记整理来自黑马视频https://www.bilibili.com/video/BV1Np4y1z7BU,相关资料可在评论区获取。
详解23种设计模式(基于Java)—— 设计模式相关内容介绍(一 / 五)
详解23种设计模式(基于Java)—— 创建者模式(二 / 五)
详解23种设计模式(基于Java)—— 结构型模式(三 / 五)
详解23种设计模式(基于Java)—— 综合练习之自定义Spring IoC(五 / 五)
(1)行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。
(2)行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。
(3)行为型模式分为以下11种:模板方法模式、策略模式、命令模式、职责链模式、状态模式、观察者模式、中介者模式、迭代器模式、访问者模式、备忘录模式。其中除了模板方法模式和解释器模式是类行为型模式,其他的全部属于对象行为型模式。
(1)在面向对象程序设计过程中,程序员常常会遇到这种情况:设计一个系统时知道了算法所需的关键步骤,而且确定了这些步骤的执行顺序,但某些步骤的具体实现还未知,或者说某些步骤的实现与具体的环境相关。
(2)例如,去银行办理业务一般要经过以下4个流程:取号、排队、办理具体业务、对银行工作人员进行评分等,其中取号、排队和对银行工作人员进行评分的业务对每个客户是一样的,可以在父类中实现,但是办理具体业务却因人而异,它可能是存款、取款或者转账等,可以延迟到子类中实现。
(3)模板方法模式:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
(4)使用场景
① 算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
② 需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。
模板方法(Template Method)模式包含以下主要角色:
(1)抽象类(Abstract Class): 负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。
① 模板方法: 定义了算法的骨架,按某种顺序调用其包含的基本方法。
② 基本方法: 是实现算法各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三种:
1)抽象方法(Abstract Method) :一个抽象方法由抽象类声明、由其具体子类实现。
2)具体方法(Concrete Method) :一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。
3)钩子方法(Hook Method) :在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。一般钩子方法是用于判断的逻辑方法,这类方法名一般为isXxx,返回值类型为boolean类型。
(2)具体子类(Concrete Class): 实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。
【例】炒菜
炒菜的步骤是固定的,分为倒油、热油、倒蔬菜、倒调料品、翻炒等步骤。现通过模板方法模式来用代码模拟。类图如下:
具体实现代码如下:
AbstractClass.java(抽象类)
package com.itheima.patterns.behaviorpattern.template;
//抽象类,定义模板方法和基本方法
public abstract class AbstractClass {
//模板方法定义
public final void cookProcess() {
pourOil();
heatOil();
pourVegetable();
pourSauce();
fry();
}
public void pourOil() {
System.out.println("倒油");
}
//第二步:热油是一样的,所以直接实现
public void heatOil() {
System.out.println("热油");
}
//第三步:倒蔬菜是不一样的(一个下包菜,一个是下菜心)
public abstract void pourVegetable();
//第四步:倒调味料是不一样
public abstract void pourSauce();
//第五步:翻炒是一样的,所以直接实现
public void fry(){
System.out.println("炒啊炒啊炒到熟啊");
}
}
ConcreteClass_BaoCai.java
package com.itheima.patterns.behaviorpattern.template;
//炒包菜类
public class ConcreteClass_BaoCai extends AbstractClass {
public void pourVegetable() {
System.out.println("下锅的蔬菜是包菜");
}
public void pourSauce() {
System.out.println("下锅的酱料是辣椒");
}
}
ConcreteClass_CaiXin.java
package com.itheima.patterns.behaviorpattern.template;
//炒菜心类
public class ConcreteClass_CaiXin extends AbstractClass {
public void pourVegetable() {
System.out.println("下锅的蔬菜是菜心");
}
public void pourSauce() {
System.out.println("下锅的酱料是蒜蓉");
}
}
Client.java
package com.itheima.patterns.behaviorpattern.template;
public class Client {
public static void main(String[] args) {
//炒包菜
//创建对象
ConcreteClass_BaoCai baoCai = new ConcreteClass_BaoCai();
//调用炒菜的功能
baoCai.cookProcess();
System.out.println("=====================");
ConcreteClass_CaiXin caiXin = new ConcreteClass_CaiXin();
caiXin.cookProcess();
}
}
注意:为防止恶意操作,一般模板方法都加上 final 关键词。
(1)优点
① 提高代码复用性:将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中。
② 实现了反向控制:通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制 ,并符合“开闭原则”。
(2)缺点
① 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象。
② 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。
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;
}
//调用了无参的read方法,该方法是每次读取一个字节数据
int c = 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;
}
}
(1)从上面代码可以看到,无参的 read() 方法是抽象方法,要求子类必须实现。而 read(byte b[])方法调用了 read(byte b[], int off, int len) 方法,所以在此处重点看的方法是带三个参数的方法。在该方法中第18行、27行,可以看到调用了无参的抽象的 read() 方法。
(2)总结如下: 在InputStream父类中已经定义好了读取一个字节数组数据的方法是每次读取一个字节,并将其存储到数组的第一个索引位置,读取len个字节数据。具体如何读取一个字节数据呢?由子类实现。
(1)先看下面的图片,我们去旅游选择出行模式有很多种,可以骑自行车、可以坐汽车、可以坐火车、可以坐飞机。
(2)策略模式:该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
策略模式的主要角色如下:
(1)抽象策略(Strategy): 这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
(2)具体策略(Concrete Strategy): 实现了抽象策略定义的接口,提供具体的算法实现或行为。
(3)环境(Context)类: 持有一个策略类的引用,最终给客户端调用。
【例】促销活动
一家百货公司在定年度的促销活动。针对不同的节日(春节、中秋节、圣诞节)推出不同的促销活动,由促销员将促销活动展示给客户。类图如下:
具体实现代码如下:
Strategy.java(抽象策略),定义百货公司所有促销活动的共同接口
package com.itheima.patterns.behaviorpattern.strategy;
public interface Strategy {
void show();
}
每个节日具体的促销活动:
StrategyA.java(具体策略)
package com.itheima.patterns.behaviorpattern.strategy;
//具体策略类
public class StrategyA implements Strategy{
@Override
public void show() {
System.out.println("买一送一");
}
}
StrategyB.java(具体策略)
package com.itheima.patterns.behaviorpattern.strategy;
public class StrategyB implements Strategy{
@Override
public void show() {
System.out.println("满200元减50元");
}
}
StrategyC.java(具体策略)
package com.itheima.patterns.behaviorpattern.strategy;
public class StrategyC implements Strategy{
@Override
public void show() {
System.out.println("满1000元加一元换购任意200元以下商品");
}
}
SalesMan.java(环境类),定义环境角色,用于连接上下文,即把促销活动推销给客户,这里可以理解为销售员
package com.itheima.patterns.behaviorpattern.strategy;
public class SalesMan {
//聚合策略类对象
private Strategy strategy;
public SalesMan(Strategy strategy){
this.strategy = strategy;
}
//向客户展示促销活动
public void salesManShow(){
strategy.show();
}
public void setStrategy(Strategy strategy) {
this.strategy = strategy;
}
}
Client.java
package com.itheima.patterns.behaviorpattern.strategy;
public class Client {
public static void main(String[] args) {
//春节来了,使用春节促销活动
SalesMan salesMan = new SalesMan(new StrategyA());
//展示促销活动
salesMan.salesManShow();
System.out.println("==============");
//中秋节到了,使用中秋节的促销活动
salesMan.setStrategy(new StrategyB());
//展示促销活动
salesMan.salesManShow();
System.out.println("==============");
//圣诞节到了,使用圣诞节的促销活动
salesMan.setStrategy(new StrategyC());
//展示促销活动
salesMan.salesManShow();
}
}
(1)优点
① 策略类之间可以自由切换,由于策略类都实现同一个接口,所以使它们之间可以自由切换。
② 易于扩展,增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合“开闭原则“
③ 避免使用多重条件选择语句(if else),充分体现面向对象设计思想。
(2)缺点
① 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
② 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。
(1)一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中。
(2)一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句。
(3)系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时。
(4)系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏与算法相关的数据结构。
(5)多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。
(1)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);
}
}
}
(2)Arrays就是一个环境角色类,这个sort方法可以传一个新策略让Arrays根据这个策略来进行排序。就比如下面的测试类。
public class demo {
public static void main(String[] args) {
Integer[] data = {12, 2, 3, 2, 4, 5, 1};
// 实现降序排序
Arrays.sort(data, new Comparator<Integer>() {
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
System.out.println(Arrays.toString(data)); //[12, 5, 4, 3, 2, 2, 1]
}
}
(3)这里我们在调用Arrays的sort方法时,第二个参数传递的是Comparator接口的子实现类对象。所以Comparator充当的是抽象策略角色,而具体的子实现类充当的是具体策略角色。环境角色类(Arrays)应该持有抽象策略的引用来调用。那么,Arrays类的sort方法到底有没有使用Comparator子实现类中的 compare() 方法吗?让我们继续查看TimSort类的 sort() 方法,代码如下:
class TimSort<T> {
static <T> void sort(T[] a, int lo, int hi, Comparator<? super T> c,T[] work, int workBase, int workLen) {
assert c != null && a != null && lo >= 0 && lo <= hi && hi <=
a.length;
int nRemaining = hi - lo;
if (nRemaining < 2)
return; // Arrays of size 0 and 1 are always sorted
// If array is small, do a "mini-TimSort" with no merges
if (nRemaining < MIN_MERGE) {
int initRunLen = countRunAndMakeAscending(a, lo, hi, c);
binarySort(a, lo, hi, lo + initRunLen, c);
return;
}
...
}
private static <T> int countRunAndMakeAscending(T[] a, int lo, int hi,Comparator<? super T> c) {
assert lo < hi;
int runHi = lo + 1;
if (runHi == hi)
return 1;
// Find end of run, and reverse range if descending
if (c.compare(a[runHi++], a[lo]) < 0) { // Descending
while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0)
runHi++;
reverseRange(a, lo, runHi);
} else { // Ascending
while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0)
runHi++;
}
return runHi - lo;
}
}
上面的代码中最终会跑到 countRunAndMakeAscending() 这个方法中。我们可以看见,只用了compare方法,所以在调用Arrays.sort方法只传具体compare重写方法的类对象就行,这也是Comparator接口中必须要子类实现的一个方法。
(1)日常生活中,我们出去吃饭都会遇到下面的场景:
(2)命令模式:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行存储、传递、调用、增加与管理。
命令模式包含以下主要角色:
(1)抽象命令类(Command)角色: 定义命令的接口,声明执行的方法。
(2)具体命令(Concrete Command)角色: 具体的命令,实现命令接口;通常会持有接收者,并调用接收者的功能来完成命令要执行的操作。
(3)实现者/接收者(Receiver)角色: 接收者,真正执行命令的对象。任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。
(4)调用者/请求者(Invoker)角色: 要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。这个是客户端真正触发命令并要求命令执行相应操作的地方,也就是说相当于使用命令对象的入口。
将上面的案例用代码实现,那就需要分析命令模式的角色在该案例中由谁来充当。
① 服务员: 就是调用者角色,由她来发起命令。
② 资深大厨: 就是接收者角色,真正命令执行的对象。
③ 订单: 命令中包含订单。
类图如下:
具体实现代码如下:
Command.java
package com.itheima.patterns.behaviorpattern.command;
//抽象命令接口
public interface Command {
void execute();
}
Order.java
package com.itheima.patterns.behaviorpattern.command;
import java.util.HashMap;
import java.util.Map;
//订单类
public class Order {
//餐桌号码
private int diningTable;
//所下的餐品及份数
private Map<String,Integer> foodDir = new HashMap<>();
public int getDiningTable() {
return diningTable;
}
public void setDiningTable(int diningTable) {
this.diningTable = diningTable;
}
public Map<String, Integer> getFoodDir() {
return foodDir;
}
public void setFood(String name,int num) {
foodDir.put(name,num);
}
}
SeniorChef.java
package com.itheima.patterns.behaviorpattern.command;
//厨师类
public class SeniorChef {
public void makeFood(String name,int num){
System.out.println(num + "份" + name);
}
}
OrderCommand.java
package com.itheima.patterns.behaviorpattern.command;
import java.util.Map;
import java.util.Set;
//具体的命令类
public class OrderCommand implements Command{
//持有接收者对象
private SeniorChef receiver;
private Order order;
public OrderCommand(SeniorChef receiver, Order order) {
this.receiver = receiver;
this.order = order;
}
@Override
public void execute() {
System.out.println(order.getDiningTable() + "桌的订单:");
Map<String, Integer> foodDir = order.getFoodDir();
//遍历map集合
Set<String> keys = foodDir.keySet();
for (String foodName : keys) {
receiver.makeFood(foodName,foodDir.get(foodName));
}
System.out.println(order.getDiningTable() + "桌的饭准备完毕!");
}
}
Waitor.java
package com.itheima.patterns.behaviorpattern.command;
import java.util.ArrayList;
import java.util.List;
//服务员类(属于请求者角色)
public class Waitor {
//持有多个命令对象
private List<Command> commands = new ArrayList<>();
public void setCommand(Command cmd){
//将cmd对象存储到List集合中
commands.add(cmd);
}
//发起命令功能,喊订单来了
public void orderUp(){
System.out.println("服务员:大厨,新订单来了。。。。");
//遍历list集合
for (Command command : commands) {
if(command != null) {
command.execute();
}
}
}
}
Client.java
package com.itheima.patterns.behaviorpattern.command;
public class Client {
public static void main(String[] args) {
//创建第一个订单对象
Order order1 = new Order();
order1.setDiningTable(1);
order1.setFood("西红柿鸡蛋面",1);
order1.setFood("小杯可乐",2);
//创建第二个订单对象
Order order2 = new Order();
order2.setDiningTable(2);
order2.setFood("尖椒肉丝盖饭",1);
order2.setFood("小杯雪碧",1);
//创建厨师对象
SeniorChef receiver = new SeniorChef();
//创建命令对象
OrderCommand cmd1 = new OrderCommand(receiver,order1);
OrderCommand cmd2 = new OrderCommand(receiver,order2);
//创建调用者(服务员对象)
Waitor invoke = new Waitor();
invoke.setCommand(cmd1);
invoke.setCommand(cmd2);
//让服务员发起命令
invoke.orderUp();
}
}
(1)优点
① 降低系统的耦合度。命令模式能将调用操作的对象与实现该操作的对象解耦。
② 增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,它满足“开闭原则”,对扩展比较灵活。
③ 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
④ 方便实现 Undo 和 Redo 操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。
(2)缺点
① 使用命令模式可能会导致某些系统有过多的具体命令类。
② 系统结构更加复杂
(1)系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
(2)系统需要在不同的时间指定请求、将请求排队和执行请求。
(3)系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
Runable是一个典型命令模式,Runnable担当命令的角色,Thread充当的是调用者,start方法就是其执行方法。
//命令接口(抽象命令角色)
public interface Runnable {
public abstract void run();
}
//调用者
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 Demo {
public static void main(String[] args) {
Receiver receiver = new Receiver();
TurnOffThread turnOffThread = new TurnOffThread(receiver);
Thread thread = new Thread(turnOffThread);
thread.start();
}
}
(1)在现实生活中,常常会出现这样的事例:一个请求有多个对象可以处理,但每个对象的处理条件或权限不同。例如,公司员工请假,可批假的领导有部门负责人、副总经理、总经理等,但每个领导能批准的天数不同,员工必须根据自己要请假的天数去找不同的领导签名,也就是说员工必须记住每个领导的姓名、电话和地址等信息,这增加了难度。这样的例子还有很多,如找领导出差报销、生活中的“击鼓传花”游戏等。
(2)职责链模式:又名职责链模式,为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
职责链模式主要包含以下角色:
(1)抽象处理者(Handler)角色: 定义一个处理请求的接口,包含抽象处理方法和一个后继连接。
(2)具体处理者(Concrete Handler)角色: 实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。
(3)客户类(Client)角色: 创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。
现需要开发一个请假流程控制系统。请假一天以下的假只需要小组长同意即可;请假1天到3天的假还需要部门经理同意;请求3天到7天还需要总经理同意才行。
具体实现代码如下:
Handler.java(抽象处理者)
package com.itheima.patterns.behaviorpattern.responsibilitychain;
//抽象处理者
public abstract class Handler {
protected final static int NUM_ONE = 1;
protected final static int NUM_THREE = 3;
protected final static int NUM_SEVEN = 7;
//该领导处理的请假天数区间
private int numStart;
private int numEnd;
//声明后续者(声明上级领导)
private Handler nextHandler;
public Handler(int numStart) {
this.numStart = numStart;
}
public Handler(int numStart, int numEnd) {
this.numStart = numStart;
this.numEnd = numEnd;
}
//设置上级领导对象
public void setNextHandler(Handler nextHandler) {
this.nextHandler = nextHandler;
}
//各级领导处理请假天数的方法
protected abstract void handleLeave(LeaveRequest leave);
//提交请假条
public final void submit(LeaveRequest leave){
if(this.nextHandler != null && leave.getNum() > this.numEnd) {
//提交给上级领导进行审批
this.nextHandler.submit(leave);
}else{
//该领导进行审批
this.handleLeave(leave);
System.out.println("请假流程结束!");
}
}
}
LeaveRequest.java
package com.itheima.patterns.behaviorpattern.responsibilitychain;
//请假条类
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 int getNum() {
return num;
}
public String getContent() {
return content;
}
}
GroupLeader.java(具体处理者)
package com.itheima.patterns.behaviorpattern.responsibilitychain;
//小组长类
public class GroupLeader extends Handler{
public GroupLeader() {
super(0,Handler.NUM_ONE);
}
protected void handleLeave(LeaveRequest leave) {
System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "。");
System.out.println("小组长审批:同意");
}
}
Manager.java(具体处理者)
package com.itheima.patterns.behaviorpattern.responsibilitychain;
//小组长类
public class Manager extends Handler{
public Manager() {
super(Handler.NUM_ONE,Handler.NUM_THREE);
}
protected void handleLeave(LeaveRequest leave) {
System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "。");
System.out.println("部门经理审批:同意");
}
}
GeneralManager.java(具体处理者)
package com.itheima.patterns.behaviorpattern.responsibilitychain;
//小组长类
public class GeneralManager extends Handler{
public GeneralManager() {
super(Handler.NUM_THREE,Handler.NUM_SEVEN);
}
protected void handleLeave(LeaveRequest leave) {
System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "。");
System.out.println("总经理审批:同意");
}
}
Client.java(客户类)
package com.itheima.patterns.behaviorpattern.responsibilitychain;
public class Client {
public static void main(String[] args) {
//创建一个请假条对象
LeaveRequest leave = new LeaveRequest("小明", 2, "身体不适");
//创建各级领导对象
GroupLeader groupLeader = new GroupLeader();
Manager manager = new Manager();
GeneralManager generalManager = new GeneralManager();
//设置处理者链
groupLeader.setNextHandler(manager);
manager.setNextHandler(generalManager);
//小明提交请假申请
groupLeader.submit(leave);
}
}
(1)优点
① 降低了对象之间的耦合度,该模式降低了请求发送者和接收者的耦合度。
② 增强了系统的可扩展性,可以根据需要增加新的请求处理类,满足开闭原则。
③ 增强了给对象指派职责的灵活性,当工作流程发生变化,可以动态地改变链内的成员或者修改它们的次序,也可动态地新增或者删除责任。
④ 责任链简化了对象之间的连接,一个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。
⑤ 责任分担,每个类只需要处理自己该处理的工作,不能处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。
(2)缺点
① 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。
② 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
③ 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。
在Java Web应用开发中,FilterChain是职责链(过滤器)模式的典型应用,以下是Filter的模拟实现分析:
(1)模拟web请求Request以及web响应Response
public interface Request{
}
public interface Response{
}
(2)模拟web过滤器Filter
public interface Filter {
public void doFilter(Request req,Response res,FilterChain c);
}
(3)模拟实现具体过滤器
public class FirstFilter implements Filter {
public void doFilter(Request req, Response res, FilterChain chain) {
System.out.println("过滤器1 前置处理");
// 先执行所有request再倒序执行所有response
chain.doFilter(req, res);
System.out.println("过滤器1 后置处理");
}
}
public class SecondFilter implements Filter {
public void doFilter(Request req, Response res, FilterChain chain) {
System.out.println("过滤器2 前置处理");
// 先执行所有request再倒序执行所有response
chain.doFilter(req, res);
System.out.println("过滤器2 后置处理");
}
}
(4)模拟实现过滤器链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);
}
}
(5)测试类
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);
}
}
(1)【例】通过按钮来控制一个电梯的状态,电梯有开门状态,关门状态,停止状态,运行状态。每一种状态改变,都有可能要根据其他状态来更新处理。例如,如果电梯门现在处于运行时状态,就不能进行开门操作,而如果电梯门是停止状态,就可以执行开门操作。其类图如下:
具体实现代码如下:
ILift.java
package com.itheima.patterns.behaviorpattern.state.before;
public interface ILift {
//定义四个电梯状态的常量
int OPENING_STATE = 1;
int CLOSING_STATE = 2;
int RUNNING_STATE = 3;
int STOPPING_STATE = 4;
//设置电梯状态的功能
void setState(int state);
//电梯操作功能
void open();
void close();
void run();
void stop();
}
Lift.java
package com.itheima.patterns.behaviorpattern.state.before;
//电梯类(ILift的子实现类)
public class Lift implements ILift {
//声明一个记录当前电梯的状态
private int state;
public void setState(int state) {
this.state = state;
}
public void open() {
switch (state) { //当前电梯状态
case OPENING_STATE :
//什么事都不做
break;
case CLOSING_STATE :
System.out.println("电梯打开了...");
//设置当前电梯状态为开启状态
setState(OPENING_STATE);
break;
case STOPPING_STATE :
System.out.println("电梯打开了...");
//设置当前电梯状态为开启状态
setState(OPENING_STATE);
break;
case RUNNING_STATE :
//什么事都不做
break;
}
}
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;
}
}
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;
}
}
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;
}
}
}
Client.java
package com.itheima.patterns.behaviorpattern.state.before;
public class Client {
public static void main(String[] args) {
//创建电梯对象
Lift lift = new Lift();
//设置当前电梯的状态
lift.setState(ILift.OPENING_STATE);
//打开
lift.open();
lift.close();
lift.run();
lift.stop();
}
}
问题分析:
① 使用了大量的switch…case这样的判断(if…else也是一样),使程序的可阅读性变差。
② 扩展性很差,如果新加了断电的状态,则需要修改上面的判断逻辑。
(2)状态模式:对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。
状态模式包含以下主要角色:
(1)环境(Context)角色: 也称为上下文,它定义了客户程序需要的接口,维护一个当前状态,并将与状态相关的操作委托给当前状态对象来处理。
(2)抽象状态(State)角色: 定义一个接口,用以封装环境对象中的特定状态所对应的行为。
(3)具体状态(Concrete State)角色: 实现抽象状态所对应的行为。
对上述电梯的案例使用状态模式进行改进。类图如下:
具体实现代码如下:
LiftState.java(抽象状态类)
package com.itheima.patterns.behaviorpattern.state.after;
//抽象状态类
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();
}
OpeningState.java(具体状态类)
package com.itheima.patterns.behaviorpattern.state.after;
//电梯开启状态类
public class OpeningState extends LiftState {
//当前状态要执行的方法
public void open() {
System.out.println("电梯开启。。。");
}
public void close() {
//修改状态
super.context.setLiftState(Context.CLOSING_STATE);
//调用当前状态中的context中的close方法
super.context.close();
}
public void run() {
//什么都不做
}
public void stop() {
//什么都不做
}
}
ClosingState.java(具体状态类)
package com.itheima.patterns.behaviorpattern.state.after;
//电梯关闭状态类
public class ClosingState extends LiftState {
@Override
//电梯门关闭,这是关闭状态要实现的动作
public void close() {
System.out.println("电梯门关闭...");
}
//电梯门关了再打开,逗你玩呢,那这个允许呀
@Override
public void open() {
super.context.setLiftState(Context.OPENING_STATE);
super.context.open();
}
//电梯门关了就跑,这是再正常不过了
@Override
public void run() {
super.context.setLiftState(Context.RUNNING_STATE);
super.context.run();
}
//电梯门关着,我就不按楼层
@Override
public void stop() {
super.context.setLiftState(Context.STOPPING_STATE);
super.context.stop();
}
}
RunningState.java(具体状态类)
package com.itheima.patterns.behaviorpattern.state.after;
//电梯运行状态类
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.STOPPING_STATE);
super.context.stop();
}
}
StoppingState.java(具体状态类)
package com.itheima.patterns.behaviorpattern.state.after;
//电梯停止状态类
public class StoppingState extends LiftState {
//停止状态,开门,那是要的!
@Override
public void open() {
//状态修改
super.context.setLiftState(Context.OPENING_STATE);
//动作委托为CloseState来执行,也就是委托给了ClosingState子类执行这个动作
super.context.getLiftState().open();
}
@Override
public void close() {//虽然可以关门,但这个动作不归我执行
//状态修改
super.context.setLiftState(Context.CLOSING_STATE);
//动作委托为CloseState来执行,也就是委托给了ClosingState子类执行这个动作
super.context.getLiftState().close();
}
//停止状态再跑起来,正常的很
@Override
public void run() {
//状态修改
super.context.setLiftState(Context.RUNNING_STATE);
//动作委托为CloseState来执行,也就是委托给了ClosingState子类执行这个动作
super.context.getLiftState().run();
}
//停止状态是怎么发生的呢?当然是停止方法执行了
@Override
public void stop() {
System.out.println("电梯停止了...");
}
}
Context.java(环境类)
package com.itheima.patterns.behaviorpattern.state.after;
//环境角色类
public class Context {
//定义对应状态对象的常量
public final static OpeningState OPENING_STATE = new OpeningState();
public final static ClosingState CLOSING_STATE = new ClosingState();
public final static RunningState RUNNING_STATE = new RunningState();
public final static StoppingState STOPPING_STATE = new StoppingState();
//定义一个当前电梯状态变量
private LiftState liftState;
public LiftState getLiftState() {
return liftState;
}
//设置当前状态对象
public void setLiftState(LiftState liftState) {
this.liftState = liftState;
//设置当前状态对象中的Context对象
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();
}
}
Client.java
package com.itheima.patterns.behaviorpattern.state.after;
public class Client {
public static void main(String[] args) {
//创建环境角色对象
Context context = new Context();
//设置当前电梯装填
context.setLiftState(new ClosingState());
context.open();
context.run();
context.close();
context.stop();
}
}
(1)优点
① 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
② 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
(2)缺点
① 状态模式的使用必然会增加系统类和对象的个数。
② 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
③ 状态模式对"开闭原则"的支持并不太好。
(1)当一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为时,就可以考虑使用状态模式。
(2)一个操作中含有庞大的分支结构,并且这些分支决定于对象的状态时。
观察者模式:又被称为发布-订阅(Publish/Subscribe)模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己。
在观察者模式中有如下角色:
(1)Subject:抽象主题(抽象被观察者),抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象。
(2)ConcreteSubject:具体主题(具体被观察者),该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。
(3)Observer:抽象观察者,是观察者的抽象类,它定义了一个更新接口,使得在得到主题更改通知时更新自己。
(4)ConcrereObserver:具体观察者,实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态。
【例】微信公众号
在使用微信公众号时,大家都会有这样的体验,当你关注的公众号中有新内容更新的话,它就会推送给关注公众号的微信用户端。我们使用观察者模式来模拟这样的场景,微信用户就是观察者,微信公众号是被观察者,有多个的微信用户关注了程序猿这个公众号。 具体类图如下:
具体实现代码如下:
Observer.java(抽象观察者)
package com.itheima.patterns.behaviorpattern.observer;
public interface Observer {
void update(String message);
}
Subject.java(抽象主题 / 抽象被观察者)
package com.itheima.patterns.behaviorpattern.observer;
//抽象主题角色
public interface Subject {
//添加订阅者(观察者对象)
void attach(Observer observer);
//删除订阅者
void detach(Observer observer);
//通知订阅者更新消息
void notify(String message);
}
WeiXinObserver.java(具体观察者)
package com.itheima.patterns.behaviorpattern.observer;
//具体的观察者角色类
public class WeiXinObserver implements Observer{
private String name;
public WeiXinObserver(String name) {
this.name = name;
}
@Override
public void update(String message) {
System.out.println(name + " : " + message);
}
}
SubscriptionSubject.java(具体主题 / 具体被观察者)
package com.itheima.patterns.behaviorpattern.observer;
import java.util.ArrayList;
import java.util.List;
//具体主题角色类
public class SubscriptionSubject implements Subject{
//定义一个集合,用来存储多个观察者对象
private List<Observer> weiXinUserList = new ArrayList<>();
@Override
public void attach(Observer observer) {
weiXinUserList.add(observer);
}
@Override
public void detach(Observer observer) {
weiXinUserList.remove(observer);
}
@Override
public void notify(String message) {
//遍历集合
for (Observer observer : weiXinUserList) {
//调用观察者对象中的update方法
observer.update(message);
}
}
}
Client.java
package com.itheima.patterns.behaviorpattern.observer;
public class Client {
public static void main(String[] args) {
//1.创建公众号对象
SubscriptionSubject subject = new SubscriptionSubject();
//2.订阅公众号
subject.attach(new WeiXinObserver("Tom"));
subject.attach(new WeiXinObserver("Mike"));
subject.attach(new WeiXinObserver("Jerry"));
//3.公众号更新
subject.notify("Java专栏更新了!");
}
}
(1)优点
① 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。
② 被观察者发送通知,所有注册的观察者都会收到信息【可以实现广播机制】。
(2)缺点
① 如果观察者非常多的话,那么所有的观察者收到被观察者发送的通知会耗时。
② 如果被观察者有循环依赖的话,那么被观察者发送通知会使观察者循环调用,会导致系统崩溃。
(1)对象间存在一对多关系,一个对象的状态发生改变会影响其他对象。
(2)当一个抽象模型有两个方面,其中一个方面依赖于另一方面时。
在 Java 中,通过 java.util.Observable 类和 java.util.Observer 接口定义了观察者模式,只要实现它们的子类就可以编写观察者模式实例。
(1)Observable类
其源码如下:
package java.util;
public class Observable {
/*
changed为一个boolean类型的内部标志,注明目标对象是否发生了变化
当它为true时,notifyObservers()才会通知观察者
*/
private boolean changed = false;
//存储所有的观察者对象
private Vector<Observer> obs;
public Observable() {
obs = new Vector<>();
}
//用于将新的观察者对象添加到集合中
public synchronized void addObserver(Observer o) {
if (o == null)
throw new NullPointerException();
if (!obs.contains(o)) {
obs.addElement(o);
}
}
//从集合中删除一个观察者
public synchronized void deleteObserver(Observer o) {
obs.removeElement(o);
}
public void notifyObservers() {
notifyObservers(null);
}
public void notifyObservers(Object arg) {
Object[] arrLocal;
synchronized (this) {
if (!changed)
return;
arrLocal = obs.toArray();
clearChanged();
}
for (int i = arrLocal.length-1; i>=0; i--)
//逆序通知集合中的观察者更新消息
((Observer)arrLocal[i]).update(this, arg);
}
//删除所集合中所有的观察者
public synchronized void deleteObservers() {
obs.removeAllElements();
}
protected synchronized void setChanged() {
changed = true;
}
protected synchronized void clearChanged() {
changed = false;
}
public synchronized boolean hasChanged() {
return changed;
}
//返回集合中观察者的数量
public synchronized int countObservers() {
return obs.size();
}
}
(2)Observer 接口
Observer 接口是抽象观察者,它监视目标对象的变化,当目标对象发生变化时,观察者得到通知,并调用 update() 方法,进行相应的工作。其源码如下:
package java.util;
public interface Observer {
/**
* This method is called whenever the observed object is changed. An
* application calls an Observable object's
* notifyObservers
method to have all the object's
* observers notified of the change.
*
* @param o the observable object.
* @param arg an argument passed to the notifyObservers
* method.
*/
void update(Observable o, Object arg);
}
(3)【例】警察抓小偷
警察抓小偷也可以使用观察者模式来实现,警察是观察者,小偷是被观察者。代码如下:
① 小偷是一个被观察者,所以需要继承Observable类
package com.itheima.patterns.behaviorpattern.observer.jdk;
import java.util.Observable;
public class Thief extends Observable {
private String name; public Thief(String name) {
this.name = name;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void steal() {
System.out.println("小偷:我偷东西了,有没有人来抓我!!!");
super.setChanged(); //即令changed = true
super.notifyObservers();
}
}
② 警察是一个观察者,所以需要让其实现Observer接口
package com.itheima.patterns.behaviorpattern.observer.jdk;
import java.util.Observable;
import java.util.Observer;
public class Policemen implements Observer {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Policemen(String name) {
this.name = name;
}
@Override
public void update(Observable o, Object arg) {
System.out.println("警察:" + ((Thief) o).getName() + ",我已经盯你很久 了,你可以保持沉默,但你所说的将成为呈堂证供!!!");
}
}
package com.itheima.patterns.behaviorpattern.observer.jdk;
public class Client {
public static void main(String[] args) {
//创建小偷对象
Thief t = new Thief("小偷");
//创建警察对象
Policemen p = new Policemen("小李");
//让警察盯着小偷
t.addObserver(p);
//小偷偷东西
t.steal();
}
}
结果如下:
小偷:我偷东西了,有没有人来抓我!!!
警察:小偷,我已经盯你很久 了,你可以保持沉默,但你所说的将成为呈堂证供!!!
(1)一般来说,同事类之间的关系是比较复杂的,多个同事类之间互相关联时,他们之间的关系会呈现为复杂的网状结构,这是一种过度耦合的架构,即不利于类的复用,也不稳定。例如在下左图中,有六个同事类对象,假如对象1发生变化,那么将会有4个对象受到影响。如果对象2发生变化,那么将会有5个对象受到影响。也就是说,同事类之间直接关联的设计是不好的。
(2)如果引入中介者模式,那么同事类之间的关系将变为星型结构,从下右图中可以看到,任何一个类的变动,只会影响的类本身,以及中介者,这样就减小了系统的耦合。一个好的设计,必定不会把所有的对象关系处理逻辑封装在本类中,而是使用一个专门的类来管理那些不属于自己的行为。
(3)中介者模式:又叫调停模式,定义一个中介角色来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。
中介者模式包含以下主要角色:
(1)抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。
(2)具体中介者(ConcreteMediator)角色:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
(3)抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。
(4)具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。
【例】租房
现在租房基本都是通过房屋中介,房主将房屋托管给房屋中介,而租房者从房屋中介获取房屋信息。房屋中介充当租房者与房屋所有者之间的中介者。其类图下:
具体实现代码如下:
Mediator.java(抽象中介者)
package com.itheima.patterns.behaviorpattern.mediator;
//抽象中介者类
public abstract class Mediator {
public abstract void constact(String message,Person person);
}
Person.java(抽象同事类)
package com.itheima.patterns.behaviorpattern.mediator;
//抽象同事类
public abstract class Person {
protected String name;
protected Mediator mediator;
public Person(String name, Mediator mediator) {
this.name = name;
this.mediator = mediator;
}
}
Tenant.java(具体同事类)
package com.itheima.patterns.behaviorpattern.mediator;
//租房者
public class Tenant extends Person{
public Tenant(String name, Mediator mediator) {
super(name, mediator);
}
//和中介沟通的方法
public void constact(String message){
mediator.constact(message,this);
}
//获取信息
public void getMessage(String message){
System.out.println("租房者"+name+"获取到的信息是:"+message);
}
}
HouseOwner.java(具体同事类)
package com.itheima.patterns.behaviorpattern.mediator;
//房主类
public class HouseOwner extends Person{
public HouseOwner(String name, Mediator mediator) {
super(name, mediator);
}
//和中介沟通的方法
public void constact(String message){
mediator.constact(message,this);
}
//获取信息
public void getMessage(String message){
System.out.println("房主"+name+"获取到的信息是:"+message);
}
}
MediatorStructure.java(具体中介者)
package com.itheima.patterns.behaviorpattern.mediator;
//具体的中介者角色类
public class MediatorStructure extends Mediator{
//聚合房主和具体的租房者对象
private HouseOwner houseOwner;
private Tenant tenant;
public HouseOwner getHouseOwner() {
return houseOwner;
}
public void setHouseOwner(HouseOwner houseOwner) {
this.houseOwner = houseOwner;
}
public Tenant getTenant() {
return tenant;
}
public void setTenant(Tenant tenant) {
this.tenant = tenant;
}
@Override
public void constact(String message, Person person) {
if(person == houseOwner){
tenant.getMessage(message);
}else{
houseOwner.getMessage(message);
}
}
}
Client.java
package com.itheima.patterns.behaviorpattern.mediator;
public class Client {
public static void main(String[] args) {
//创建中介者对象
MediatorStructure mediator = new MediatorStructure();
//创建租房者对象
Tenant tenant = new Tenant("李四",mediator);
//创建房主对象
HouseOwner houseOwner = new HouseOwner("张三",mediator);
//中介者要知道具体的房主和租房者
mediator.setTenant(tenant);
mediator.setHouseOwner(houseOwner);
tenant.constact("我要租三室的房子!");
houseOwner.constact("我这里有三室的房子,你要租吗?");
}
}
结果如下:
房主张三获取到的信息是:我要租三室的房子!
租房者李四获取到的信息是:我这里有三室的房子,你要租吗?
(1)优点
① 松散耦合
中介者模式通过把多个同事对象之间的交互封装到中介者对象里面,从而使得同事对象之间松散耦合,基本上可以做到互补依赖。这样一来,同事对象就可以独立地变化和复用,而不再像以前那样“牵一处而动全身”了。
② 集中控制交互
多个同事对象的交互,被封装在中介者对象里面集中管理,使得这些交互行为发生变化的时候,只需要修改中介者对象就可以了,当然如果是已经做好的系统,那么就扩展中介者对象,而各个同事类不需要做修改。
③ 一对多关联转变为一对一的关联
没有使用中介者模式的时候,同事对象之间的关系通常是一对多的,引入中介者对象以后,中介者对象和同事对象的关系通常变成双向的一对一,这会让对象的关系更容易理解和实现。
(2)缺点
当同事类太多时,中介者的职责将很大,它会变得复杂而庞大,以至于系统难以维护。
(1)系统中对象之间存在复杂的引用关系,系统结构混乱且难以理解。
(2)当想创建一个运行于多个类之间的对象,又不想生成新的子类时。
迭代器模式:提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。
迭代器模式主要包含以下角色:
(1)抽象聚合(Aggregate)角色: 定义存储、添加、删除聚合元素以及创建迭代器对象的接口。
(2)具体聚合(ConcreteAggregate)角色: 实现抽象聚合类,返回一个具体迭代器的实例。
(3)抽象迭代器(Iterator)角色: 定义访问和遍历聚合元素的接口,通常包含 hasNext()、next() 等方法。
(4)具体迭代器(Concretelterator)角色: 实现抽象迭代器接口中所定义的方法,完成对聚合对象的遍历,记录遍历的当前位置。
【例】定义一个可以存储学生对象的容器对象,将遍历该容器的功能交由迭代器实现,其类图如下:
具体实现代码如下:
Student.java
package com.itheima.patterns.behaviorpattern.Iterator;
public class Student {
private String name;
private String number;
public Student() {
}
public Student(String name, String number) {
this.name = name;
this.number = number;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", 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;
}
}
StudentIterator.java(抽象迭代器)
package com.itheima.patterns.behaviorpattern.Iterator;
//抽象迭代器角色接口
public interface StudentIterator {
//判断是否还有元素
boolean hasNext();
//获取下一个元素
Student next();
}
StudentIteratorImpl.java(具体迭代器)
package com.itheima.patterns.behaviorpattern.Iterator;
import java.util.List;
//具体迭代器角色类
public class StudentIteratorImpl implements StudentIterator{
private List<Student> list;
//用来记录遍历时的位置
private int position = 0;
public StudentIteratorImpl(List<Student> list) {
this.list = list;
}
@Override
public boolean hasNext() {
return position < list.size();
}
@Override
public Student next() {
//从集合中或者去指定位置的元素
Student currentStudent = list.get(position);
position++;
return currentStudent;
}
}
StudentAggregate.java(抽象聚合)
package com.itheima.patterns.behaviorpattern.Iterator;
//抽象聚合(容器)角色接口
public interface StudentAggregate {
//添加学生
void addStudent(Student stu);
//删除学生
void removeStudent(Student stu);
//获取迭代器对象
StudentIterator getStudentIterator();
}
StudentAggregateImpl.java(具体聚合)
package com.itheima.patterns.behaviorpattern.Iterator;
import java.util.ArrayList;
import java.util.List;
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 StudentIterator getStudentIterator() {
return new StudentIteratorImpl(list);
}
}
Client.java
package com.itheima.patterns.behaviorpattern.Iterator;
public class Client {
public static void main(String[] args) {
//创建聚合(容器)对象
StudentAggregateImpl aggregate = new StudentAggregateImpl();
Student student1 = new Student("Tom", "1001");
Student student2 = new Student("Mike", "1002");
Student student3 = new Student("Jerry", "1003");
Student student4 = new Student("Mary", "1004");
//添加元素
aggregate.addStudent(student1);
aggregate.addStudent(student2);
aggregate.addStudent(student3);
aggregate.addStudent(student4);
//删除元素
aggregate.removeStudent(student3);
//遍历聚合对象
//1.获取迭代器对象
StudentIterator iterator = aggregate.getStudentIterator();
//2.遍历
while (iterator.hasNext()){
//3.获取元素
Student student = iterator.next();
System.out.println(student.toString());
}
}
}
结果如下:
Student{name='Tom', number='1001'}
Student{name='Mike', number='1002'}
Student{name='Mary', number='1004'}
(1)优点
① 它支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式。在迭代器模式中只需要用一个不同的迭代器来替换原有迭代器即可改变遍历算法,我们也可以自己定义迭代器的子类以支持新的遍历方式。
② 迭代器简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据遍历等方法,这样可以简化聚合类的设计。
③ 在迭代器模式中,由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改原有代码,满足 “开闭原则” 的要求。
(2)缺点
增加了类的个数,这在一定程度上增加了系统的复杂性。
(1)当需要为聚合对象提供多种遍历方式时。
(2)当需要为遍历不同的聚合结构提供一个统一的接口时。
(3)当访问一个聚合对象的内容而无须暴露其内部细节的表示时。
(1)迭代器模式在Java的很多集合类中被广泛应用,接下来看下面的例子:
public class TestArratList {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
//添加元素
list.add("1001");
list.add("1002");
list.add("1003");
//删除元素
list.remove("1003");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
结果如下:
1001
1002
(2)看完这段代码是不是觉得很熟悉,它与我们上面的代码基本类似。单列集合都使用到了迭代器,所以现在以ArrayList举例来说明
① List:抽象聚合类
② ArrayList:具体的聚合类
③ Iterator:抽象迭代器
④ list.iterator():返回的是实现了 Iterator 接口的具体迭代器对象
(3)具体来看看 ArrayList的部分关键代码实现:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
public Iterator<E> iterator() {
return new Itr();
}
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];
}
...
}
(4)这部分代码大致就是在 iterator 方法中返回了一个实例化的 Iterator 对象。Itr是一个内部类,它实现了 Iterator 接口并重写了其中的抽象方法。
(5)当我们在使用Java开发的时想使用迭代器模式的话,只要让我们自己定义的容器类实现java.util.Iterable 并实现其中的iterator()方法使其返回一个 java.util.Iterator 的实现类就可以了。
访问者模式:封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作。
访问者模式包含以下主要角色:
(1)抽象访问者(Visitor)角色: 定义了对每一个元素 (Element) 访问的行为,它的参数就是可以访问的元素,它的方法个数理论上来讲与元素类个数(Element的实现类个数)是一样的,从这点不难看出,访问者模式要求元素类的个数不能改变。
(2)具体访问者(ConcreteVisitor)角色: 给出对每一个元素类访问时所产生的具体行为。
(3)抽象元素(Element)角色: 定义了一个接受访问者的方法( accept ),其意义是指,每一个元素都要可以被访问者访问。
(4)具体元素(ConcreteElement)角色: 提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。
(5)对象结构(Object Structure)角色: 定义当中所提到的对象结构,对象结构是一个抽象表述,具体点可以理解为一个具有容器性质或者复合对象特性的类,它会含有一组元素( Element ),并且可以迭代这些元素,供访问者访问。
【例】给宠物喂食
现在养宠物的人特别多,我们就以这个为例,当然宠物还分为狗,猫等,要给宠物喂食的话,主人可以喂,其他人也可以喂食。类图如下:
具体实现代码如下:
Person.java(抽象访问者)
package com.itheima.patterns.behaviorpattern.visitor;
//抽象访问者角色接口
public interface Person {
//给宠物猫喂食
void feed(Cat cat);
//给宠物狗喂食
void feed(Dog dog);
}
Animal.java(抽象元素)
package com.itheima.patterns.behaviorpattern.visitor;
//抽象元素角色类
public interface Animal {
//接受访问者访问的功能
void accept(Person person);
}
Cat.java(具体元素)
package com.itheima.patterns.behaviorpattern.visitor;
//具体元素角色类(宠物猫)
public class Cat implements Animal{
@Override
public void accept(Person person) {
//访问者给宠物猫喂食
person.feed(this);
System.out.println("宠物猫接受喂食");
}
}
Dog.java(具体元素)
package com.itheima.patterns.behaviorpattern.visitor;
//具体元素角色类(宠物狗)
public class Dog implements Animal{
@Override
public void accept(Person person) {
//访问者给宠物狗喂食
person.feed(this);
System.out.println("宠物狗接受喂食");
}
}
Owner.java(具体访问者)
package com.itheima.patterns.behaviorpattern.visitor;
//具体访问者角色类(宠物主人)
public class Owner implements Person{
@Override
public void feed(Cat cat) {
System.out.println("主人给猫喂食");
}
@Override
public void feed(Dog dog) {
System.out.println("主人给狗喂食");
}
}
SomeOne.java(具体访问者)
package com.itheima.patterns.behaviorpattern.visitor;
//具体访问者角色类(其他人)
public class SomeOne implements Person{
@Override
public void feed(Cat cat) {
System.out.println("其他人给猫喂食");
}
@Override
public void feed(Dog dog) {
System.out.println("其他人给猫喂食");
}
}
Home.java(对象结构)
package com.itheima.patterns.behaviorpattern.visitor;
import java.util.ArrayList;
import java.util.List;
//对象结构类
public class Home {
//声明一个集合对象,用来存储元素对象
private List<Animal> nodeList = new ArrayList<>();
//添加元素
public void add(Animal animal){
nodeList.add(animal);
}
public void action(Person person){
//遍历集合,获取每一个元素,让访问者访问每一个元素
for (Animal animal : nodeList) {
animal.accept(person);
}
}
}
Client.java
package com.itheima.patterns.behaviorpattern.visitor;
public class Client {
public static void main(String[] args) {
//创建Home对象
Home home = new Home();
//添加元素到home对象中
home.add(new Dog());
home.add(new Cat());
//创建主人对象
Owner owner = new Owner();
//让主人喂食所有的宠物
home.action(owner);
System.out.println("===============");
//创建其他人对象
SomeOne someOne = new SomeOne();
//让其他人喂食所有的宠物
home.action(someOne);
}
}
结果如下:
主人给狗喂食
宠物狗接受喂食
主人给猫喂食
宠物猫接受喂食
===============
其他人给猫喂食
宠物狗接受喂食
其他人给猫喂食
宠物猫接受喂食
(1)优点
扩展性好 | 在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。 |
---|---|
复用性好 | 通过访问者来定义整个对象结构通用的功能,从而提高复用程度。 |
分离无关行为 | 通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一。 |
(2)缺点
对象结构变化很困难 | 在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。 |
---|---|
违反了依赖倒置原则 | 访问者模式依赖了具体类,而没有依赖抽象类。 |
(1)对象结构相对稳定,但其操作算法经常变化的程序。
(2)对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。
事实上,访问者模式用到了一种名为双分派的技术。
(1)分派
变量被声明时的类型叫做变量的静态类型,有些人又把静态类型叫做明显类型;而变量所引用的对象的真实类型又叫做变量的实际类型。比如 Map map = new HashMap() ,map变量的静态类型是 Map,实际类型是 HashMap 。根据对象的类型而对方法进行的选择,就是分派(Dispatch),分派(Dispatch)又分为两种,即静态分派和动态分派。
① 静态分派(Static Dispatch) 发生在编译时期,分派根据静态类型信息发生。静态分派对于我们来说并不陌生,方法重载就是静态分派。
② 动态分派(Dynamic Dispatch) 发生在运行时期,动态分派动态地置换掉某个方法。Java通过方法的重写支持动态分派。
(2)动态分配
通过方法的重写支持动态分派。
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编译器在编译时期并不总是知道哪些代码会被执行,因为编译器仅仅知道对象的静态类型,而不知道对象的真实类型;而方法的调用则是根据对象的真实类型,而不是静态类型。
(3)静态分配
通过方法重载支持静态分派。
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();
Execute exe = new Execute();
exe.execute(a);
exe.execute(a1);
exe.execute(a2);
}
}
运行结果如下:
animal
animal
animal
这个结果可能出乎一些人的意料了,为什么呢?因为重载方法的分派是根据静态类型进行的,这个分派过程在编译时期就完成了。
(4)双分派
所谓双分派技术就是在选择一个方法的时候,不仅仅要根据消息接收者(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,就是具体的实际类型的对象。
② 说到这里,我们已经明白双分派是怎么回事了,但是它有什么效果呢?就是可以实现方法的动态绑定,我们可以对上面的程序进行修改。
③ 双分派实现动态绑定的本质,就是在重载方法委派的前面加上了继承体系中覆盖的环节,由于覆盖是动态的,所以重载就是动态的了。
(1)备忘录模式:又叫快照模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。
(2)备忘录模式提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂时存储起来的备忘录将状态复原,很多软件都提供了撤销(Undo)操作,如 Word、记事本、Photoshop、IDEA等软件在编辑时按 Ctrl+Z 组合键时能撤销当前操作,使文档恢复到之前的状态;还有在浏览器中的后退键、数据库事务管理中的回滚操作、玩游戏时的中间结果存档功能、数据库与操作系统的备份操作、棋类游戏中的悔棋功能等都属于这类。
备忘录模式的主要角色如下:
(1)发起人(Originator)角色: 记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
(2)备忘录(Memento)角色: 负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
(3)管理者(Caretaker)角色: 对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。
除此之外,备忘录有两个等效的接口:
窄接口: 管理者(Caretaker)对象(和其他发起人对象之外的任何对象)看到的是备忘录的窄接口(narror Interface),这个窄接口只允许他把备忘录对象传给其他的对象。
宽接口: 与管理者看到的窄接口相反,发起人对象可以看到一个宽接口(wide Interface),这个宽接口允许它读取所有的数据,以便根据这些数据恢复这个发起人对象的内部状态。
【例】游戏挑战BOSS
游戏中的某个场景,一游戏角色有生命力、攻击力、防御力等数据,在打Boss前和后一定会不一样的,我们允许玩家如果感觉与Boss决斗的效果不理想可以让游戏恢复到决斗之前的状态。
(1)“白箱”备忘录模式
备忘录角色对任何对象都提供一个接口,即宽接口,备忘录角色的内部所存储的状态就对所有对象公开。类图如下:
具体实现代码如下:
RoleStateMemento.java(备忘录角色)
package com.itheima.patterns.behaviorpattern.memento.whitebox;
//备忘录角色类(存储历史状态)
public class RoleStateMemento {
//生命力
private int vit;
//攻击力
private int atk;
//防御力
private int def;
public RoleStateMemento() {
}
public RoleStateMemento(int vit, int atk, int def) {
this.vit = vit;
this.atk = atk;
this.def = 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;
}
}
GameRole.java(发起人角色)
package com.itheima.patterns.behaviorpattern.memento.whitebox;
//游戏角色类(发起人角色)
public class GameRole {
//生命力
private int vit;
//攻击力
private int atk;
//防御力
private int def;
//初始化内部状态
public void initState(){
this.vit = 100;
this.atk = 100;
this.def = 100;
}
//战斗
public void fight(){
this.vit = 0;
this.atk = 0;
this.def = 0;
}
//保存角色状态功能
public RoleStateMemento saveState(){
return new RoleStateMemento(vit,atk,def);
}
//恢复角色状态
public void recoverState(RoleStateMemento roleStateMemento){
//将备忘录对象中存储的状态赋值给当前对象成员
this.vit = roleStateMemento.getVit();
this.atk = roleStateMemento.getAtk();
this.def = roleStateMemento.getDef();
}
//展示状态功能
public void stateDisplay(){
System.out.println("角色生命力:" + vit);
System.out.println("角色攻击力:" + atk);
System.out.println("角色防御力:" + 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;
}
}
RoleStateCaretaker.java(管理者角色)
package com.itheima.patterns.behaviorpattern.memento.whitebox;
//备忘录对象管理对象
public class RoleStateCaretaker {
//声明RoleStateMemento类型的变量
private RoleStateMemento roleStateMemento;
public RoleStateMemento getRoleStateMemento() {
return roleStateMemento;
}
public void setRoleStateMemento(RoleStateMemento roleStateMemento) {
this.roleStateMemento = roleStateMemento;
//白箱
//roleStateMemento.setAtk(100);
}
}
Client.java
package com.itheima.patterns.behaviorpattern.memento.whitebox;
public class Client {
public static void main(String[] args) {
System.out.println("---------------大战boos前-----------------");
//创建游戏角色对象
GameRole gameRole = new GameRole();
gameRole.initState();//初始化状态操作
gameRole.stateDisplay();
//将该游戏角色内部状态进行备份
//创建管理者对象
RoleStateCaretaker roleStateCaretaker = new RoleStateCaretaker();
roleStateCaretaker.setRoleStateMemento(gameRole.saveState());
System.out.println("---------------大战boos后-----------------");
//损耗严重
gameRole.fight();
gameRole.stateDisplay();
System.out.println("---------------恢复之前的状态-----------------");
gameRole.recoverState(roleStateCaretaker.getRoleStateMemento());
gameRole.stateDisplay();
}
}
结果如下:
---------------大战boos前-----------------
角色生命力:100
角色攻击力:100
角色防御力:100
---------------大战boos后-----------------
角色生命力:0
角色攻击力:0
角色防御力:0
---------------恢复之前的状态-----------------
角色生命力:100
角色攻击力:100
角色防御力:100
分析:白箱备忘录模式是破坏封装性的。但是通过程序员自律,同样可以在一定程度上实现模式的大部分用意。
(2)“黑箱”备忘录模式
备忘录角色对发起人对象提供一个宽接口,而为其他对象提供一个窄接口。在Java语言中,实现双重接口的办法就是将备忘录类设计成发起人类的内部成员类。 将 RoleStateMemento 设为 GameRole 的内部类,从而将 RoleStateMemento 对象封装在GameRole 里面;在外面提供一个标识接口 Memento 给 RoleStateCaretaker 及其他对象使用。这样 GameRole 类看到的是 RoleStateMemento 所有的接口,而 RoleStateCaretaker 及其他对象看到的仅仅是标识接口 Memento 所暴露出来的接口,从而维护了封装型。其类图如下:
具体实现代码如下:
Memento.java(备忘录接口)
package com.itheima.patterns.behaviorpattern.memento.blackbox;
//备忘录接口,对外提供窄接口
public interface Memento {
}
GameRole.java(发起人角色)
package com.itheima.patterns.behaviorpattern.memento.blackbox;
//游戏角色类(属于发起人角色)
public class GameRole {
private int vit; //生命力
private int atk; //攻击力
private int def; //防御力
//初始化内部状态
public void initState() {
this.vit = 100;
this.atk = 100;
this.def = 100;
}
//战斗
public void fight() {
this.vit = 0;
this.atk = 0;
this.def = 0;
}
//保存角色状态功能
public Memento saveState() {
return new RoleStateMemento(vit,atk,def);
}
//恢复角色状态
public void recoverState(Memento memento) {
RoleStateMemento roleStateMemento = (RoleStateMemento) memento;
//将备忘录对象中存储的状态赋值给当前对象的成员
this.vit = roleStateMemento.getVit();
this.atk = roleStateMemento.getAtk();
this.def = roleStateMemento.getDef();
}
//展示状态功能
public void stateDisplay() {
System.out.println("角色生命力:" + vit);
System.out.println("角色攻击力:" + atk);
System.out.println("角色防御力:" + 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;
}
//在内部定义备忘录内部类 RoleStateMemento (该内部类设置为私有的)
private class RoleStateMemento implements Memento {
private int vit; //生命力
private int atk; //攻击力
private int def; //防御力
public RoleStateMemento(int vit, int atk, int def) {
this.vit = vit;
this.atk = atk;
this.def = def;
}
public RoleStateMemento() {
}
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;
}
}
}
RoleStateCaretaker.java(管理者角色)
package com.itheima.patterns.behaviorpattern.memento.blackbox;
//备忘录对象管理对象
public class RoleStateCaretaker {
/*
负责人角色类 RoleStateCaretaker 能够得到的备忘录对象是以 Memento 为接口的,由于
这个接口仅仅是一个标识接口,因此负责人角色不可能改变这个备忘录对象的内容。
*/
//声明RoleStateMemento类型的变量
private Memento memento;
public Memento getMemento() {
return memento;
}
public void setMemento(Memento memento) {
this.memento = memento;
}
}
package com.itheima.patterns.behaviorpattern.memento.blackbox;
public class Client {
public static void main(String[] args) {
System.out.println("---------------大战boos前-----------------");
//创建游戏角色对象
GameRole gameRole = new GameRole();
gameRole.initState();//初始化状态操作
gameRole.stateDisplay();
//将该游戏角色内部状态进行备份
//创建管理者对象
RoleStateCaretaker roleStateCaretaker = new RoleStateCaretaker();
roleStateCaretaker.setMemento(gameRole.saveState());
System.out.println("---------------大战boos后-----------------");
//损耗严重
gameRole.fight();
gameRole.stateDisplay();
System.out.println("---------------恢复之前的状态-----------------");
gameRole.recoverState(roleStateCaretaker.getMemento());
gameRole.stateDisplay();
}
}
结果与“黑箱”备忘录模式的一样。
(1)优点
① 提供了一种可以恢复状态的机制。当用户需要时能够方便地将数据恢复到某个历史的状态。
② 实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
③ 简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。
(2)缺点
如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。
(1)需要保存与恢复数据的场景,如玩游戏时的中间结果的存档功能。
(2)需要提供一个可回滚操作的场景,如 Word、记事本、Photoshop,idea等软件在编辑时按Ctrl+Z 组合键,还有数据库中事务操作。
(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等等。显然,现在需要一种翻译识别机器,能够解析由数字以及 + - 符号构成的合法的运算序列。如果把运算符和数字都看作节点的话,能够逐个节点的进行读取解析运算,这就是解释器模式的思维。
(2)解释器模式:给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。
(3)在解释器模式中,我们需要将待解决的问题,提取出规则,抽象为一种“语言”。比如加减法运算,规则为:由数值和±符号组成的合法序列,“1+3-2” 就是这种语言的句子。解释器就是要解析出来语句的含义。但是如何描述规则呢?
(4)文法/语法规则(用于描述语言的语法结构的形式规则):
expression ::= value | plus | minus
plus ::= expression '+' expression
minus ::= expression '-' expression
value ::= integer
注意: 这里的符号“::=”表示“定义为”的意思,竖线 | 表示或,左右的其中一个,引号内为字符本身,引号外为语法。
上面规则描述为 :表达式可以是一个值,也可以是plus或者minus运算,而plus和minus又是由表达式结合运算符构成,值的类型为整型数。
(5)抽象语法树
在计算机科学中,抽象语法树(AbstractSyntaxTree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。用树形来表示符合文法规则的句子:
解释器模式包含以下主要角色:
(1)抽象表达式(Abstract Expression)角色: 定义解释器的接口,约定解释器的解释操作,主要包含解释方法 interpret()。
(2)终结符表达式(Terminal Expression)角色: 是抽象表达式的子类,用来实现文法中与终结符相关的操作,文法中的每一个终结符都有一个具体终结表达式与之相对应。
(3)非终结符表达式(Nonterminal Expression)角色: 也是抽象表达式的子类,用来实现文法中与非终结符相关的操作,文法中的每条规则都对应于一个非终结符表达式。
(4)环境(Context)角色: 通常包含各个解释器需要的数据或是公共的功能,一般用来传递被所有解释器共享的数据,后面的解释器可以从这里获取这些值。
(5)客户端(Client): 主要任务是将需要分析的句子或表达式转换成使用解释器对象描述的抽象语法树,然后调用解释器的解释方法,当然也可以通过环境角色间接访问解释器的解释方法。
【例】设计实现加减法的软件
具体实现代码如下:
AbstractExpression.java(抽象表达式角色)
package com.itheima.patterns.behaviorpattern.interpreter;
//抽象表达式类
public abstract class AbstractExpression {
public abstract int interpret(Context context);
}
Variable.java
package com.itheima.patterns.behaviorpattern.interpreter;
//用于封装变量的类
public class Variable extends AbstractExpression{
//声明存储变量名的成员变量
private String name;
public Variable(String name) {
this.name = name;
}
@Override
public int interpret(Context context) {
//直接返回变量的值
return context.getValue(this);
}
@Override
public String toString() {
return name;
}
}
Context.java(环境角色)
package com.itheima.patterns.behaviorpattern.interpreter;
import java.util.HashMap;
import java.util.Map;
//环境角色类
public class Context {
//定义一个map集合,用来存储变量以及对应的值
private Map<Variable,Integer> map = new HashMap<>();
//添加变量的功能
public void assign(Variable var,Integer value){
map.put(var,value);
}
//根据变量获取对应的值
public int getValue(Variable var){
return map.get(var);
}
}
Plus.java
package com.itheima.patterns.behaviorpattern.interpreter;
//加法表达式类
public class Plus extends AbstractExpression{
//+左边的表达式
private AbstractExpression left;
//+右边的表达式
private AbstractExpression right;
public Plus(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret(Context context) {
//将左边表达式的结果和右边的进行相加 2+3
return left.interpret(context) + right.interpret(context);
}
@Override
public String toString() {
return "(" + left.toString() + "+" + right.toString() + ")";
}
}
Minus.java
package com.itheima.patterns.behaviorpattern.interpreter;
//减法表达式类
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) {
//将左边表达式的结果和右边的进行相减 2-3
return left.interpret(context) - right.interpret(context);
}
@Override
public String toString() {
return "(" + left.toString() + "-" + right.toString() + ")";
}
}
Client.java(客户端)
package com.itheima.patterns.behaviorpattern.interpreter;
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,2);
context.assign(c,3);
context.assign(d,4);
//获取抽象语法树 a + b - c + d
AbstractExpression expression = new Minus(a,new Plus(new Minus(b,c),d));
//解释(计算)
int result = expression.interpret(context);
System.out.println(expression + "=" + result);
}
}
结果如下:
(a-((b-c)+d))=-2
(1)优点
① 易于改变和扩展文法
由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言。
② 实现文法较为容易
在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂。
③ 增加新的解释表达式较为方便
如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无须修改,符合 “开闭原则”。
(2)缺点
① 对于复杂文法难以维护
在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护。
② 执行效率较低
由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调试过程也比较麻烦。
(1)当语言的文法较为简单,且执行效率不是关键问题时。
(2)当问题重复出现,且可以用一种简单的语言来进行表达时。
(3)当一个语言需要解释执行,并且语言中的句子可以表示为一个抽象语法树的时候。