1. 模板方法模式
1.1 介绍
定义:
定义一个操作中的算法框架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
结构:
模板方法模式包含以下主要角色:
抽象类(Abstract Class):负责给出一个算法的轮廓和骨架,它由一个模板方法和若干个基本方法构成;
模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法;
基本方法:是实现算法各个步骤的方法,是模板方法的组成部分,基本方法有可以分为三种:
- 抽象方法(Abstract Method):一个抽象方法由抽象类声明、由其具体子类实现;
- 具体方法(Contrete Method):一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承;
- 钩子方法(Hook Method):在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种;
一般钩子方法是用于判断的逻辑方法,这类方法名一般为isXxx,返回值类型为boolean类型。
具体子类(Conctete Class):实现抽象类中所定义的抽象方法和构造方法,它们是一个顶级逻辑的组成步骤;
类图:
优缺点:
优点:
- 提高代码复用性
将相同部分的代码放在抽象的父类中,而将不同的代码放入了不同的子类中。
- 实现了反向控制
通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制,并符合”开闭原则“。
缺点:
- 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象;
- 父类中的抽象方法由子类实现,子类执行的结果会导致父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度;
1.2 适用场景
算法的整体步骤很固定,但其中个别部分易变时,这时候就可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制JDK源码解析。
InputStream类就使用了模板方法模式,在InputStream类中定义了多个read()方法
public abstract int read() throws IOException;
public int read(byte[] var1) throws IOException {
return this.read(var1, 0, var1.length);
}
public int read(byte[] var1, int var2, int var3) throws IOException {
if (var1 == null) {
throw new NullPointerException();
} else if (var2 >= 0 && var3 >= 0 && var3 <= var1.length - var2) {
if (var3 == 0) {
return 0;
} else {
int var4 = this.read();//调用了无参的read(),使用了反向控制,调用的是实现类
if (var4 == -1) {
return -1;
} else {
var1[var2] = (byte)var4;
int var5 = 1;
try {
while(var5 < var3) {
var4 = this.read();
if (var4 == -1) {
break;
}
var1[var2 + var5] = (byte)var4;
++var5;
}
} catch (IOException var7) {
}
return var5;
}
}
} else {
throw new IndexOutOfBoundsException();
}
}
2. 策略模式
2.1 介绍
定义:
该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户,策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委托给不同的对象对这些算法进行管理。
结构:
策略模式的主要角色如下:
抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现,此角色给出所有的具体策略所需的接口;
具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为;
环境(Context)类:持有一个策略类的引用,最终给客户端调用;
类图:
优缺点:
优点:
- 策略类之间可以自由切换
由于策略类都实现同一个接口,所以使它们之间可以自由切换。
- 易于扩展
增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有代码,符合“开闭原则”;
避免使用多重条件选择语句(if else),充分体现面向对象设计思想;
缺点:
- 客户端必须指定所有的策略类,并自行决定使用哪一个策略类;
- 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量;
2.2 使用场景
- 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略中;
- 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句;
- 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时;
- 系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏域算法相关的数据结构;
- 多个类区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为;
2.3 JDK源码解析
Comparator中的策略模式。在Array类中有一个sort()方法
public static <T> void sort(T[] var0, Comparator<? super T> var1) {
if (var1 == null) {
sort(var0);
} else if (Arrays.LegacyMergeSort.userRequested) {
legacyMergeSort(var0, var1);
} else {
TimSort.sort(var0, 0, var0.length, var1, (Object[])null, 0, 0);
}
}
参数Comparator<? super T>用到了策略模式,即实现那种具体的策略
3. 命令模式
3.1 介绍
定义:
将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分隔开,这样两者之间通过命令对象进行沟通,这样方便将命令对象进行存储、传递、调用、增加与管理。
结构:
命令模式包含以下主要角色:
- 抽象命令类(Command)角色:定义命令的接口,声明执行的方法;
- 具体命令(Concrete Command)角色:具体的命令,实现命令接口;通常会持有接受者,并调用接受者的功能来完成命令要执行的操作;
- 实现者/接受者(Receiver)角色:接受者,真正执行命令的对象,任何类都可能成为一个接受者,只要它能够实现命令要求实现的相应功能;
- 调用者/请求者(Invoker)角色:要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象,这个是客户端真正触发命令并要求命令执行相应操作的地方,也就是说相当于使用命令对象的入口;
类图:
优缺点:
优点:
- 降低系统的耦合度,命令模式能将调用操作的对象与实现该操作的对象解耦;
- 增加或删除命令非常方便,采用命令模式增加与删除命令不会影响其他类,它满足“开闭原则”,对扩展比较灵活;
- 可以实现宏命令,命令模式可以与组合模式相结合,将多个命令装配成一个组合命令,及宏命令;
- 方便实现Undo和Redo操作,命令模式可以与后面介绍的备忘录结合,实现命令的撤销与恢复;
缺点:
- 使用命令模式可能会导致某些系统有过多的具体命令类;
- 系统结构更加复杂;
3.2 使用场景
- 系统需要将请求调用者和请求接收者解耦,使得调用者和接受者不直接交互;
- 系统需要在不同的时间指定请求、将请求排队和执行请求;
- 系统需要支持命令的撤销(Undo)和恢复(Redo)操作;
3.3 JDK源码解析
Runable是一个典型命令模式,Runnable担当命令的角色,Thread充当的是调用者,start方法就是其执行方法
/*命令接口(抽象命令角色)*/
public interface Runnable {
void run();
}
/*调用者*/
public class Thread implements Runnable {
private Runnable target;
public synchronized void start() {
if (this.threadStatus != 0) {
throw new IllegalThreadStateException();
} else {
this.group.add(this);
boolean var1 = false;
try {
this.start0();
var1 = true;
} finally {
try {
if (!var1) {
this.group.threadStartFailed(this);
}
} catch (Throwable var8) {
}
}
}
}
//调用系统方法,开启一个线程
private native void start0();
public void run() {
if (this.target != null) {
this.target.run();
}
}
}
4. 责任链模式
4.1 介绍
定义:
又名职责链模式,为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链,当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
结构:
职责链模式主要包含以下角色:
- 抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后续连接;
- 具体处理者(Contrete Handler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转发给他的后继者;
- 客户类(Client)角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程;
类图:
优缺点:
优点:
- 降低了对象之间的耦合度;
- 该模式降低了请求发送者和接受者的耦合度;
- 增强了系统的可扩展性;
- 可以根据需要增加新的请求处理类,满足开闭原则;
- 增强了给对象指派职责的灵活性;
- 当工作流程发送变化时,可以动态地改变链内的成员或者修改它们的次序,也可动态地新增或者删除责任;
- 责任链简化了对象之间的连接;
- 一个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的if或者if - else语句;
- 责任分组;
每个类只需要处理自己该处理的工作,不能处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。
缺点:
- 不能保证每个请求一定被处理,由于一个请求没有明确的接收者,所有不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理;
- 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响;
- 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用;
4.2 源码解析
在javaweb应用开发中,FilterChain是职责链(过滤器)模式的典型应用
Filter
public interface Filter {
public void doFilter(Request request, Response response, FilterChain chain);
}
Request
public interface Request {
}
Response
public interface Response {
}
FilterChain
public class FilterChain {
private List<Filter> filters = new ArrayList<>();
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);
}
}
FirstFilter
public class FirstFilter implements Filter {
public void doFilter(Request request, Response response, FilterChain chain) {
System.out.println("过滤器1 前置处理");
chain.doFilter(request, response);
System.out.println("过滤器1 后置处理");
}
}
SecondFilter
public class SecondFilter implements Filter {
public void doFilter(Request request, Response response, FilterChain chain) {
System.out.println("过滤器2 前置处理");
chain.doFilter(request, response);
System.out.println("过滤器2 后置处理");
}
}
Client
public class Client {
public static void main(String[] args) {
Request req = null;
Response res = null;
FilterChain filterChain = new FilterChain();
filterChain.addFilter(new FirstFilter()).addFilter(new SecondFilter());
filterChain.doFilter(req,res);
}
}
5. 状态模式
5.1 介绍
定义:
对于有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发送改变时改变其行为。
结构:
状态模式包含以下主要角色:
- 环境(Context)角色:也称为上下文,它定义了客户程序需要的接口,维护一个当前状态,并将与状态相关的操作委托给当前状态对象来处理;
- 抽象状态(State)角色:定义一个接口,用以封装环境对象中的特定状态所对应的行为;
- 具体状态(Concrete)角色:实现抽象状态所对应的行为;
类图1:
类图2:
优缺点:
优点:
- 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为;
- 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块;
缺点:
- 状态模型的使用必然会增加系统类和对象的个数;
- 状态模型的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱;
- 状态模型对“开闭原则”的支持并不太好;
5.2 使用场景
- 当一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为时,就可以考虑使用状态模型;
- 一个操作中含有庞大的分支结构,并且这些分支决定于对象的状态时;
6. 观察者模式
6.1 介绍
定义:
又被称为发布-订阅(Publish/Subscribe)模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监视某一个主题对象,这个主题对象在状态改变时,会通知所有观察者对象,使他们能够自动更新自己。
结构:
在观察者模式中有如下角色:
Subject:抽象主题(抽象被观察者),抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象;
ConcreteSubject:具体主题(具体被观察者),该角色将有关状态存入具体观察者对象,在具体主题的内部状态发送改变时,给所有注册过的观察者发送通知;
Observe:抽象观察者,是观察者的抽象类,它定义了一个更新接口,使得在得到主题更换通知时更新自己;
ConcreteObserve:具体观察者,实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态;
类图:
优缺点:
优点:
- 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系;
- 被观察者发送通知,所有注册的观察者都会收到信息(可以实现广播机制);
缺点:
- 如果观察者非常多的话,那么所有的观察者收到被观察者发送的通知会耗时;
- 如果被观察者有循环依赖的话,那么被观察者发送通知会使观察者循环调用,会导致系统奔溃;
6.2 使用场景
- 对象间存在一对多关系,一个对象的状态发送改变会影响其他对象;
- 当一个抽象模型有两个方面,其中一个方面依赖于另一个方面时;
6.3 JDK源码
Observable类和Observer接口
案例:警察抓小偷
7. 中介者模式
7.1 介绍
定义:
又叫调停模式,定义一个中介角色来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。
结构:
中介者模式包含以下角色:
抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法;
具体中介者(ConcreteMediator)角色:实现中介接口,定义一个List来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色;
抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能;
具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互;
类图:
优缺点:
优点:
- 松散耦合
中介者模式通过把多个同事之间的交互封装到中介者对象里面,从而使得同事对象之间松散耦合,基本上可以做到互补依赖,这样一来,同事对象就可以独立地变化和复用,而不再像以前那样“牵一处而动全身”了。
- 集中控制交互
多个同事对象的交互,本封装在中介对象里面集中管理,使得这些交互行为发生改变的时候,只需要改变中介者对象就可以了,当然如果是已经做好的系统,那么扩展中介者对象,而各个同事类不需要做修改。
- 一对多关联变为1对1关联
没有使用中介模式的时候,同事对象之间的通常是一对多的,引入中介对象以后,中介者对象和同事对象的关系通常变成双向的一对一,这会让对象的关系更容易理解和实现。
缺点:
当同事类太多时,中介者的职责将很大,它会变得复杂而庞大,以至于系统难以维护。
7.2 使用场景
- 系统中对象之间存在复杂的引用关系,系统结构混乱且难以理解;
- 当想创建一个运行于多个类之间的对象,又不想生成新的子类时;
8. 迭代器模式
8.1 介绍
定义:
提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。
结构:
迭代器模式主要包含以下角色:
抽象聚合(Aggregate)角色:定义存储、添加、删除聚合元素以及创建迭代器对象的接口;
具体聚合(ConcreteAggregate)角色:实现抽象聚合类,返回一个具体迭代器的实例;
抽象迭代器(Iterator)角色:定义访问和遍历聚合元素的接口,通常包括hasNext()、next()等方法;
具体迭代器(ConcreteIterator)角色:实现抽象迭代器接口中所定义的方法,完成对聚合对象的遍历,记录遍历的当前位置;
类图:
优缺点:
优点:
- 它支持不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式,在迭代器模式中只需要用一个不同的迭代器来替换原有迭代器即可改变遍历算法,我们也可以自己定义迭代器的子类以支持新的遍历方式;
- 迭代器简化了聚合类,由于引入了迭代器,在原有的聚合对象中不需要再自行提供遍历等方法,这样我们可以简化聚合类的设计;
- 在迭代器模式中,由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改原有代码,满足“开闭原则”的要求;
缺点:
- 增加了类的个数,这在一定程度上增加了系统的复杂性。
8.2 使用场景
- 当需要为聚合对象提供多种遍历方式时;
- 当需要为遍历不同的聚合结构提供一个统计的接口时;
- 当访问一个聚合对象的内容而无须暴露其内部细节的表示时;
8.3 JDK源码:
List:抽象聚合类;
ArrayList:具体的聚合类;
Iterator:抽象迭代器;
List.iterator():返回的是实现了Iterator接口的具体迭代器对象;
注意:
当我们在使用JAVA开发的时候,想要使用迭代器模式的话,只要让我们自己定义的容器类实现java.util.Iterable并实现其中的iterator()方法使其返回一个java.util.Iterator的实现类就可以了
9. 访问者模式
9.1 介绍
定义:
封装在一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作。
结构:
访问者模式包含以下主要角色:
- 抽象访问者(Visitor)角色:定义了对每一个元素(Element)访问的行为,它的参数就是可以访问的元素,它的方法个数理论上来讲与元素类个数(Element的实现类个数)是一样的,从这点不难看出,访问者模式要求元素类的个数不能改变;
- 具体访问者(ConcreteVisitor)角色:给出对每一个元素类访问时所产生的具体行为;
- 抽象元素(Element)角色:定义了一个接受访问者的方法(accept),其意义是指,每一个元素都要可以被访问者访问;
- 具体元素(ConcreteElement)角色:提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问中提供的访问该元素类的方法;
- 对象结构(Object Structure)角色:定义当中所提到的对象结构,对象结构是一个抽象表述,具体点可以理解为一个具有容器性质或者符合对象特性的类,它会含有一组元素(Element),并且可以迭代这些元素,供访问者访问;
类图:
优缺点:
优点:
- 扩展性好
在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
- 复用性好
通过访问者来定义整个对象结构通用的功能,从而提高复用程度。
- 分离无关行为
通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一。
缺点:
- 对象结构变化很困难
在访问者模式中,没增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
- 违反了依赖倒置原则
访问者模式依赖了具体类,而没有依赖抽象。
9.2 使用场景
- 对象结构相对稳定,但其操作算法经常变化的程序;
- 对象结构中的对象要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构;
10. 备忘录模式
10.1 介绍
定义:
又叫快照模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。
结构:
发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录的功能,实现其他业务功能,它可以访问备忘录里的所有信息;
备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人;
管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改;
备忘录有两个等价的接口:
宅接口:管理者(Caretajer)对象:(和其他发起人对象之外的任何对象)看到的是备忘录的宅接口(narror Interface),这个宅接口只允许他把备忘录对象传给其他的对象
宽接口:与管理者看到的宅接口相反,发起人对象可以看到一个宽接(wideInterface),这个宽接口允许它读取所有的数据,以便根据这些数据恢复这个发起人对象的内部状态;
类图:
白箱备忘录
黑箱备忘录
优缺点:
优点:
- 提供了一种可以恢复状态的机制,当用户需要时能够比较方便地将数据恢复到某个历史的状态
- 实现了内部状态的封装,除了创建它的发起人之外,其他对象都不能访问这些状态信息
- 简化了发起人类,发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则
缺点:
- 资源消耗大,如果要保存的内部状态信息过多或者特点频繁,将会占用比较大的内存资源。
10.2 使用场景
- 需要保存和恢复的场景,需要存档功能;
- 需要提供一个可回滚操作的场景,如Word、记事本、Photoshop、idea等软件在编辑时按ctrl + z组合键,还有数据库中事务操作;
11. 解释器模式
11.1 介绍
定义:
给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。
结构:
抽象表达式(Abstract Expression)角色:定义解释器的接口,约定解释器的解释操作,主要包含解释方法interpret();
终结符表达式(Termnal Expression)角色:是抽象表达式的子类,用来实现文法中与终结符相关的操作,文法中的每一个终结符都有一个具体终结表达式与之相对应;
非终结符表达式(Nonterminal Expression)角色:也是抽象表达式的子类,用来实现文法中与非终结符相关的操作,文法中的每条规则都对应于一个非终结符表达式;
环境(Context)角色:通常包含各个解释器需要的数据或是公共的功能,一般用来传递所有解释器共享的数据,后面的解释器可以从这里获取这些值
客户端(Client):主要任务是将需要分析的句子或表达式转换成使用解释器对象描述的抽象语法树,然后调用解释器的解释方法,当然也可以通过环境角色间接访问解释器的解释方法;
类图:
优点:
- 易于改变和扩展文法
由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法,每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言。
- 实现文法较为容易
在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂。
- 增加新的解释表达式较为方便
如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式,原有表达式类代码无需修改,符合“开闭原则”。
缺点:
- 对于复杂文法难以维护
在解释器模式中,每一天规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数会急剧增肌,导致系统难以管理和维护。
- 执行效率较低
由于在解释器模式中使用了大量的循环和递归调用,因此在解释器较为复杂的句子时其速度很慢,而且代码调试过程也比较麻烦。
11.2 使用场景
- 当语言的文法较为简单,且执行效率不是关键问题时;
- 当问题重复出现,且可以用一种简单的语言来进行表达时;
- 当一个语言需要解释执行,并且语言中的句子可以表示为一个抽象语法树的时候;
参考声明:
设计模式参考了B主黑马程序员:
https://www.bilibili.com/video/BV1Np4y1z7BU?spm_id_from=333.999.0.0
附github源码:
https://github.com/Fyupeng/java