设计模式-行为型模式(下)
1.访问者模式
访问者模式在实际开发中使用的非常少,因为它比较难以实现并且应用该模式肯能会导致代码的可读性变差,可维护性变差,在没有特别必要的情况下,不建议使用访问者模式.
访问者模式(Visitor Pattern) 的原始定义是:
允许在运行时将一个或多个操作应用于一组对象,将操作与对象结构分离。
这个定义会比较抽象,但是我们依然能看出两个关键点:
一个是: 运行时使用一组对象的一个或多个操作,比如,对不同类型的文件(.pdf、.xml、.properties)进行扫描;
另一个是: 分离对象的操作和对象本身的结构,比如,扫描多个文件夹下的多个文件,对于文件来说,扫描是额外的业务操作,如果在每个文件对象上都加一个扫描操作,太过于冗余,而扫描操作具有统一性,非常适合访问者模式。
访问者模式主要解决的是数据与算法的耦合问题, 尤其是在数据结构比较稳定,而算法多变的情况下.为了不污染数据本身,访问者会将多种算法独立归档,并在访问数据时根据数据类型自动切换到对应的算法,实现数据的自动响应机制,并确保算法的自由扩展.
1.1访问者模式原理
1.1.1访问者模式包含以下主要角色:
抽象访问者(Visitor)角色:
可以是接口或者抽象类,定义了一系列操作方法,用来处理所有数据元素,通常为同名的访问方法,并以数据元素类作为入参来确定那个重载方法被调用.
具体访问者(ConcreteVisitor)角色:访问者接口的实现类,可以有多个实现,每个访问者都需要实现所有数据元素类型的访问重载方法.
抽象元素(Element)角色:被访问的数据元素接口,定义了一个接受访问者的方法(`accept`),其意义是指,每一个元素都要可以被访问者访问。
具体元素(ConcreteElement)角色:具体数据元素实现类,提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法,其accept实现方法中调用访问者并将自己 "this" 传回。
对象结构(Object Structure)角色:包含所有可能被访问的数据对象的容器,可以提供数据对象的迭代功能,可以是任意类型的数据结构.
客户端 ( Client ) :使用容器并初始化其中各类数据元素,并选择合适的访问者处理容器中的所有数据对象.
1.2访问者模式实现
我们以超市购物为例,假设超市中的三类商品: 水果,糖果,酒水进行售卖. 我们可以忽略每种商品的计价方法,因为最终结账时由收银员统一集中处理,在商品类中添加计价方法是不合理的设计.我们先来定义糖果类和酒类、水果类.
/**
* 抽象商品父类
* @author spikeCong
* @date 2022/10/18
**/
public abstract class Product {
private String name; //商品名
private LocalDate producedDate; // 生产日期
private double price; //单品价格
public Product(String name, LocalDate producedDate, double price) {
this.name = name;
this.producedDate = producedDate;
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public LocalDate getProducedDate() {
return producedDate;
}
public void setProducedDate(LocalDate producedDate) {
this.producedDate = producedDate;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
}
/**
* 糖果类
* @author spikeCong
* @date 2022/10/18
**/
public class Candy extends Product{
public Candy(String name, LocalDate producedDate, double price) {
super(name, producedDate, price);
}
}
/**
* 酒水类
* @author spikeCong
* @date 2022/10/18
**/
public class Wine extends Product{
public Wine(String name, LocalDate producedDate, double price) {
super(name, producedDate, price);
}
}
/**
* 水果类
* @author spikeCong
* @date 2022/10/18
**/
public class Fruit extends Product{
//重量
private float weight;
public Fruit(String name, LocalDate producedDate, double price, float weight) {
super(name, producedDate, price);
this.weight = weight;
}
public float getWeight() {
return weight;
}
public void setWeight(float weight) {
this.weight = weight;
}
}
访问者接口
收银员就类似于访问者,访问用户选择的商品,我们假设根据生产日期进行打折,过期商品不能够出售. 注意这种计价策略不适用于酒类,作为收银员要对不同商品应用不同的计价方法.
/**
* 访问者接口-根据入参不同调用对应的重载方法
* @author spikeCong
* @date 2022/10/18
**/
public interface Visitor {
public void visit(Candy candy); //糖果重载方法
public void visit(Wine wine); //酒类重载方法
public void visit(Fruit fruit); //水果重载方法
}
具体访问者
创建计价业务类,对三类商品进行折扣计价,折扣计价访问者的三个重载方法分别实现了3类商品的计价方法,体现了visit() 方法的多态性.
/**
* 折扣计价访问者类
* @author spikeCong
* @date 2022/10/18
**/
public class DiscountVisitor implements Visitor {
private LocalDate billDate;
public DiscountVisitor(LocalDate billDate) {
this.billDate = billDate;
System.out.println("结算日期: " + billDate);
}
@Override
public void visit(Candy candy) {
System.out.println("糖果: " + candy.getName());
//获取产品生产天数
long days = billDate.toEpochDay() - candy.getProducedDate().toEpochDay();
if(days > 180){
System.out.println("超过半年的糖果,请勿食用!");
}else{
double rate = 0.9;
double discountPrice = candy.getPrice() * rate;
System.out.println("糖果打折后的价格"+NumberFormat.getCurrencyInstance().format(discountPrice));
}
}
@Override
public void visit(Wine wine) {
System.out.println("酒类: " + wine.getName()+",无折扣价格!");
System.out.println("原价: "+NumberFormat.getCurrencyInstance().format(wine.getPrice()));
}
@Override
public void visit(Fruit fruit) {
System.out.println("水果: " + fruit.getName());
//获取产品生产天数
long days = billDate.toEpochDay() - fruit.getProducedDate().toEpochDay();
double rate = 0;
if(days > 7){
System.out.println("超过七天的水果,请勿食用!");
}else if(days > 3){
rate = 0.5;
}else{
rate = 1;
}
double discountPrice = fruit.getPrice() * fruit.getWeight() * rate;
System.out.println("水果价格: "+NumberFormat.getCurrencyInstance().format(discountPrice));
}
public static void main(String[] args) {
LocalDate billDate = LocalDate.now();
Candy candy = new Candy("徐福记",LocalDate.of(2022,10,1),10.0);
System.out.println("糖果: " + candy.getName());
double rate = 0.0;
long days = billDate.toEpochDay() - candy.getProducedDate().toEpochDay();
System.out.println(days);
if(days > 180){
System.out.println("超过半年的糖果,请勿食用!");
}else{
rate = 0.9;
double discountPrice = candy.getPrice() * rate;
System.out.println("打折后的价格"+NumberFormat.getCurrencyInstance().format(discountPrice));
}
}
}
客户端
public class Client {
public static void main(String[] args) {
//德芙巧克力,生产日期2002-5-1 ,原价 10元
Candy candy = new Candy("德芙巧克力",LocalDate.of(2022,5,1),10.0);
Visitor visitor = new DiscountVisitor(LocalDate.of(2022,10,11));
visitor.visit(candy);
}
}
上面的代码虽然可以完成当前的需求,但是设想一下这样一个场景: 由于访问者的重载方法只能对当个的具体商品进行计价,如果顾客选择了多件商品来结账时,就可能会引起重载方法的派发问题(到底该由谁来计算的问题).
首先我们定义一个接待访问者的类 Acceptable,其中定义了一个accept(Visitor visitor)方法, 只要是visitor的子类都可以接收.
/**
* 接待者接口(抽象元素角色)
* @author spikeCong
* @date 2022/10/18
**/
public interface Acceptable {
//接收所有的Visitor访问者的子类实现类
public void accept(Visitor visitor);
}
/**
* 糖果类
* @author spikeCong
* @date 2022/10/18
**/
public class Candy extends Product implements Acceptable{
public Candy(String name, LocalDate producedDate, double price) {
super(name, producedDate, price);
}
@Override
public void accept(Visitor visitor) {
//accept实现方法中调用访问者并将自己 "this" 传回。this是一个明确的身份,不存在任何泛型
visitor.visit(this);
}
}
//酒水与水果类同样实现Acceptable接口,重写accept方法
测试
public class Client {
public static void main(String[] args) {
// //德芙巧克力,生产日期2002-5-1 ,原价 10元
Candy candy = new Candy("德芙巧克力",LocalDate.of(2022,5,1),10.0);
Visitor visitor = new DiscountVisitor(LocalDate.of(2022,10,11));
visitor.visit(candy);
//模拟添加多个商品的操作
List<Acceptable> products = Arrays.asList(
new Candy("金丝猴奶糖",LocalDate.of(2022,6,10),10.00),
new Wine("衡水老白干",LocalDate.of(2020,6,10),100.00),
new Fruit("草莓",LocalDate.of(2022,10,12),50.00,1)
);
Visitor visitor = new DiscountVisitor(LocalDate.of(2022,10,17));
for (Acceptable product : products) {
product.accept(visitor);
}
}
}
代码编写到此出,就可以应对计价方式或者业务逻辑的变化了,访问者模式成功地将数据资源(需实现接待者接口)与数据算法 (需实现访问者接口)分离开来。重载方法的使用让多样化的算法自成体系,多态化的访问者接口保证了系统算法的可扩展性,数据则保持相对固定,最终形成⼀个算法类对应⼀套数据。
1.3访问者模式总结
1.3.1访问者模式优点:
扩展性好
在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
复用性好
通过访问者来定义整个对象结构通用的功能,从而提高复用程度。
分离无关行为
通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一。
1.3.2访问者模式缺点:
对象结构变化很困难
在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
违反了依赖倒置原则
访问者模式依赖了具体类,而没有依赖抽象类。
1.3.3使用场景
当对象的数据结构相对稳定,而操作却经常变化的时候。 比如,上面例子中路由器本身的内部构造(也就是数据结构)不会怎么变化,但是在不同操作系统下的操作可能会经常变化,比如,发送数据、接收数据等。
需要将数据结构与不常用的操作进行分离的时候。 比如,扫描文件内容这个动作通常不是文件常用的操作,但是对于文件夹和文件来说,和数据结构本身没有太大关系(树形结构的遍历操作),扫描是一个额外的动作,如果给每个文件都添加一个扫描操作会太过于重复,这时采用访问者模式是非常合适的,能够很好分离文件自身的遍历操作和外部的扫描操作。
需要在运行时动态决定使用哪些对象和方法的时候。 比如,对于监控系统来说,很多时候需要监控运行时的程序状态,但大多数时候又无法预知对象编译时的状态和参数,这时使用访问者模式就可以动态增加监控行为。
2.备忘录模式
备忘录模式提供了一种对象状态的撤销实现机制,当系统中某一个对象需要恢复到某一历史状态时可以使用备忘录模式进行设计.
很多软件都提供了撤销(Undo)操作,如 Word、记事本、Photoshop、IDEA等软件在编辑时按 Ctrl+Z 组合键时能撤销当前操作,使文档恢复到之前的状态;还有在 浏览器 中的后退键、数据库事务管理中的回滚操作、玩游戏时的中间结果存档功能、数据库与操作系统的备份操作、棋类游戏中的悔棋功能等都属于这类。
备忘录模式(memento pattern)定义: 在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态.
2.1备忘录模式原理
2.1.1备忘录模式的主要角色:
发起人(Originator)角色:
状态需要被记录的元对象类, 记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
看护人(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。
2.2.备忘录模式实现
下面我们再来看看 UML 对应的代码实现。首先,我们创建原始对象 Originator,对象中有四个属性,分别是 state 用于显示当前对象状态,id、name、phone 用来模拟业务属性,并添加 get、set 方法、create() 方法用于创建备份对象,restore(memento) 用于恢复对象状态。
/**
* 发起人类
* @author spikeCong
* @date 2022/10/19
**/
public class Originator {
private String state = "原始对象";
private String id;
private String name;
private String phone;
public Originator() {
}
//创建备忘录对象
public Memento create(){
return new Memento(id,name,phone);
}
//恢复对象状态
public void restore(Memento m){
this.state = m.getState();
this.id = m.getId();
this.name = m.getName();
this.phone = m.getPhone();
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
@Override
public String toString() {
return "Originator{" +
"state='" + state + '\'' +
", id='" + id + '\'' +
", name='" + name + '\'' +
", phone='" + phone + '\'' +
'}';
}
}
/**
* 备忘录对象
* 访问权限为: 默认,也就是同包下可见(保证只有发起者类可以访问备忘录类)
* @author spikeCong
* @date 2022/10/19
**/
class Memento {
private String state = "从备份对象恢复为原始对象";
private String id;
private String name;
private String phone;
public Memento() {
}
public Memento(String id, String name, String phone) {
this.id = id;
this.name = name;
this.phone = phone;
}
//get、set、toString......
}
/**
* 负责人类-保存备忘录对象
* @author spikeCong
* @date 2022/10/19
**/
public class Caretaker {
private Memento memento;
public Memento getMemento() {
return memento;
}
public void setMemento(Memento memento) {
this.memento = memento;
}
}
public class Client {
public static void main(String[] args) {
//创建发起人对象
Originator originator = new Originator();
originator.setId("1");
originator.setName("spike");
originator.setPhone("13512341234");
System.out.println("=============" + originator);
//创建负责人对象,并保存备忘录对象
Caretaker caretaker = new Caretaker();
caretaker.setMemento(originator.create());
//修改
originator.setName("update");
System.out.println("=============" + originator);
//从负责人对象中获取备忘录对象,实现撤销
originator.restore(caretaker.getMemento());
System.out.println("=============" + originator);
}
}
2.3 备忘录模式应用实例
设计一个收集水果和获取金钱数的掷骰子游戏,游戏规则如下
1. 游戏玩家通过扔骰子来决定下一个状态
2. 当点数为1,玩家金钱增加
3. 当点数为2,玩家金钱减少
4. 当点数为6,玩家会得到水果
5. 当钱消耗到一定程度,就恢复到初始状态
Memento类: 表示玩家的状态
/**
* Memento 表示状态
* @author spikeCong
* @date 2022/10/19
**/
public class Memento {
int money; //所持金钱
ArrayList fruits; //获得的水果
//构造函数
Memento(int money) {
this.money = money;
this.fruits = new ArrayList();
}
//获取当前玩家所有的金钱
int getMoney() {
return money;
}
//获取当前玩家所有的水果
List getFruits() {
return (List)fruits.clone();
}
//添加水果
void addFruit(String fruit){
fruits.add(fruit);
}
}
Player玩家类,只要玩家的金币还够,就会一直进行游戏,在该类中会设置一个createMemento方法,其作用是保存当前玩家状态.还会包含一个restore撤销方法,相当于复活操作.
package com.mashibing.memento.example02;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* @author spikeCong
* @date 2022/10/19
**/
public class Player {
private int money; //所持金钱
private List<String> fruits = new ArrayList(); //获得的水果
private Random random = new Random(); //随机数对象
private static String[] fruitsName={ //表示水果种类的数组
"苹果","葡萄","香蕉","橘子"
};
//构造方法
public Player(int money) {
this.money = money;
}
//获取当前所持有的金钱
public int getMoney() {
return money;
}
//获取一个水果
public String getFruit() {
String prefix = "";
if (random.nextBoolean()) {
prefix = "好吃的";
}
//从数组中获取水果
String f = fruitsName[random.nextInt(fruitsName.length)];
return prefix + f;
}
//掷骰子游戏
public void yacht(){
int dice = random.nextInt(6) + 1; //掷骰子
if(dice == 1){
money += 100;
System.out.println("所持有的金钱增加了..");
}else if(dice == 2){
money /= 2;
System.out.println("所持有的金钱减半..");
}else if(dice == 6){ //获取水果
String fruit = getFruit();
System.out.println("获得了水果: " + fruit);
fruits.add(fruit);
}else{
//骰子结果为3、4、5
System.out.println("无效数字,继续投掷");
}
}
//拍摄快照
public Memento createMemento(){
Memento memento = new Memento(money);
for (String fruit : fruits) {
if(fruit.startsWith("好吃的")){
memento.addFruit(fruit);
}
}
return memento;
}
//撤销方法
public void restore(Memento memento){
this.money = memento.money;
this.fruits = memento.getFruits();
}
@Override
public String toString() {
return "Player{" +
"money=" + money +
", fruits=" + fruits +
'}';
}
}
测试: 由于引入了备忘录模式,可以保存某个时间点的玩家状态,这样就可以对玩家进行复活操作.
public class MainApp {
public static void main(String[] args) throws InterruptedException {
Player player = new Player(100); //最初所持的金钱数
Memento memento = player.createMemento(); //保存最初状态
for (int i = 0; i < 100; i++) {
//显示扔骰子的次数
System.out.println("=====" + i);
//显示当前状态
System.out.println("当前状态: " + player);
//开启游戏
player.yacht();
System.out.println("所持有的金钱为: " + player.getMoney() + " 元");
//决定如何操作Memento
if(player.getMoney() > memento.getMoney()){
System.out.println("赚到金币,保存当前状态,继续游戏!");
memento = player.createMemento();
}else if(player.getMoney() < memento.getMoney() / 2){
System.out.println("所持金币不多了,将游戏恢复到初始状态!");
player.restore(memento);
}
Thread.sleep(1000);
System.out.println("");
}
}
}
2.4备忘录模式总结
2.4.1备忘录模式的优点
1. 提供了一种状态恢复的实现机制,使得用户可以方便的回到一个特定的历史步骤,当新的状态无效或者存在问题的时候,可以使用暂时存储起来的备忘录将状态复原.
2. 备忘录实现了对信息的封装,一个备忘录对象是一种发起者对象状态的表示,不会被其他代码所改动.备忘录保存了发起者的状态,采用集合来存储备忘录可以实现多次撤销的操作
2.4.2备忘录模式的缺点
资源消耗过大,如果需要保存的发起者类的成员变量比较多, 就不可避免的需要占用大量的存储空间,每保存一次对象的状态,都需要消耗一定系统资源
2.4.3备忘录模式使用场景
1. 需要保存一个对象在某一时刻的状态时,可以使用备忘录模式.
2. 不希望外界直接访问对象内部状态时.
3.命令模式
命令模式(command pattern)的定义: 命令模式将请求(命令)封装为一个对象,这样可以使用不同的请求参数化其他对象(将不 同请求依赖注入到其他对象),并且能够支持请求(命令)的排队执行、记录日志、撤销等 (附加控制)功能。
命令模式的核心是将指令信息封装成一个对象,并将此对象作为参数发送给接收方去执行,达到使命令的请求与执行方解耦,双方只通过传递各种命令对象来完成任务.
在实际的开发中,如果你用到的编程语言并不支持用函数作为参数来传递,那么就可以借助命令模式将函数封装为对象来使用。
我们知道,C 语 言支持函数指针,我们可以把函数当作变量传递来传递去。但是,在大部分编程语言中,函 数没法儿作为参数传递给其他函数,也没法儿赋值给变量。借助命令模式,我们可以将函数 封装成对象。具体来说就是,设计一个包含这个函数的类,实例化一个对象传来传去,这样 就可以实现把函数像对象一样使用。
3.1命令模式原理
命令模式包含以下主要角色:
抽象命令类(Command)角色:
定义命令的接口,声明执行的方法。
具体命令(Concrete Command)角色:具体的命令,实现命令接口;通常会持有接收者,并调用接收者的功能来完成命令要执行的操作。
实现者/接收者(Receiver)角色:接收者,真正执行命令的对象。任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。
调用者/请求者(Invoker)角色:要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。这个是客户端真正触发命令并要求命令执行相应操作的地方,也就是说相当于使用命令对象的入口。
3.2命令模式实现
模拟酒店后厨的出餐流程,来对命令模式进行一个演示,命令模式角色的角色与案例中角色的对应关系如下:
服务员: 即调用者角色,由她来发起命令.
厨师: 接收者,真正执行命令的对象.
订单: 命令中包含订单
/**
* 订单类
* @author spikeCong
* @date 2022/10/19
**/
public class Order {
private int diningTable; //餐桌号码
//存储菜名与份数
private Map<String,Integer> foodMenu = new HashMap<>();
public int getDiningTable() {
return diningTable;
}
public void setDiningTable(int diningTable) {
this.diningTable = diningTable;
}
public Map<String, Integer> getFoodMenu() {
return foodMenu;
}
public void setFoodDic(Map<String, Integer> foodMenu) {
this.foodMenu = foodMenu;
}
}
/**
* 厨师类 -> Receiver角色
* @author spikeCong
* @date 2022/10/19
**/
public class Chef {
public void makeFood(int num,String foodName){
System.out.println(num + "份," + foodName);
}
}
/**
* 抽象命令接口
* @author spikeCong
* @date 2022/10/19
**/
public interface Command {
void execute(); //只需要定义一个统一的执行方法
}
/**
* 具体命令
* @author spikeCong
* @date 2022/10/19
**/
public class OrderCommand implements Command {
//持有接收者对象
private Chef receiver;
private Order order;
public OrderCommand(Chef receiver, Order order) {
this.receiver = receiver;
this.order = order;
}
@Override
public void execute() {
System.out.println(order.getDiningTable() + "桌的订单: ");
Set<String> keys = order.getFoodMenu().keySet();
for (String key : keys) {
receiver.makeFood(order.getFoodMenu().get(key),key);
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(order.getDiningTable() + "桌的菜已上齐.");
}
}
/**
* 服务员-> Invoker调用者
* @author spikeCong
* @date 2022/10/19
**/
public class Waiter {
//可以持有很多的命令对象
private ArrayList<Command> commands;
public Waiter() {
commands = new ArrayList();
}
public Waiter(ArrayList<Command> commands) {
this.commands = commands;
}
public void setCommands(Command command) {
commands.add(command);
}
//发出命令 ,指挥厨师工作
public void orderUp(){
System.out.println("服务员: 叮咚,有新的订单,请厨师开始制作......");
for (Command cmd : commands) {
if(cmd != null){
cmd.execute();
}
}
}
}
public class Client {
public static void main(String[] args) {
Order order1 = new Order();
order1.setDiningTable(1);
order1.getFoodMenu().put("鲍鱼炒饭",1);
order1.getFoodMenu().put("茅台迎宾",1);
Order order2 = new Order();
order2.setDiningTable(3);
order2.getFoodMenu().put("海参炒面",1);
order2.getFoodMenu().put("五粮液",1);
//创建接收者
Chef receiver = new Chef();
//将订单和接收者封装成命令对象
OrderCommand cmd1 = new OrderCommand(receiver,order1);
OrderCommand cmd2 = new OrderCommand(receiver,order2);
//创建调用者
Waiter invoke = new Waiter();
invoke.setCommands(cmd1);
invoke.setCommands(cmd2);
//将订单发送到后厨
invoke.orderUp();
}
}
3.3命令模式总结
3.3.1命令模式优点:
降低系统的耦合度。命令模式能将调用操作的对象与实现该操作的对象解耦。
增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,它满足“开闭原则”,对扩展比较灵活。
可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
3.3.2命令模式缺点:
使用命令模式可能会导致某些系统有过多的具体命令类。
系统结构更加复杂。
3.3.3使用场景
系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
系统需要在不同的时间指定请求、将请求排队和执行请求。
系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
4.解释器模式
解释器模式使用频率不算高,通常用来描述如何构建一个简单“语言”的语法解释器。它只在一些非常特定的领域被用到,比如编译器、规则引擎、正则表达式、SQL 解析等。不过,了解它的实现原理同样很重要,能帮助你思考如何通过更简洁的规则来表示复杂的逻辑。
解释器模式(Interpreter pattern)的原始定义是:用于定义语言的语法规则表示,并提供解释器来处理句子中的语法。
我们通过一个例子给大家解释一下解释器模式
假设我们设计一个软件用来进行加减计算。我们第一想法就是使用工具类,提供对应的加法和减法的工具方法。
//用于两个整数相加的方法
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;
}
public static int add(Integer ... arr){
int sum = 0;
for(Integer num : arr){
sum += num;
}
return sum;
}
+ -
上面的形式比较单一、有限,如果形式变化非常多,这就不符合要求,因为加法和减法运算,两个运算符与数值可以有无限种组合方式。比如: 5-3+2-1, 10-5+20....
文法规则和抽象语法树
解释器模式描述了如何为简单的语言定义一个文法,如何在该语言中表示一个句子,以及如何解释这些句子.
在上面提到的加法/减法解释器中,每一个输入表达式(比如:2+3+4-5) 都包含了3个语言单位,可以使用下面的文法规则定义:
文法是用于描述语言的语法结构的形式规则。
expression ::= value | plus | minus
plus ::= expression ‘+’ expression
minus ::= expression ‘-’ expression
value ::= integer
注意: 这里的符号“::=”表示“定义为”的意思,竖线 | 表示或,左右的其中一个,引号内为字符本身,引号外为语法。
上面规则描述为 :
表达式可以是一个值,也可以是plus或者minus运算,而plus和minus又是由表达式结合运算符构成,值的类型为整型数。
抽象语法树:
在解释器模式中还可以通过一种称为抽象语法树的图形方式来直观的表示语言的构成,每一棵抽象语法树对应一个语言实例,例如加法/减法表达式语言中的语句 " 1+ 2 + 3 - 4 + 1" 可以通过下面的抽象语法树表示
4.1解释器模式原理
4.1.1解释器模式包含以下主要角色。
抽象表达式(Abstract Expression)角色:
定义解释器的接口,约定解释器的解释操作,主要包含解释方法 interpret()。
终结符表达式(Terminal Expression)角色:是抽象表达式的子类,用来实现文法中与终结符相关的操作,文法中的每一个终结符都有一个具体终结表达式与之相对应。上例中的value 是终结符表达式.
非终结符表达式(Nonterminal Expression)角色:也是抽象表达式的子类,用来实现文法中与非终结符相关的操作,文法中的每条规则都对应于一个非终结符表达式。上例中的 plus , minus 都是非终结符表达式
环境(Context)角色:通常包含各个解释器需要的数据或是公共的功能,一般用来传递被所有解释器共享的数据,后面的解释器可以从这里获取这些值。
客户端(Client):主要任务是将需要分析的句子或表达式转换成使用解释器对象描述的抽象语法树,然后调用解释器的解释方法,当然也可以通过环境角色间接访问解释器的解释方法。
4.2解释器模式实现
为了更好的给大家解释一下解释器模式, 我们来定义了一个进行加减乘除计算的“语言”,语法规则如下:
运算符只包含加、减、乘、除,并且没有优先级的概念;
表达式中,先书写数字,后书写运算符,空格隔开;
我们举个例子来解释一下上面的语法规则:
比如 `“ 9 5 7 3 - + * ”`这样一个表达式,我们按照上面的语法规则来处理,取出数字 `“9、5”` 和 `“-”` 运算符,计算得到 4,于是表达式就变成了 `“ 4 7 3 + * ”`。然后,我们再取出 `“4 7”`和“ + ”运算符,计算得到 11,表达式就变成了“ 11 3 * ”。最后,我们取出“ 11 3”和“ * ”运算符,最终得到的结果就是 33。
代码示例:
用户按照上 面的规则书写表达式,传递给 interpret() 函数,就可以得到最终的计算结果。
/**
* 表达式解释器类
* @author spikeCong
* @date 2022/10/20
**/
public class ExpressionInterpreter {
//Deque双向队列,可以从队列的两端增加或者删除元素
private Deque<Long> numbers = new LinkedList<>();
//接收表达式进行解析
public long interpret(String expression){
String[] elements = expression.split(" ");
int length = elements.length;
//获取表达式中的数字
for (int i = 0; i < (length+1)/2; ++i) {
//在 Deque的尾部添加元素
numbers.addLast(Long.parseLong(elements[i]));
}
//获取表达式中的符号
for (int i = (length+1)/2; i < length; ++i) {
String operator = elements[i];
//符号必须是 + - * / 否则抛出异常
boolean isValid = "+".equals(operator) || "-".equals(operator)
|| "*".equals(operator) || "/".equals(operator);
if (!isValid) {
throw new RuntimeException("Expression is invalid: " + expression);
}
//pollFirst()方法, 移除Deque中的第一个元素,并返回被移除的值
long number1 = numbers.pollFirst(); //数字
long number2 = numbers.pollFirst();
long result = 0; //运算结果
//对number1和number2进行运算
if (operator.equals("+")) {
result = number1 + number2;
} else if (operator.equals("-")) {
result = number1 - number2;
} else if (operator.equals("*")) {
result = number1 * number2;
} else if (operator.equals("/")) {
result = number1 / number2;
}
//将运算结果添加到集合头部
numbers.addFirst(result);
}
//运算完成numbers中应该保存着运算结果,否则是无效表达式
if (numbers.size() != 1) {
throw new RuntimeException("Expression is invalid: " + expression);
}
//移除Deque的第一个元素,并返回
return numbers.pop();
}
}
4.3代码重构
代码的所有的解析逻辑都耦合在一个函数中,这样显然是不合适的。这 个时候,我们就要考虑拆分代码,将解析逻辑拆分到独立的小类中, 前面定义的语法规则有两类表达式,一类是数字,一类是运算符,运算符又包括加减乘除。 利用解释器模式,我们把解析的工作拆分到以下五个类:plu,sub,mul,div
NumExpression
PluExpression
SubExpression
MulExpression
DivExpression
/**
* 表达式接口
* @author spikeCong
* @date 2022/10/20
**/
public interface Expression {
long interpret();
}
/**
* 数字表达式
* @author spikeCong
* @date 2022/10/20
**/
public class NumExpression implements Expression {
private long number;
public NumExpression(long number) {
this.number = number;
}
public NumExpression(String number) {
this.number = Long.parseLong(number);
}
@Override
public long interpret() {
return this.number;
}
}
/**
* 加法运算
* @author spikeCong
* @date 2022/10/20
**/
public class PluExpression implements Expression{
private Expression exp1;
private Expression exp2;
public PluExpression(Expression exp1, Expression exp2) {
this.exp1 = exp1;
this.exp2 = exp2;
}
@Override
public long interpret() {
return exp1.interpret() + exp2.interpret();
}
}
/**
* 减法运算
* @author spikeCong
* @date 2022/10/20
**/
public class SubExpression implements Expression {
private Expression exp1;
private Expression exp2;
public SubExpression(Expression exp1, Expression exp2) {
this.exp1 = exp1;
this.exp2 = exp2;
}
@Override
public long interpret() {
return exp1.interpret() - exp2.interpret();
}
}
/**
* 乘法运算
* @author spikeCong
* @date 2022/10/20
**/
public class MulExpression implements Expression {
private Expression exp1;
private Expression exp2;
public MulExpression(Expression exp1, Expression exp2) {
this.exp1 = exp1;
this.exp2 = exp2;
}
@Override
public long interpret() {
return exp1.interpret() * exp2.interpret();
}
}
/**
* 除法
* @author spikeCong
* @date 2022/10/20
**/
public class DivExpression implements Expression {
private Expression exp1;
private Expression exp2;
public DivExpression(Expression exp1, Expression exp2) {
this.exp1 = exp1;
this.exp2 = exp2;
}
@Override
public long interpret() {
return exp1.interpret() / exp2.interpret();
}
}
//测试
public class Test01 {
public static void main(String[] args) {
ExpressionInterpreter e = new ExpressionInterpreter();
long result = e.interpret("6 2 3 2 4 / - + *");
System.out.println(result);
}
}
4.4解释器模式总结
4.4.1解释器优点
易于改变和扩展文法
因为在解释器模式中使用类来表示语言的文法规则的,因此就可以通过继承等机制改变或者扩展文法.每一个文法规则都可以表示为一个类,因此我们可以快速的实现一个迷你的语言
实现文法比较容易
在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂
增加新的解释表达式比较方便
如果用户需要增加新的解释表达式,只需要对应增加一个新的表达式类就可以了.原有的表达式类不需要修改,符合开闭原则
4.4.2解释器缺点
对于复杂文法难以维护
在解释器中一条规则至少要定义一个类,因此一个语言中如果有太多的文法规则,就会使类的个数急剧增加,当值系统的维护难以管理.
执行效率低
在解释器模式中大量的使用了循环和递归调用,所有复杂的句子执行起来,整个过程也是非常的繁琐
4.4.3使用场景
当语言的文法比较简单,并且执行效率不是关键问题.
当问题重复出现,且可以用一种简单的语言来进行表达
当一个语言需要解释执行,并且语言中的句子可以表示为一个抽象的语法树的时候
5.中介者模式
提到中介模式,有一个比较经典的例子就是航空管制。 为了让飞机在飞行的时候互不干扰,每架飞机都需要知道其他飞机每时每刻的位置,这就需要时刻跟其他飞机通信。飞机通信形成的通信网络就会无比复杂。这个时候,我们通过引 入“塔台”这样一个中介,让每架飞机只跟塔台来通信,发送自己的位置给塔台,由塔台来 负责每架飞机的航线调度。这样就大大简化了通信网络。
中介模式(mediator pattern)的定义:
定义一个单独的(中介)对象,来封装一组对象之间的交互,将这组对象之间的交互委派给予中介对象交互,来避免对象之间的交互.
中介者对象就是用于处理对象与对象之间的直接交互,封装了多个对象之间的交互细节。中介模式的设计跟中间层很像,通过引入中介这个中间层,将一组对象之间的交互关系从多对多的网状关系转换为一对多的星状关系.原来一个对象要跟N个对象交互,现在只需要跟一个中介对象交互,从而最小化对象之间的交互关系,降低代码的复杂度,提高代码的可读性和可维护性.
5.1中介者模式原理
5.1.1中介者模式包含以下主要角色:
抽象中介者(Mediator)角色:
它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。
具体中介者(ConcreteMediator)角色:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。
具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。
5.2中介者模式实现
/**
* 抽象中介者
* @author spikeCong
* @date 2022/10/20
**/
public interface Mediator {
void apply(String key);
}
/**
* 具体中介者
* @author spikeCong
* @date 2022/10/20
**/
public class MediatorImpl implements Mediator {
@Override
public void apply(String key) {
System.out.println("最终中介者执行操作,key为: " + key);
}
}
/**
* 抽象同事类
* @author spikeCong
* @date 2022/10/20
**/
public abstract class Colleague {
private Mediator mediator;
public Colleague(Mediator mediator) {
this.mediator = mediator;
}
public Mediator getMediator() {
return mediator;
}
public abstract void exec(String key);
}
/**
* 具体同事类
* @author spikeCong
* @date 2022/10/20
**/
public class ConcreteColleagueA extends Colleague {
public ConcreteColleagueA(Mediator mediator) {
super(mediator);
}
@Override
public void exec(String key) {
System.out.println("====在组件A中,通过中介者执行!");
getMediator().apply(key);
}
}
public class ConcreteColleagueB extends Colleague {
public ConcreteColleagueB(Mediator mediator) {
super(mediator);
}
@Override
public void exec(String key) {
System.out.println("====在组件B中,通过中介者执行!");
getMediator().apply(key);
}
}
public class Client {
public static void main(String[] args) {
//创建中介者
MediatorImpl mediator = new MediatorImpl();
//创建同事对象
Colleague c1 = new ConcreteColleagueA(mediator);
c1.exec("key-A");
Colleague c2 = new ConcreteColleagueB(mediator);
c2.exec("key-B");
}
}
====在组件A中,通过中介者执行!
最终中介者执行操作,key为: key-A
====在组件B中,通过中介者执行!
最终中介者执行操作,key为: key-B
5.3中介者模式应用实例
【例】租房
现在租房基本都是通过房屋中介,房主将房屋托管给房屋中介,而租房者从房屋中介获取房屋信息。房屋中介充当租房者与房屋所有者之间的中介者。
/**
* 抽象中介者
* @author spikeCong
* @date 2022/10/20
**/
public abstract class Mediator {
//申明一个联络方法
public abstract void contact(String message,Person person);
}
/**
* 抽象同事类
* @author spikeCong
* @date 2022/10/20
**/
public abstract class Person {
protected String name;
protected Mediator mediator;
public Person(String name, Mediator mediator) {
this.name = name;
this.mediator = mediator;
}
}
/**
* 中介机构
* @author spikeCong
* @date 2022/10/20
**/
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 contact(String message, Person person) {
if(person == houseOwner){ //如果是房主,则租房者获得信息
tenant.getMessage(message);
}else { //如果是租房者则获取房主信息
houseOwner.getMessage(message);
}
}
}
/**
* 具体同事类-房屋拥有者
* @author spikeCong
* @date 2022/10/20
**/
public class HouseOwner extends Person{
public HouseOwner(String name, Mediator mediator) {
super(name, mediator);
}
//与中介者联系
public void contact(String message){
mediator.contact(message,this);
}
//获取信息
public void getMessage(String message){
System.out.println("房主" + name + "获取到的信息: " + message);
}
}
/**
* 具体同事类-承租人
* @author spikeCong
* @date 2022/10/20
**/
public class Tenant extends Person{
public Tenant(String name, Mediator mediator) {
super(name, mediator);
}
//与中介者联系
public void contact(String message){
mediator.contact(message,this);
}
//获取信息
public void getMessage(String message){
System.out.println("租房者" + name + "获取到的信息: " + message);
}
}
public class Client {
public static void main(String[] args) {
//一个房主 一个租房者 一个中介机构
MediatorStructure mediator = new MediatorStructure();
//房主和租房者只需要知道中介机构即可
HouseOwner houseOwner = new HouseOwner("路飞", mediator);
Tenant tenant = new Tenant("娜美", mediator);
//中介收集房租和租房者信息
mediator.setHouseOwner(houseOwner);
mediator.setTenant(tenant);
tenant.contact("需要一个两室一厅的房子,一家人住");
houseOwner.contact("出租一套两室一厅带电梯,月租5000");
}
}
5.4中介者模式总结
5.4.1中介者模式的优点
中介者模式简化了对象之间的交互,他用中介者和同事的一对多代替了原来的同事之间的多对多的交互,一对多关系更好理解 易于维护和扩展,将原本难以理解的网状结构转换成习相对简单的星型结构.
可以将各个同事就对象进行解耦.中介者有利于各个同事之间的松耦合,可以独立的改变或者复用每一个同事或者中介者,增加新的中介者类和新的同事类都比较方便,更符合开闭原则
可以减少子类生成,中介者将原本分布与多个对象的行为集中在了一起,改变这些行为只需要生成新的中介者的子类即可,使得同事类可以被重用,无需直接对同事类进行扩展.
5.4.2中介者模式的缺点
在具体中介者类中包含了大量同事之间的交互细节,可能会导致中介者类变得非常的复杂,使得系统不好维护.
5.4.3中介者模式使用场景
系统中对象之间存在复杂的引用关系,系统结构混乱且难以理解.
一个对象由于引用了其他的很多对象并且直接和这些对象进行通信,导致难以复用该对象.
想要通过一个中间类来封装多个类中的行为,而又不想生成太多的子类,此时可以通过引用中介者类来实现,在中介者类中定义对象的交互的公共行为,如果需要改变行为则可以在增加新的中介类.