行为型设计模式(11种)
概述
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。
行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。
分类
行为型(11种): 负责对象间的高效沟通和职责传递委派
- 常用: 观察者模式、模板模式、策略模式、责任链模式、迭代器模式、状态模式。
- 不常用: 访问者模式、备忘录模式、命令模式、解释器模式、中介模式。
1、观察者模式
1.1、定义
定义对象之间的一对多依赖关系,这样当一个对象改变状态时,它的所有依赖项都会自动得到通知和更新。
1.2、特点
观察者模式它是用于建立一种对象与对象之间的依赖关系,一个对象发生改变时将自动通知其他对象,其他对象将相应的作出反应。
在观察者模式中发生改变的对象称为观察目标,而被通知的对象称为观察者,一个观察目标可以应对多个观察者,而且这些观察者之间可以没有任何相互联系,可以根据需要增加和删除观察者,使得系统更易于扩展。
观察者模式的别名有发布-订阅(Publish/Subscribe)模式,模型-视图(Model-View)模式、源-监听(Source-Listener) 模式等
1.3、主要角色
- Subject:抽象主题(抽象被观察者),抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象。
- ConcreteSubject:具体主题(具体被观察者),该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。
- Observer:抽象观察者,是观察者的抽象类,它定义了一个更新接口,使得在得到主题更改通知时更新自己。
- ConcrereObserver:具体观察者,实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态。在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要与具体目标保持一致。
1.4、原理
1.5、实现
Observer:抽象观察者
public interface Observer {
void update();
}
ConcreteObserver1(ConcrereObserver):具体观察者1
public class ConcreteObserver1 implements Observer{
@Override
public void update() {
System.out.println("观察者ConcreteObserver1 得到通知,更新内容……");
}
}
ConcreteObserver2(ConcrereObserver):具体观察者2
public class ConcreteObserver2 implements Observer{
@Override
public void update() {
System.out.println("观察者ConcreteObserver2 得到通知,更新内容……");
}
}
Subject:抽象主题(抽象被观察者)
public interface Subject {
void attach(Observer obs);
void detach(Observer obs);
void notifyObservers();
}
ConcreteSubject:具体主题(具体被观察者)
public class ConcreteSubject implements Subject{
private List<Observer> observers = new ArrayList<>();
@Override
public void attach(Observer obs) {
observers.add(obs);
}
@Override
public void detach(Observer obs) {
observers.remove(obs);
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update();
}
}
}
1.6、测试代码
public class TestObserver {
@Test
public void testExample01() {
// 创建目标类
Subject subject = new ConcreteSubject();
// 注册观察者,注册多个
subject.attach(new ConcreteObserver1());
subject.attach(new ConcreteObserver2());
// 具体的主题内部发生改变,给所有注册的观察者发送通知
subject.notifyObservers();
}
}
1.7、测试效果
1.8、总结
1)、 观察者模式的优点:
- 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。
- 被观察者发送通知,所有注册的观察者都会收到信息【可以实现广播机制】
2)、 观察者模式的缺点:
- 如果观察者非常多的话,那么所有的观察者收到被观察者发送的通知会耗时
- 如果被观察者有循环依赖的话,那么被观察者发送通知会使观察者循环调用,会导致系统崩溃
3 ) 、观察者模式常见的应用场景
- 当一个对象状态的改变需要改变其他对象时。比如,商品库存数量发生变化时,需要通知商品详情页、购物车等系统改变数量。
- 一个对象发生改变时只想要发送通知,而不需要知道接收者是谁。比如,订阅微信公众号的文章,发送者通过公众号发送,订阅者并不知道哪些用户订阅了公众号。
- 需要创建一种链式触发机制时。比如,在系统中创建一个触发链,
A
对象的行为将影响B
对象,B
对象的行为将影响C
对象……这样通过观察者模式能够很好地实现。 - 微博或微信朋友圈发送的场景。这是观察者模式的典型应用场景,一个人发微博或朋友圈,只要是关联的朋友都会收到通知;一旦取消关注,此人以后将不会收到相关通知。
- 需要建立基于事件触发的场景。比如,基于
Java UI
的编程,所有键盘和鼠标事件都由它的侦听器对象和指定函数处理。当用户单击鼠标时,订阅鼠标单击事件的函数将被调用,并将所有上下文数据作为方法参数传递给它。
4 )、 JDK 中对观察者模式的支持
JDK
中提供了 Observable
类以及 Observer
接口,它们构成了 JDK
对观察者模式的支持。
-
java.util.Observer
接口:该接口中声明了一个方法,它充当抽象观察者,其中声明了一个update
方法。void update(Observable o, Object arg);
-
java.util.Observable
类:充当观察目标类(被观察类) ,在该类中定义了一个Vector
集合来存储观察者对象。下面是它最重要的 3 个方法。- void addObserver(Observer o) 方法:用于将新的观察者对象添加到集合中。
-
void notifyObservers(Object arg) 方法:调用集合中的所有观察者对象的
update
方法,通知它们数据发生改变。通常越晚加入集合的观察者越先得到通知。 -
void setChange() 方法:用来设置一个
boolean
类型的内部标志,注明目标对象发生了变化。当它为true
时,notifyObservers()
才会通知观察者。
用户可以直接使用 Observer
接口和 Observable
类作为观察者模式的抽象层,再自定义具体观察者类和具体观察目标类,使用 JDK
中提供的这两个类可以更加方便的实现观察者模式。
2、模板方法模式
2.1、定义
在操作中定义算法的框架,将一些步骤推迟到子类中。模板方法让子类在不改变算法结构的情况下重新定义算法的某些步骤。
2.2、特点
模板方法模式是一种基于继承的代码复用技术,它是一种类行为模式。模板方法模式其结构中只存在父类与子类之间的继承关系。
例如:我们去医院看病一般要经过以下4个流程:挂号、取号、排队、医生问诊等,其中挂号、 取号 、排队对每个病人是一样的,可以在父类中实现,但是具体医生如何根据病情开药每个人都是不一样的,所以开药这个操作可以延迟到子类中实现。
2.3、主要角色
模板方法(Template Method)模式包含以下主要角色:
- 抽象父类:定义一个算法所包含的所有步骤,并提供一些通用的方法逻辑。
- 具体子类:继承自抽象父类,根据需要重写父类提供的算法步骤中的某些步骤。
抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。
-
模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
-
基本方法:是实现算法各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三种:
-
抽象方法(Abstract Method) :一个抽象方法由抽象类声明、由其具体子类实现。
-
具体方法(Concrete Method) :一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。
-
钩子方法(Hook Method) :在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。
一般钩子方法是用于判断的逻辑方法,这类方法名一般为
isXxx
,返回值类型为boolean
类型。
-
2.4、原理
2.5、实现
AbstractClassTemplate (抽象父类模板):定义一个算法所包含的所有步骤,并提供一些通用的方法逻辑。
public abstract class AbstractClassTemplate {
public void step1(String key) {
System.out.println("模板----->执行步骤1");
if (step2(key)) {
step3();
} else {
step4();
}
step5();
}
private void step5() {
System.out.println("模板----->执行步骤5");
}
private boolean step2(String key) {
System.out.println("模板----->执行步骤2");
if ("x".equals(key)) {
return true;
} else {
return false;
}
}
public abstract void step3();
public abstract void step4();
public void run(String key){
step1(key);
}
}
ConcreteClassA (具体子类A):根据需要重写父类提供的算法步骤中的某些步骤。
public class ConcreteClassA extends AbstractClassTemplate{
@Override
public void step3() {
System.out.println("在子类A中------->执行步骤3");
}
@Override
public void step4() {
System.out.println("在子类A中------->执行步骤4");
}
}
ConcreteClassB (具体子类B):根据需要重写父类提供的算法步骤中的某些步骤。
public class ConcreteClassB extends AbstractClassTemplate{
@Override
public void step3() {
System.out.println("在子类B中------->执行步骤3");
}
@Override
public void step4() {
System.out.println("在子类B中------->执行步骤4");
}
}
2.6、测试代码
public class TestTemplate {
@Test
public void testExample01(){
AbstractClassTemplate classA = new ConcreteClassA();
classA.run("x");
System.out.println("====================================================");
AbstractClassTemplate classB = new ConcreteClassB();
classB.run("");
}
}
2.7、测试效果
2.8、总结
模板方法模式的优点:
-
在父类中形式化的定义一个算法,而由它的子类来实现细节处理,在子类实现详细的处理代码时,并不会改变父类算法中步骤的执行顺序。
-
模板方法可以实现一种反向的控制结构,通过子类覆盖父类的钩子方法,来决定某一个特定步骤是否需要执行。
-
在模板方法模式中可以通过子类来覆盖父类的基本方法,不同的子类可以提供基本方法的不同实现,更换和增加新的子类很方便,符合单一职责原则和开闭原则。
模板方法模式的缺点:
- 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象。
- 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。
模板方法模式的应用场景一般有:
- 多个类有相同的方法并且逻辑可以共用时;
- 将通用的算法或固定流程设计为模板,在每一个具体的子类中再继续优化算法步骤或流程步骤时;
- 重构超长代码时,发现某一个经常使用的公有方法。
3、策略模式
3.1、定义
定义一系列算法,将每一个算法封装起来,并使它们可以相互替换。
3.2、特点
策略模式让算法可以独立于使用它的客户端而变化。
例如,出行旅游可以选择多种策略,如坐飞机、坐火车、骑自行车或开私家车等。
例如,网购,你可以选择工商银行、农业银行、建设银行等等,但是它们提供的算法都是一致的,就是帮你付款。
3.3、主要角色
- 抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
- 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。
- 环境或上下文(Context)类:使用算法的角色,持有一个策略类的引用,最终给客户端调用。
3.4、原理
3.5、实现
Strategy (抽象策略):定义具体策略类主要的功能或特征。
public interface Strategy {
void algorithm();
}
ConcreteStrategyA (具体策略):实现抽象策略,提供具体的算法实现或行为。
public class ConcreteStrategyA implements Strategy{
@Override
public void algorithm() {
System.out.println("执行策略A");
}
}
ConcreteStrategyB (具体策略):实现抽象策略,提供具体的算法实现或行为。
public class ConcreteStrategyB implements Strategy{
@Override
public void algorithm() {
System.out.println("执行策略B");
}
}
Context(环境或上下文):使用算法的角色,持有一个策略类的引用,最终给客户端调用。(策略模式的本质是通过 Context作为控制单元,对不同的策略进行调度分配)。
public class Context {
// 维护一个抽象策略的引用
private Strategy strategy;
public Context(Strategy strategy){
this.strategy = strategy;
}
// 调用策略类中的算法
public void algorithm(){
this.strategy.algorithm();
}
}
3.6、测试代码
public class TestStrategy {
@Test
public void testExample01() {
Strategy strategy = new ConcreteStrategyA();
Context context = new Context(strategy);
context.algorithm();
}
}
3.7、测试效果
3.8、总结
1)、 策略模式优点:
-
策略类之间可以自由切换。
由于策略类都实现同一个接口,所以使它们之间可以自由切换。
-
易于扩展。
增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合“开闭原则“。
-
避免使用多重条件选择语句(
if else
),充分体现面向对象设计思想。
2) 、策略模式缺点:
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
- 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。
3)、 策略模式应用场景
-
一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中。
策略模式最大的作用在于分离使用算法的逻辑和算法自身实现的逻辑,这样就意味着当我们想要优化算法自身的实现逻辑时就变得非常便捷,一方面可以采用最新的算法实现逻辑,另一方面可以直接弃用旧算法而采用新算法。使用策略模式能够很方便地进行替换。
-
一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句。
在实际开发中,有许多算法可以实现某一功能,如查找、排序等,通过
if-else
等条件判断语句来进行选择非常方便。但是这就会带来一个问题:当在这个算法类中封装了大量查找算法时,该类的代码就会变得非常复杂,维护也会突然就变得非常困难。虽然策略模式看上去比较笨重,但实际上在每一次新增策略时都通过新增类来进行隔离,短期虽然不如直接写 if-else 来得效率高,但长期来看,维护单一的简单类耗费的时间其实远远低于维护一个超大的复杂类。 -
系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏与算法相关的数据结构。
如果我们不希望客户知道复杂的、与算法相关的数据结构,在具体策略类中封装算法与相关数据结构,可以提高算法的保密性与安全性。
设计原则和思想其实比设计模式更加的普适和重要,掌握了代码的设计原则和思想,我们自然而然的就可以使用到设计模式,还有可能自己创建出一种新的设计模式。
4、职责链模式
4.1、定义
避免将一个请求的发送者与接收者耦合在一起,让多个对象都有机会处理请求。将接收请求的对象连接成一条链,并且沿着这条链传递请求,直到有一个对象能够处理它为止。
4.2、特点
在职责链模式中,多个处理器(也就是刚刚定义中说的“接收对象”)依次处理同一个请求。一个请求先经过 A 处理器处理,然后再把请求传递给 B 处理器,B 处理器处理完后再传递给 C 处理器,以此类推,形成一个链条。链条上的每个处理器各自承担各自的处理职责,所以叫作职责链模式。
例如:大学生申请创业扶持,审批过程可能有不同级别的负责人逐级审批。
4.3、主要角色
- 抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接(链上的每个处理者都有一个成员变量来保存对于下一处理者的引用,比如上图中的successor) 。
- 具体处理者(Concrete Handler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。
4.4、原理
4.5、实现
创建职责链,"请求数据:ABCD
"中,经过 handleA
则去掉 A
—> 经过 handleB
则去掉 B
—> 经过 handleC
则去掉 C
。
Handler(Handler):抽象处理类。
public abstract class Handler {
// 后继处理着的引用
public Handler successor;
public void setSuccessor(Handler successor){
this.successor = successor;
}
public abstract void handle(RequestData requestData);
}
HandlerA(HandleA):具体处理类。
public class HandlerA extends Handler {
@Override
public void handle(RequestData requestData) {
System.out.println("HandlerA执行代码逻辑,处理:" + requestData.getData());
requestData.setData(requestData.getData().replace("A", ""));
// 判断是否继续向后调用处理
if (successor != null) {
successor.handle(requestData);
} else {
System.out.println("执行终止");
}
}
}
HandlerB(HandleB):具体处理类。
public class HandlerB extends Handler {
@Override
public void handle(RequestData requestData) {
System.out.println("HandlerB执行代码逻辑,处理:" + requestData.getData());
requestData.setData(requestData.getData().replace("B", ""));
// 判断是否继续向后调用处理
if (successor != null) {
successor.handle(requestData);
} else {
System.out.println("执行终止");
}
}
}
HandlerC(HandleC):具体处理类
public class HandlerC extends Handler {
@Override
public void handle(RequestData requestData) {
System.out.println("HandlerC执行代码逻辑,处理:" + requestData.getData());
requestData.setData(requestData.getData().replace("C", ""));
// 判断是否继续向后调用处理
if (successor != null) {
successor.handle(requestData);
} else {
System.out.println("执行终止");
}
}
}
RequestData:请求数据实体类。
public class RequestData {
private String data;
public RequestData(String data) {
this.data = data;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
}
4.6、测试代码
@Test
public void testExample01(){
Handler handlerA = new HandlerA();
Handler handlerB = new HandlerB();
Handler handlerC = new HandlerC();
// 创建处理链
handlerA.setSuccessor(handlerB);
handlerB.setSuccessor(handlerC);
RequestData requestData = new RequestData("请求数据:ABCD");
// 调用处理链头部方法
handlerA.handle(requestData);
}
4.7、测试效果
4.8、总结
1) 、职责链模式的优点:
-
降低了对象之间的耦合度
该模式降低了请求发送者和接收者的耦合度。
-
增强了系统的可扩展性
可以根据需要增加新的请求处理类,满足开闭原则。
-
增强了给对象指派职责的灵活性
当工作流程发生变化,可以动态地改变链内的成员或者修改它们的次序,也可动态地新增或者删除责任。
-
责任链简化了对象之间的连接
一个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。
-
责任分担
每个类只需要处理自己该处理的工作,不能处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。
2) 、职责链模式的缺点:
- 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。
- 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
- 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。
3)、 应用场景
责任链模式常见的使用场景有以下几种情况。
- 在运行时需要动态使用多个关联对象来处理同一次请求时。比如,请假流程、员工入职流程、编译打包发布上线流程等。
- 不想让使用者知道具体的处理逻辑时。比如,做权限校验的登录拦截器。
- 需要动态更换处理对象时。比如,工单处理系统、网关 API 过滤规则系统等。
- 职责链模式常被用在框架开发中,用来实现框架的过滤器、拦截器功能,让框架的使用者在不修改源码的情况下,添加新的过滤拦截功能。
5、状态模式
5.1、定义
允许一个对象在其内部状态改变时改变它的行为。 对象看起来似乎修改了它的类。
5.2、特点
状态模式就是用于解决系统中复杂对象的状态转换以及不同状态下行为的封装问题。状态模式将一个对象的状态从该对象中分离出来,封装到专门的状态类中(用类来表示状态),使得对象状态可以灵活变化。
5.3、主要角色
-
上下文信息类(Context):实际上就是存储当前状态的类,对外提供更新状态的操作。在该类中维护着一个抽象状态接口State实例,这个实例定义当前状态。
-
抽象状态类(State):可以是一个接口或抽象类,用于定义声明状态更新的操作方法有哪些,具体实现由子类完成。
-
具体状态类(StateA 等):实现抽象状态类定义的方法,根据具体的场景来指定对应状态改变后的代码实现逻辑。
5.4、原理
5.5、实现
例如,交通信号灯状态的变化。
交通信号灯一般包括了红、黄、绿3种颜色状态,不同状态之间的切换逻辑为:红灯只能切换为黄灯,黄灯可以切换为绿灯或红灯,绿灯只能切换为黄灯。
TrafficState(State 抽象状态类):交通灯状态
public interface TrafficState {
void switchToGreen(TrafficLight trafficLight); // 切换为绿灯
void switchToYellow(TrafficLight trafficLight);// 切换为黄灯
void switchToRed(TrafficLight trafficLight);// 切换为红灯
}
RedState(ConcreteStateA 具体状态类):红灯
public class RedState implements TrafficState{
@Override
public void switchToGreen(TrafficLight trafficLight) {
System.out.println("红灯不能切换为绿灯!");
}
@Override
public void switchToYellow(TrafficLight trafficLight) {
System.out.println("黄灯亮起……时长:60s!");
}
@Override
public void switchToRed(TrafficLight trafficLight) {
System.out.println("当前为红灯,无需切换!");
}
}
GreenState(ConcreteStateB 具体状态类):绿灯
public class GreenState implements TrafficState{
@Override
public void switchToGreen(TrafficLight trafficLight) {
System.out.println("当前为绿灯,无需切换!");
}
@Override
public void switchToYellow(TrafficLight trafficLight) {
System.out.println("黄灯亮起……时长:60s!");
}
@Override
public void switchToRed(TrafficLight trafficLight) {
System.out.println("绿灯不能切换为红灯!");
}
}
YellowState(ConcreteStateC 具体状态类):黄灯
public class YellowState implements TrafficState{
@Override
public void switchToGreen(TrafficLight trafficLight) {
System.out.println("绿灯亮起……时长:60s!");
}
@Override
public void switchToYellow(TrafficLight trafficLight) {
System.out.println("当前为黄灯,无需切换!");
}
@Override
public void switchToRed(TrafficLight trafficLight) {
System.out.println("红灯亮起……时长:60s!");
}
}
TrafficLight(Context 上下文信息类):交通信号灯
public class TrafficLight {
// 初始化状态
private TrafficState state = new RedState();
public void setState(TrafficState state) {
this.state = state;
}
// 切换为绿灯,通行状态
public void switchToGreen(){
this.state.switchToGreen(this);
}
// 切换为黄灯,警戒状态
public void switchToYellow(){
this.state.switchToYellow(this);
}
// 切换为红灯,警戒状态
public void switchToRed(){
this.state.switchToRed(this);
}
}
5.6、测试代码
public class TestState {
@Test
public void testExample03(){
TrafficLight trafficLight = new TrafficLight();
trafficLight.switchToRed();
trafficLight.switchToGreen();
trafficLight.switchToYellow();
}
}
5.7、测试效果
5.8、总结
1) 、状态模式的优点:
- 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
- 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
2) 、状态模式的缺点:
- 状态模式的使用必然会增加系统类和对象的个数。
- 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
- 状态模式对"开闭原则"的支持并不太好 (添加新的状态类需要修改那些负责状态转换的源代码)。
3) 、状态模式常见的应用场景:
-
对象根据自身状态的变化来进行不同行为的操作时。
比如:购物订单状态。
-
对象需要根据自身变量的当前值改变行为,不期望使用大量 if-else 语句时。
比如:商品库存状态。
-
对于某些确定的状态和行为,不想使用重复代码时。
比如:某一个会员当天的购物浏览记录。
6、迭代器模式
6.1、定义
迭代器模式(Iterator pattern)又叫游标(Cursor)模式,它的定义是:迭代器提供一种对容器对象中的各个元素进行访问的方法,而又不需要暴露该对象的内部细节。
6.2、特点
将遍历数据的行为从容器中抽取出来,封装到迭代器对象中,由迭代器来提供遍历数据的行为,这将简化聚合对象的设计,更加符合单一职责原则。
6.3、主要角色
- 抽象集合(Aggregate)角色:用于存储和管理元素对象,定义存储、添加、删除集合元素的功能,并且声明了一个createIterator()方法用于创建迭代器对象。
- 具体集合(ConcreteAggregate)角色:实现抽象集合类,返回一个具体迭代器的实例。
- 抽象迭代器(Iterator)角色:定义访问和遍历聚合元素的接口,通常包含 hasNext()、next() 等方法。
- hasNext()函数用于判断集合中是否还有下一个元素
- next() 函数用于将游标后移一位元素
- currentItem() 函数,用来返回当前游标指向的元素
- 具体迭代器(Concretelterator)角色:实现抽象迭代器接口中所定义的方法,完成对集合对象的遍历,同时记录遍历的当前位置。
6.4、原理
6.5、实现
Aggregate(Aggregate 抽象集合):用于存储和管理元素对象,定义存储、添加、删除集合元素的功能,并且声明了一个iterator()方法用于创建迭代器对象。
public interface Aggregate<E> {
Iterator<E> iterator();
}
ConcreteAggregate:具体集合,实现抽象集合类,返回一个具体迭代器的实例。
public class ConcreteAggregate<E> implements Aggregate<E>{
private List<E> aggregateList;
public ConcreteAggregate(List<E> aggregateList) {
this.aggregateList = aggregateList;
}
@Override
public Iterator<E> iterator() {
return new ConcreteIterator<E>(this.aggregateList);
}
}
Iterator(Iterator 抽象迭代器):定义访问和遍历聚合元素的接口,通常包含 hasNext()、next() 等方法。
public interface Iterator<E> {
// 重置游标
void reset();
// 判断集合中是否有下一个元素
boolean hasNext();
// 游标后移
E next();
// 返回当前游标指定的元素
E currentItem();
}
ConcreteIterator(ConcreteIterator 具体迭代器):实现抽象迭代器接口中所定义的方法,完成对集合对象的遍历,同时记录遍历的当前位置。
public class ConcreteIterator<E> implements Iterator<E>{
private int cursor; // 游标
private List<E> arrayList; // 容器
public ConcreteIterator(List<E> arrayList) {
this.cursor = 0;
this.arrayList = arrayList;
}
@Override
public void reset() {
this.cursor = 0;
}
@Override
public boolean hasNext() {
return this.cursor != this.arrayList.size();
}
@Override
public E next() {
return arrayList.get(this.cursor++);
}
@Override
public E currentItem() {
if(this.cursor >= this.arrayList.size()){
throw new NoSuchElementException();
}
return arrayList.get(this.cursor);
}
}
6.6、测试代码
public class TestIterator {
@Test
public void testExample03(){
List<String> names = new ArrayList<>();
names.add("name1");
names.add("name2");
names.add("name3");
names.add("name4");
ConcreteAggregate<String> aggregate = new ConcreteAggregate<>(names);
Iterator<String> iterator = aggregate.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
6.7、测试效果
6.8、总结
1)、 迭代器的优点:
- 迭代器模式支持以不同方式遍历一个集合对象,在同一个集合对象上可以定义多种遍历方式。在迭代器模式中只需要用一个不同的迭代器来替换原有的迭代器,即可改变遍历算法,也可以自己定义迭代器的子类以支持新的遍历方式.
- 迭代器简化了集合类。由于引入了迭代器,在原有的集合对象中不需要再自行提供数据遍历等方法,这样可以简化集合类的设计。
- 在迭代器模式中,由于引入了抽象层,增加新的集合类和迭代器类都很方便,无须修改原有代码,满足 “基于接口编程而非实现” 和 “开闭原则” 的要求。
2) 、迭代器的缺点:
- 由于迭代器模式将存储数据和遍历数据的职责分离,增加了类的个数,这在一定程度上增加了系统的复杂性。
- 抽象迭代器的设计难度较大,需要充分考虑到系统将来的扩展。
3) 、应用场景
-
减少程序中重复的遍历代码
对于放入一个集合容器中的多个对象来说,访问必然涉及遍历算法。如果我们不将遍历算法封装到容器里(比如,List、Set、Map 等),那么就需要使用容器的人自行去实现遍历算法,这样容易造成很多重复的循环和条件判断语句出现,不利于代码的复用和扩展,同时还会暴露不同容器的内部结构。而使用迭代器模式是将遍历算法作为容器对象自身的一种“属性方法”来使用,能够有效地避免写很多重复的代码,同时又不会暴露内部结构。
-
当需要为遍历不同的集合结构提供一个统一的接口时或者当访问一个集合对象的内容而无须暴露其内部细节的表示时。
迭代器模式把对不同集合类的访问逻辑抽象出来,这样在不用暴露集合内部结构的情况下,可以隐藏不同集合遍历需要使用的算法,同时还能够对外提供更为简便的访问算法接口。
7、访问者模式
7.1、定义
允许在运行时将一个或多个操作应用于一组对象,将操作与对象结构分离。
这个定义有两个关键点:
- 一个是:运行时使用一组对象的一个或多个操作,比如,对不同类型的文件(.pdf、.xml、.properties)进行扫描;
- 另一个是:分离对象的操作和对象本身的结构,比如,扫描多个文件夹下的多个文件,对于文件来说,扫描是额外的业务操作,如果在每个文件对象上都加一个扫描操作,太过于冗余,而扫描操作具有统一性,非常适合访问者模式。
7.2、特点
访问者模式主要解决的是数据与算法的耦合问题,尤其是在数据结构比较稳定,而算法多变的情况下。为了不污染数据本身,访问者会将多种算法独立归档,并在访问数据时根据数据类型自动切换到对应的算法,实现数据的自动响应机制,并确保算法的自由扩展。
7.3、主要角色
- 抽象访问者(Visitor)角色:可以是接口或者抽象类,定义了一系列操作方法,用来处理所有数据元素,通常为同名的访问方法,并以数据元素类作为入参来确定那个重载方法被调用。
- 具体访问者(ConcreteVisitor)角色:访问者接口的实现类,可以有多个实现,每个访问者都需要实现所有数据元素类型的访问重载方法。
- 抽象元素(Element)角色:被访问的数据元素接口,定义了一个接受访问者的方法(
accept
),其意义是指,每一个元素都要可以被访问者访问。 - 具体元素(ConcreteElement)角色: 具体数据元素实现类,提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法,其accept实现方法中调用访问者并将自己 “this” 传回。
- 对象结构(Object Structure)角色:包含所有可能被访问的数据对象的容器,可以提供数据对象的迭代功能,可以是任意类型的数据结构。
- 客户端 ( Client ) :使用容器并初始化其中各类数据元素,并选择合适的访问者处理容器中的所有数据对象。
7.4、原理
7.5、实现
我们以超市购物为例,假设超市中的三类商品:水果、糖果、酒水进行售卖。我们可以忽略每种商品的计价方法,因为最终结账时由收银员统一集中处理。
Acceptable(Element 抽象元素):收银员,统一处理计价
public interface Acceptable {
void accept(Visitor visitor);
}
Product:抽象商品父类,定义商品的主要特性或功能
@Data
@AllArgsConstructor
public abstract class Product{
private String name; // 商品名称
private LocalDate productDate; // 商品生产日期
private double price; // 商品价格
}
Candy(ConcreteElementA 具体元素类):糖果类
public class Candy extends Product implements Acceptable {
public Candy(String name, LocalDate productDate, double price) {
super(name, productDate, price);
}
@Override
public void accept(Visitor visitor) {
// 在accept方法中调用访问者,并将自己this传入。
visitor.visit(this);
}
}
Wine(ConcreteElementB具体元素类):酒水类
public class Wine extends Product implements Acceptable{
public Wine(String name, LocalDate productDate, double price) {
super(name, productDate, price);
}
@Override
public void accept(Visitor visitor) {
// 在accept方法中调用访问者,并将自己this传入。
visitor.visit(this);
}
}
Fruit(ConcreteElementC 具体元素类):水果类
public class Fruit extends Product implements Acceptable{
private double weight; // 重量
public Fruit(String name, LocalDate productDate, double price, double weight) {
super(name, productDate, price);
this.weight = weight;
}
@Override
public void accept(Visitor visitor) {
// 在accept方法中调用访问者,并将自己this传入。
visitor.visit(this);
}
public double getWeight() {
return weight;
}
public void setWeight(double weight) {
this.weight = weight;
}
}
Visitor(Visitor 访问者接口):访问者接口 ----> 根据入参的不同调用对应的重载方法
public interface Visitor {
void visit(Candy candy); // 糖果重载方法
void visit(Fruit fruit); // 水果重载方法
void visit(Wine wine); // 酒水重载方法
}
DiscountVisitor(ConcreteVisitorA具体访问者类):折扣计价
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());
// 糖果大于180天禁止售卖,否则糖果一律九折
long days = billDate.toEpochDay() - candy.getProductDate().toEpochDay();
if (days > 180) {
System.out.println("超过半年的糖果,请勿使用");
} else {
double realPrice = candy.getPrice() * 0.9;
System.out.println("糖果打折后的价格为:" + NumberFormat.getCurrencyInstance().format(realPrice));
}
}
@Override
public void visit(Fruit fruit) {
System.out.println("水果:" + fruit.getName());
// 水果大于7天禁止售卖
long days = billDate.toEpochDay() - fruit.getProductDate().toEpochDay();
double rate = 0;
if (days > 7) {
System.out.println("超过7天的糖果,请勿、食用");
} else if (days > 3) { // 超过3天,打5折
rate = 0.5;
} else {
rate = 1;
}
double realPrice = fruit.getPrice() * fruit.getWeight() * rate;
System.out.println("水果打折后的价格为:" + NumberFormat.getCurrencyInstance().format(realPrice));
}
@Override
public void visit(Wine wine) {
System.out.println("酒类:" + wine.getName() + ",无打折价格");
System.out.println("酒类原价售卖:" + NumberFormat.getCurrencyInstance().format(wine.getPrice()));
}
7.6、测试代码
public class TestVisitor {
@Test
public void testExample02(){
List<Acceptable> list = Arrays.asList(
new Candy("大白兔奶糖", LocalDate.of(2022, 11, 5), 10.0),
new Wine("红酒", LocalDate.of(2022, 11, 5), 6000.0),
new Fruit("葡萄", LocalDate.of(2022, 11, 8), 23.0, 1)
);
Visitor visitor = new DiscountVisitor(LocalDate.of(2022,12,16));
for (Acceptable product : list) {
product.accept(visitor);
System.out.println("=======================");
}
}
}
7.7、测试效果
7.8、总结
1)、 访问者模式优点:
-
扩展性好
在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
-
复用性好
通过访问者来定义整个对象结构通用的功能,从而提高复用程度。
-
分离无关行为
通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一。
2)、 访问者模式缺点:
-
对象结构变化很困难
在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
-
违反了依赖倒置原则
访问者模式依赖了具体类,而没有依赖抽象类。
3) 、应用场景
-
当对象的数据结构相对稳定,而操作却经常变化的时候。
比如:上面例子中路由器本身的内部构造(也就是数据结构)不会怎么变化,但是在不同操作系统下的操作可能会经常变化,比如,发送数据、接收数据等。
-
需要将数据结构与不常用的操作进行分离的时候。
比如:扫描文件内容这个动作通常不是文件常用的操作,但是对于文件夹和文件来说,和数据结构本身没有太大关系(树形结构的遍历操作),扫描是一个额外的动作,如果给每个文件都添加一个扫描操作会太过于重复,这时采用访问者模式是非常合适的,能够很好分离文件自身的遍历操作和外部的扫描操作。
-
需要在运行时动态决定使用哪些对象和方法的时候。
比如:对于监控系统来说,很多时候需要监控运行时的程序状态,但大多数时候又无法预知对象编译时的状态和参数,这时使用访问者模式就可以动态增加监控行为。
8、备忘录模式
8.1、定义
在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。
8.2、特点
备忘录模式提供了一种对象状态的撤销实现机制,当系统中某一个对象需要恢复到某一历史状态时可以使用备忘录模式进行设计。
很多软件都提供了撤销(Undo)操作,如 Word、记事本、Photoshop、IDEA等软件在编辑时按 Ctrl+Z 组合键时能撤销当前操作,使文档恢复到之前的状态;还有在 浏览器 中的后退键、数据库事务管理中的回滚操作、玩游戏时的中间结果存档功能、数据库与操作系统的备份操作、棋类游戏中的悔棋功能等都属于这类。
8.3、主要角色
- 发起人(Originator)角色:状态需要被记录的元对象类,记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
- 备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
- 看护人(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。
8.4、原理
8.5、实现
例如:设计一个收集水果和获取金钱数的掷骰子游戏。
游戏规则如下:
- 游戏玩家通过扔骰子来决定下一个状态
- 当点数为1,玩家金钱增加
- 当点数为2,玩家金钱减少
- 当点数为6,玩家会得到水果
- 当钱消耗到一定程度,就恢复到初始状态
Memento(Memento 备忘录角色):备份玩家的状态
public class Memento {
private int money; // 所持的金钱
private ArrayList fruits; // 玩家获取的水果
public Memento(int money){
this.money = money;
this.fruits = new ArrayList();
}
// 获取当前玩家的金币
public int getMoney(){
return money;
}
// 获取当前玩家的水果
public List getFruits(){
return (List) fruits.clone();
}
// 添加水果
public void addFruit(String fruit){
fruits.add(fruit);
}
}
Player(既是发起人(Originator)角色,又是看护人(Caretaker)角色):玩家类
public class Player {
private int money; // 金币
private List<String> fruits = new ArrayList<>(); // 玩家获得的水果
private static String[] fruitsName = { // 水果种类数据
"苹果", "葡萄", "香蕉", "橘子",
};
private Random random = new Random();
public Player(int money) {
this.money = money;
}
// 获取当前金币
public int getMoney() {
return money;
}
// 获取水果
public String getFruit() {
String prefix = "";
if (random.nextBoolean()) {
prefix = "好吃的";
}
// 从数组中拿一个水果
String fruitName = fruitsName[random.nextInt(fruitsName.length)];
return prefix + fruitName;
}
// 掷骰子
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 = " + fruit);
fruits.add(fruit);
} else {
// 其他结果
System.out.println("无效数字:" + dice + ",请继续投掷……");
}
}
// 记录某一时刻的状态
public Memento createMemento() {
Memento memento = new Memento(money);
for (String fruit : fruits) {
if (fruit.startsWith("好吃的")) { // 只保存好吃的水果
memento.addFruit(fruit);
}
}
return memento;
}
// 撤销的方法
public void restoreMemento(Memento memento) {
this.money = memento.getMoney();
this.fruits = memento.getFruits();
}
@Override
public String toString() {
return new StringJoiner(", ", Player.class.getSimpleName() + "[", "]")
.add("money=" + money)
.add("fruits=" + fruits)
.toString();
}
}
8.6、测试代码
public class TestMemento {
@Test
public void testExample02() 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() + "元");
// 复活操作(玩家金币大于备忘录的金币,证明赚到了,保存当前状态)
if(player.getMoney() > memento.getMoney()){
System.out.println("玩家金币大于备忘录的金币,保存当前状态!!!");
memento = player.createMemento(); // 更新状态
}else if(player.getMoney() < memento.getMoney()/2){
System.out.println("所持的金币不多了,将游戏恢复到初始状态");
player.restoreMemento(memento);
}
Thread.sleep(1000);
System.out.println("===================================================");
}
}
}
8.7、测试效果
8.8、总结
1 )、 备忘录模式的优点:
- 提供了一种状态恢复的实现机制,使得用户可以方便的回到一个特定的历史步骤,当新的状态无效或者存在问题的时候,可以使用暂时存储起来的备忘录将状态复原。
- 备忘录实现了对信息的封装,一个备忘录对象是一种发起者对象状态的表示,不会被其他代码所改动。备忘录保存了发起者的状态,采用集合来存储备忘录可以实现多次撤销的操作。
2 ) 、备忘录模式的缺点:
- 资源消耗过大,如果需要保存的发起者类的成员变量比较多,就不可避免的需要占用大量的存储空间,每保存一次对象的状态,都需要消耗一定系统资源。
3) 备忘录模式使用场景
- 需要保存一个对象在某一时刻的状态时,可以使用备忘录模式。
- 不希望外界直接访问对象内部状态时。
9、命令模式
9.1、定义
命令模式将请求(命令)封装为一个对象,这样可以使用不同的请求参数化其他对象(将不同请求依赖注入到其他对象),并且能够支持请求(命令)的排队执行、记录日志、撤销等 (附加控制)功能。
9.2、特点
命令模式的核心是将指令信息封装成一个对象,并将此对象作为参数发送给接收方去执行,达到使命令的请求与执行方解耦,双方只通过传递各种命令对象来完成任务。
在实际的开发中,如果你用到的编程语言并不支持用函数作为参数来传递,那么就可以借助命令模式将函数封装为对象来使用。
我们知道,C 语言支持函数指针,我们可以把函数当作变量传递来传递去。但是,在大部分编程语言中,函数没法儿作为参数传递给其他函数,也没法儿赋值给变量。借助命令模式,我们可以将函数 封装成对象。具体来说就是,设计一个包含这个函数的类,实例化一个对象传来传去,这样就可以实现把函数像对象一样使用。
9.3、主要角色
- 抽象命令类(Command)角色: 定义命令的接口,声明执行的方法。
- 具体命令(Concrete Command)角色:具体的命令,实现命令接口;通常会持有接收者,并调用接收者的功能来完成命令要执行的操作。
- 实现者/接收者(Receiver)角色: 接收者,真正执行命令的对象。任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。
- 调用者/请求者(Invoker)角色: 要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。这个是客户端真正触发命令并要求命令执行相应操作的地方,也就是说相当于使用命令对象的入口。
9.4、原理
9.5、实现
模拟酒店后厨的出餐流程。
命令模式角色的角色与案例中角色的对应关系如下:
- 服务员:即调用者角色,由她来发起命令。
- 厨师:接收者,真正执行命令的对象。
- 订单:命令中包含订单。
Command(Command 抽象命令接口): 定义命令的接口,声明执行的方法。
public interface Command {
void execute(); // 统一的执行方法
}
Waiter(invoker 调用者):服务员
public class Waiter {
// 可以持有多个对象
private ArrayList<Command> commands;
public Waiter() {
this.commands = new ArrayList<>();
}
public Waiter(ArrayList<Command> commands) {
this.commands = commands;
}
public void setCommand(Command commands) {
this.commands.add(commands);
}
// 发出指令
public void orderUp(){
System.out.println("服务员:有新的订单,请师傅开始制作");
for (Command command : commands) {
if(command != null){
command.execute();
}
}
}
}
Chef(Receiver 接受者角色):厨师类
public class Chef {
public void makeFood(int num, String foodName){
System.out.println(num + "份," + foodName);
}
}
Order:订单类
public class Order {
private Integer diningTable; // 餐桌号码
private Map<String, Integer> foodMenu = new HashMap<>(); // 存储菜名和份数
public Integer getDiningTable() {
return diningTable;
}
public void setDiningTable(Integer diningTable) {
this.diningTable = diningTable;
}
public Map<String, Integer> getFoodMenu() {
return foodMenu;
}
public void setFoodMenu(Map<String, Integer> foodMenu) {
this.foodMenu = foodMenu;
}
}
OrderCommand (Concrete Command 具体命令)
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> keySet = order.getFoodMenu().keySet();
for (String key : keySet) {
receiver.makeFood(order.getFoodMenu().get(key), key);
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(order.getDiningTable() + "桌的菜品已上齐!!!");
}
}
9.6、测试代码
public class TestCommand {
@Test
public void testExample01() {
Order order1 = new Order();
order1.setDiningTable(8);
order1.getFoodMenu().put("番茄炒蛋", 1);
order1.getFoodMenu().put("鱼香肉丝", 2);
order1.getFoodMenu().put("红烧肉", 1);
Order order2 = new Order();
order2.setDiningTable(6);
order2.getFoodMenu().put("麻婆豆腐", 1);
order2.getFoodMenu().put("宫爆鸡丁", 1);
// 创建接收者对象
Chef chef = new Chef();
// 将订单和接收者封装成命令对象
OrderCommand cmd1 = new OrderCommand(chef, order1);
OrderCommand cmd2 = new OrderCommand(chef, order2);
// 创建调用者
Waiter waiter = new Waiter();
waiter.setCommand(cmd1);
waiter.setCommand(cmd2);
// 订单给厨师
waiter.orderUp();
}
}
9.7、测试效果
9.8、总结
1) 、命令模式优点:
- 降低系统的耦合度。命令模式能将调用操作的对象与实现该操作的对象解耦。
- 增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,它满足“开闭原则”,对扩展比较灵活。
- 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
2)、 命令模式缺点:
- 使用命令模式可能会导致某些系统有过多的具体命令类。
- 系统结构更加复杂。
3)、 应用场景
- 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
- 系统需要在不同的时间指定请求、将请求排队和执行请求。
- 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
10、解释器模式
10.1、定义
用于定义语言的语法规则表示,并提供解释器来处理句子中的语法。
10.2、特点
解释器模式使用频率不算高,通常用来描述如何构建一个简单“语言”的语法解释器。
它只在一些非常特定的领域被用到,比如编译器、规则引擎、正则表达式、SQL 解析等。不过,了解它的实现原理同样很重要,能帮助你思考如何通过更简洁的规则来表示复杂的逻辑。
通过一个例子解释一下解释器模式
- 假设我们设计一个软件用来进行加减计算。我们第一想法就是使用工具类,提供对应的加法和减法的工具方法。
//用于两个整数相加的方法
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" 可以通过下面的抽象语法树表示。
10.3、主要角色
- 抽象表达式(Abstract Expression)角色:定义解释器的接口,约定解释器的解释操作,主要包含解释方法 interpret()。
- 终结符表达式(Terminal Expression)角色:是抽象表达式的子类,用来实现文法中与终结符相关的操作,文法中的每一个终结符都有一个具体终结表达式与之相对应。上例中的value 是终结符表达式。
- 非终结符表达式(Nonterminal Expression)角色:也是抽象表达式的子类,用来实现文法中与非终结符相关的操作,文法中的每条规则都对应于一个非终结符表达式。上例中的 plus、minus 都是非终结符表达式。
- 环境(Context)角色:通常包含各个解释器需要的数据或是公共的功能,一般用来传递被所有解释器共享的数据,后面的解释器可以从这里获取这些值。
- 客户端(Client):主要任务是将需要分析的句子或表达式转换成使用解释器对象描述的抽象语法树,然后调用解释器的解释方法,当然也可以通过环境角色间接访问解释器的解释方法。
10.4、原理
10.5、实现
我们来定义一个进行加减乘除计算的“语言”,语法规则如下:
- 运算符只包含加、减、乘、除,并且没有优先级的概念;
- 表达式中,先书写数字,后书写运算符,空格隔开;
举个例子来解释一下上面的语法规则:
- 比如
“ 9 5 7 3 - + * ”
这样一个表达式,我们按照上面的语法规则来处理,取出数字“9、5”
和“-”
运算符,计算得到 4,于是表达式就变成了“ 4 7 3 + * ”
。然后,我们再取出“4 7”
和“ + ”运算符,计算得到 11,表达式就变成了“ 11 3 * ”。最后,我们取出“ 11 3”和“ * ”运算符,最终得到的结果就是 33。
Expression(Abstract Expression 抽象表达式):定义解释器的接口,约定解释器的解释操作,主要包含解释方法 interpret()。
public interface Expression {
Long interpret();
}
NumberExpression:数字表达式
public class NumberExpression implements Expression{
private Long number;
public NumberExpression(String number) {
this.number = Long.parseLong(number);
}
@Override
public Long interpret() {
return this.number;
}
public Long getNumber() {
return number;
}
public void setNumber(Long number) {
this.number = number;
}
}
PlusExpression(Nonterminal Expression 非终结符表达式):加法运算
public class PlusExpression implements Expression {
private Expression exp1;
private Expression exp2;
public PlusExpression(Expression exp1, Expression exp2) {
this.exp1 = exp1;
this.exp2 = exp2;
}
@Override
public Long interpret() {
return exp1.interpret() + exp2.interpret();
}
}
MinusExpression(Nonterminal Expression 非终结符表达式):减法运算
public class MinusExpression implements Expression {
private Expression exp1;
private Expression exp2;
public MinusExpression(Expression exp1, Expression exp2) {
this.exp1 = exp1;
this.exp2 = exp2;
}
@Override
public Long interpret() {
return exp1.interpret() - exp2.interpret();
}
}
MutliExpression(Nonterminal Expression 非终结符表达式):乘法运算
public class MutliExpression implements Expression {
private Expression exp1;
private Expression exp2;
public MutliExpression(Expression exp1, Expression exp2) {
this.exp1 = exp1;
this.exp2 = exp2;
}
@Override
public Long interpret() {
return exp1.interpret() * exp2.interpret();
}
}
DivExpression(Nonterminal Expression 非终结符表达式):除法运算
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();
}
}
ExpressionInterpreter02:表达式解析
public class ExpressionInterpreter02 {
private Deque<Expression> 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) {
numbers.addLast(new NumberExpression(elements[i]));
}
for (int i = (length + 1) / 2; i < length; ++i) {
String operator = elements[i];
boolean isValid = "-".equalsIgnoreCase(operator) || "+".equalsIgnoreCase(operator) ||
"*".equalsIgnoreCase(operator) || "/".equalsIgnoreCase(operator);
if (!isValid) {
throw new RuntimeException("表达式不合法(数字个数要比操作符个数多一个):" + expression);
}
Expression exp1 = numbers.pollFirst();
Expression exp2 = numbers.pollFirst();
if (exp1 == null || exp2 == null) {
throw new RuntimeException("表达式不合法(数字个数要比操作符个数多一个):" + expression);
}
Expression result = null;
if ("-".equalsIgnoreCase(operator)) {
result = new MinusExpression(exp1,exp2);
} else if ("+".equalsIgnoreCase(operator)) {
result = new PlusExpression(exp1,exp2);
} else if ("*".equalsIgnoreCase(operator)) {
result = new MutliExpression(exp1,exp2);
} else if ("/".equalsIgnoreCase(operator)) {
result = new DivExpression(exp1,exp2);
}
Long num = result.interpret();
numbers.addFirst(new NumberExpression(num.toString()));
}
if (numbers.size() != 1) {
throw new RuntimeException("表达式不合法:" + expression);
}
return numbers.pop().interpret();
}
}
10.6、测试代码
public class TestInterpreter {
@Test
public void testExample02(){
ExpressionInterpreter02 interpreter02 = new ExpressionInterpreter02();
Long interpret = interpreter02.interpret("9 6 7 3 - + *");
System.out.println("表达式计算后的结果为:" + interpret);
}
}
10.7、测试结果
10.8、总结
1) 、解释器优点:
-
易于改变和扩展文法
因为在解释器模式中使用类来表示语言的文法规则的,因此就可以通过继承等机制改变或者扩展文法。每一个文法规则都可以表示为一个类,因此我们可以快速的实现一个迷你的语言。
-
实现文法比较容易
在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂。
-
增加新的解释表达式比较方便
如果用户需要增加新的解释表达式,只需要对应增加一个新的表达式类就可以了。原有的表达式类不需要修改,符合开闭原则。
2) 、解释器缺点:
-
对于复杂文法难以维护
在解释器中一条规则至少要定义一个类,因此一个语言中如果有太多的文法规则,就会使类的个数急剧增加,当值系统的维护难以管理。
-
执行效率低
在解释器模式中大量的使用了循环和递归调用,所有复杂的句子执行起来,整个过程也是非常的繁琐。
3)、 应用场景
- 当语言的文法比较简单,并且执行效率不是关键问题。
- 当问题重复出现,且可以用一种简单的语言来进行表达。
- 当一个语言需要解释执行,并且语言中的句子可以表示为一个抽象的语法树的时候。
11、中介者模式
11.1、定义
定义一个单独的(中介)对象,来封装一组对象之间的交互,将这组对象之间的交互委派给予中介对象交互,来避免对象之间的交互。
11.2、特点
中介者对象就是用于处理对象与对象之间的直接交互,封装了多个对象之间的交互细节。
中介模式的设计跟中间层很像,通过引入中介这个中间层,将一组对象之间的交互关系从多对多的网状关系转换为一对多的星状关系。原来一个对象要跟N个对象交互,现在只需要跟一个中介对象交互,从而最小化对象之间的交互关系,降低代码的复杂度,提高代码的可读性和可维护性。
11.3、主要角色
-
抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。
-
具体中介者(ConcreteMediator)角色:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
-
抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。
-
具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。
11.4、原理
11.5、实现
例如,租房
现在租房基本都是通过房屋中介,房主将房屋托管给房屋中介,而租房者从房屋中介获取房屋信息。房屋中介充当租房者与房屋所有者之间的中介者。
Person(Colleague 抽象同事类):抽象出主要特征或功能。
public abstract class Person {
public String name;
// 持有中介者的引用
public Mediator mediator;
public Person(String name, Mediator mediator) {
this.name = name;
this.mediator = mediator;
}
}
HouseOwner(ConcreteColleagueA 具体同事类):房屋拥有者
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);
}
}
Tenant(ConcreteColleagueB 具体同事类):租房者
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);
}
}
Mediator(Mediator 抽象中介者)
public abstract class Mediator {
// 创建联络方法
public abstract void contact(String message, Person person);
}
MediatorStructure(ConcreteMediator 具体中介者):中介机构
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);
}
}
}
11.6、测试代码
public class TestMediator02 {
@Test
public void testExample02(){
// 中介机构
MediatorStructure mediator = new MediatorStructure();
// 房主
HouseOwner owner = new HouseOwner("张三", mediator);
// 租房者
Tenant tenant = new Tenant("李四", mediator);
// 中介收集房主以及住房者信息
mediator.setHouseOwner(owner);
mediator.setTenant(tenant);
// 租房者需求
tenant.contact("需要在江苏省找一个两室一厅的房子,房租在 5000~6000 之间");
// 房主需求
owner.contact("出租一个在江苏省内的房子,房租4000");
}
}
11.7、测试效果
11.8、总结
1) 、中介者模式的优点:
- 中介者模式简化了对象之间的交互,他用中介者和同事的一对多代替了原来的同事之间的多对多的交互,一对多关系更好理解、易于维护和扩展,将原本难以理解的网状结构转换成习相对简单的星型结构。
- 可以将各个同事就对象进行解耦。中介者有利于各个同事之间的松耦合,可以独立的改变或者复用每一个同事或者中介者,增加新的中介者类和新的同事类都比较方便,更符合开闭原则。
- 可以减少子类生成,中介者将原本分布与多个对象的行为集中在了一起,改变这些行为只需要生成新的中介者的子类即可,使得同事类可以被重用,无需直接对同事类进行扩展。
2) 、中介者模式的缺点:
- 在具体中介者类中包含了大量同事之间的交互细节,可能会导致中介者类变得非常的复杂,使得系统不好维护。
3)、中介者模式应用场景
- 系统中对象之间存在复杂的引用关系,系统结构混乱且难以理解。
- 一个对象由于引用了其他的很多对象并且直接和这些对象进行通信,导致难以复用该对象。
- 想要通过一个中间类来封装多个类中的行为,而又不想生成太多的子类,此时可以通过引用中介者类来实现,在中介者类中定义对象的交互的公共行为,如果需要改变行为则可以在增加新的中介类。