5,结构型模式
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。
由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。
结构型模式分为以下 7 种:
- 代理模式
- 适配器模式
- 装饰者模式
- 桥接模式
- 外观模式
- 组合模式
- 享元模式
5.1 代理模式
5.1.1 概述
由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。
Java中的代理按照代理类生成时机不同又分为静态代理和动态代理。静态代理代理类在编译期就生成,而动态代理代理类则是在Java运行时动态生成。动态代理又有JDK代理和CGLib代理两种。
5.1.2 结构
代理(Proxy)模式分为三种角色:
- 抽象主题(Subject)类: 通过接口或抽象类声明真实主题和代理对象实现的业务方法。
- 真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
- 代理(Proxy)类 : 提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。
5.1.3 静态代理
我们通过案例来感受一下静态代理。
【例】火车站卖票
如果要买火车票的话,需要去火车站买票,坐车到火车站,排队等一系列的操作,显然比较麻烦。而火车站在多个地方都有代售点,我们去代售点买票就方便很多了。这个例子其实就是典型的代理模式,火车站是目标对象,代售点是代理对象。类图如下:
代码如下:
1
//卖票接口
2
public interface SellTickets {
3
void sell();
4
}
5
6
//火车站 火车站具有卖票功能,所以需要实现SellTickets接口
7
public class TrainStation implements SellTickets {
8
9
public void sell() {
10
System.out.println("火车站卖票");
11
}
12
}
13
14
//代售点
15
public class ProxyPoint implements SellTickets {
16
17
private TrainStation station = new TrainStation();
18
19
public void sell() {
20
System.out.println("代理点收取一些服务费用");
21
station.sell();
22
}
23
}
24
25
//测试类
26
public class Client {
27
public static void main(String[] args) {
28
ProxyPoint pp = new ProxyPoint();
29
pp.sell();
30
}
31
}
从上面代码中可以看出测试类直接访问的是ProxyPoint类对象,也就是说ProxyPoint作为访问对象和目标对象的中介。同时也对sell方法进行了增强(代理点收取一些服务费用)。
5.1.4 JDK动态代理
接下来我们使用动态代理实现上面案例,先说说JDK提供的动态代理。Java中提供了一个动态代理类Proxy,Proxy并不是我们上述所说的代理对象的类,而是提供了一个创建代理对象的静态方法(newProxyInstance方法)来获取代理对象。
代码如下:
1
//卖票接口
2
public interface SellTickets {
3
void sell();
4
}
5
6
//火车站 火车站具有卖票功能,所以需要实现SellTickets接口
7
public class TrainStation implements SellTickets {
8
9
public void sell() {
10
System.out.println("火车站卖票");
11
}
12
}
13
14
//代理工厂,用来创建代理对象
15
public class ProxyFactory {
16
17
private TrainStation station = new TrainStation();
18
19
public SellTickets getProxyObject() {
20
//使用Proxy获取代理对象
21
/*
22
newProxyInstance()方法参数说明:
23
ClassLoader loader : 类加载器,用于加载代理类,使用真实对象的类加载器即可
24
Class<?>[] interfaces : 真实对象所实现的接口,代理模式真实对象和代理对象实现相同的接口
25
InvocationHandler h : 代理对象的调用处理程序
26
*/
27
SellTickets sellTickets = (SellTickets) Proxy.newProxyInstance(station.getClass().getClassLoader(),
28
station.getClass().getInterfaces(),
29
new InvocationHandler() {
30
/*
31
InvocationHandler中invoke方法参数说明:
32
proxy : 代理对象
33
method : 对应于在代理对象上调用的接口方法的 Method 实例
34
args : 代理对象调用接口方法时传递的实际参数
35
*/
36
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
37
38
System.out.println("代理点收取一些服务费用(JDK动态代理方式)");
39
//执行真实对象
40
Object result = method.invoke(station, args);
41
return result;
42
}
43
});
44
return sellTickets;
45
}
46
}
47
48
//测试类
49
public class Client {
50
public static void main(String[] args) {
51
//获取代理对象
52
ProxyFactory factory = new ProxyFactory();
53
54
SellTickets proxyObject = factory.getProxyObject();
55
proxyObject.sell();
56
}
57
}
使用了动态代理,我们思考下面问题:
-
ProxyFactory是代理类吗?
ProxyFactory不是代理模式中所说的代理类,而代理类是程序在运行过程中动态的在内存中生成的类。通过阿里巴巴开源的 Java 诊断工具(Arthas【阿尔萨斯】)查看代理类的结构:
1 package com.sun.proxy; 2 3 import com.itheima.proxy.dynamic.jdk.SellTickets; 4 import java.lang.reflect.InvocationHandler; 5 import java.lang.reflect.Method; 6 import java.lang.reflect.Proxy; 7 import java.lang.reflect.UndeclaredThrowableException; 8 9 public final class $Proxy0 extends Proxy implements SellTickets { 10 private static Method m1; 11 private static Method m2; 12 private static Method m3; 13 private static Method m0; 14 15 public $Proxy0(InvocationHandler invocationHandler) { 16 super(invocationHandler); 17 } 18 19 static { 20 try { 21 m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object")); 22 m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]); 23 m3 = Class.forName("com.itheima.proxy.dynamic.jdk.SellTickets").getMethod("sell", new Class[0]); 24 m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]); 25 return; 26 } 27 catch (NoSuchMethodException noSuchMethodException) { 28 throw new NoSuchMethodError(noSuchMethodException.getMessage()); 29 } 30 catch (ClassNotFoundException classNotFoundException) { 31 throw new NoClassDefFoundError(classNotFoundException.getMessage()); 32 } 33 } 34 35 public final boolean equals(Object object) { 36 try { 37 return (Boolean)this.h.invoke(this, m1, new Object[]{object}); 38 } 39 catch (Error | RuntimeException throwable) { 40 throw throwable; 41 } 42 catch (Throwable throwable) { 43 throw new UndeclaredThrowableException(throwable); 44 } 45 } 46 47 public final String toString() { 48 try { 49 return (String)this.h.invoke(this, m2, null); 50 } 51 catch (Error | RuntimeException throwable) { 52 throw throwable; 53 } 54 catch (Throwable throwable) { 55 throw new UndeclaredThrowableException(throwable); 56 } 57 } 58 59 public final int hashCode() { 60 try { 61 return (Integer)this.h.invoke(this, m0, null); 62 } 63 catch (Error | RuntimeException throwable) { 64 throw throwable; 65 } 66 catch (Throwable throwable) { 67 throw new UndeclaredThrowableException(throwable); 68 } 69 } 70 71 public final void sell() { 72 try { 73 this.h.invoke(this, m3, null); 74 return; 75 } 76 catch (Error | RuntimeException throwable) { 77 throw throwable; 78 } 79 catch (Throwable throwable) { 80 throw new UndeclaredThrowableException(throwable); 81 } 82 } 83 }从上面的类中,我们可以看到以下几个信息:
- 代理类($Proxy0)实现了SellTickets。这也就印证了我们之前说的真实类和代理类实现同样的接口。
- 代理类($Proxy0)将我们提供了的匿名内部类对象传递给了父类。
-
动态代理的执行流程是什么样?
下面是摘取的重点代码:
1 //程序运行过程中动态生成的代理类 2 public final class $Proxy0 extends Proxy implements SellTickets { 3 private static Method m3; 4 5 public $Proxy0(InvocationHandler invocationHandler) { 6 super(invocationHandler); 7 } 8 9 static { 10 m3 = Class.forName("com.itheima.proxy.dynamic.jdk.SellTickets").getMethod("sell", new Class[0]); 11 } 12 13 public final void sell() { 14 this.h.invoke(this, m3, null); 15 } 16 } 17 18 //Java提供的动态代理相关类 19 public class Proxy implements java.io.Serializable { 20 protected InvocationHandler h; 21 22 protected Proxy(InvocationHandler h) { 23 this.h = h; 24 } 25 } 26 27 //代理工厂类 28 public class ProxyFactory { 29 30 private TrainStation station = new TrainStation(); 31 32 public SellTickets getProxyObject() { 33 SellTickets sellTickets = (SellTickets) Proxy.newProxyInstance(station.getClass().getClassLoader(), 34 station.getClass().getInterfaces(), 35 new InvocationHandler() { 36 37 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 38 39 System.out.println("代理点收取一些服务费用(JDK动态代理方式)"); 40 Object result = method.invoke(station, args); 41 return result; 42 } 43 }); 44 return sellTickets; 45 } 46 } 47 48 49 //测试访问类 50 public class Client { 51 public static void main(String[] args) { 52 //获取代理对象 53 ProxyFactory factory = new ProxyFactory(); 54 SellTickets proxyObject = factory.getProxyObject(); 55 proxyObject.sell(); 56 } 57 }
执行流程如下:
- 在测试类中通过代理对象调用sell()方法
- 根据多态的特性,执行的是代理类($Proxy0)中的sell()方法
- 代理类($Proxy0)中的sell()方法中又调用了InvocationHandler接口的子实现类对象的invoke方法
- invoke方法通过反射执行了真实对象所属类(TrainStation)中的sell()方法
5.1.5 CGLIB动态代理
同样是上面的案例,我们再次使用CGLIB代理实现。
如果没有定义SellTickets接口,只定义了TrainStation(火车站类)。很显然JDK代理是无法使用了,因为JDK动态代理要求必须定义接口,对接口进行代理。
CGLIB是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。
CGLIB是第三方提供的包,所以需要引入jar包的坐标:
1
<dependency>
2
<groupId>cglib</groupId>
3
<artifactId>cglib</artifactId>
4
<version>2.2.2</version>
5
</dependency>
代码如下:
1
//火车站
2
public class TrainStation {
3
4
public void sell() {
5
System.out.println("火车站卖票");
6
}
7
}
8
9
//代理工厂
10
public class ProxyFactory implements MethodInterceptor {
11
12
private TrainStation target = new TrainStation();
13
14
public TrainStation getProxyObject() {
15
//创建Enhancer对象,类似于JDK动态代理的Proxy类,下一步就是设置几个参数
16
Enhancer enhancer =new Enhancer();
17
//设置父类的字节码对象
18
enhancer.setSuperclass(target.getClass());
19
//设置回调函数
20
enhancer.setCallback(this);
21
//创建代理对象
22
TrainStation obj = (TrainStation) enhancer.create();
23
return obj;
24
}
25
26
/*
27
intercept方法参数说明:
28
o : 代理对象
29
method : 真实对象中的方法的Method实例
30
args : 实际参数
31
methodProxy :代理对象中的方法的method实例
32
*/
33
public TrainStation intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
34
System.out.println("代理点收取一些服务费用(CGLIB动态代理方式)");
35
TrainStation result = (TrainStation) methodProxy.invokeSuper(o, args);
36
return result;
37
}
38
}
39
40
//测试类
41
public class Client {
42
public static void main(String[] args) {
43
//创建代理工厂对象
44
ProxyFactory factory = new ProxyFactory();
45
//获取代理对象
46
TrainStation proxyObject = factory.getProxyObject();
47
48
proxyObject.sell();
49
}
50
}
5.1.6 三种代理的对比
-
jdk代理和CGLIB代理
使用CGLib实现动态代理,CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,在JDK1.6之前比使用Java反射效率要高。唯一需要注意的是,CGLib不能对声明为final的类或者方法进行代理,因为CGLib原理是动态生成被代理类的子类。
在JDK1.6、JDK1.7、JDK1.8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLib代理效率,只有当进行大量调用的时候,JDK1.6和JDK1.7比CGLib代理效率低一点,但是到JDK1.8的时候,JDK代理效率高于CGLib代理。所以如果有接口使用JDK动态代理,如果没有接口使用CGLIB代理。
-
动态代理和静态代理
动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。
如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。而动态代理不会出现该问题
5.1.7 优缺点
优点:
- 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
- 代理对象可以扩展目标对象的功能;
- 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度;
缺点:
- 增加了系统的复杂度;
5.1.8 使用场景
-
远程(Remote)代理
本地服务通过网络请求远程服务。为了实现本地到远程的通信,我们需要实现网络通信,处理其中可能的异常。为良好的代码设计和可维护性,我们将网络通信部分隐藏起来,只暴露给本地服务一个接口,通过该接口即可访问远程服务提供的功能,而不必过多关心通信部分的细节。
-
防火墙(Firewall)代理
当你将浏览器配置成使用代理功能时,防火墙就将你的浏览器的请求转给互联网;当互联网返回响应时,代理服务器再把它转给你的浏览器。
-
保护(Protect or Access)代理
控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限。
5.2 适配器模式
5.2.1 概述
如果去欧洲国家去旅游的话,他们的插座如下图最左边,是欧洲标准。而我们使用的插头如下图最右边的。因此我们的笔记本电脑,手机在当地不能直接充电。所以就需要一个插座转换器,转换器第1面插入当地的插座,第2面供我们充电,这样使得我们的插头在当地能使用。生活中这样的例子很多,手机充电器(将220v转换为5v的电压),读卡器等,其实就是使用到了适配器模式。
定义:
将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
适配器模式分为类适配器模式和对象适配器模式,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。
5.2.2 结构
适配器模式(Adapter)包含以下主要角色:
- 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
- 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
- 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。
5.2.3 类适配器模式
实现方式:定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件。
【例】读卡器
现有一台电脑只能读取SD卡,而要读取TF卡中的内容的话就需要使用到适配器模式。创建一个读卡器,将TF卡中的内容读取出来。
类图如下:
代码如下:
1
//SD卡的接口
2
public interface SDCard {
3
//读取SD卡方法
4
String readSD();
5
//写入SD卡功能
6
void writeSD(String msg);
7
}
8
9
//SD卡实现类
10
public class SDCardImpl implements SDCard {
11
public String readSD() {
12
String msg = "sd card read a msg :hello word SD";
13
return msg;
14
}
15
16
public void writeSD(String msg) {
17
System.out.println("sd card write msg : " + msg);
18
}
19
}
20
21
//电脑类
22
public class Computer {
23
24
public String readSD(SDCard sdCard) {
25
if(sdCard == null) {
26
throw new NullPointerException("sd card null");
27
}
28
return sdCard.readSD();
29
}
30
}
31
32
//TF卡接口
33
public interface TFCard {
34
//读取TF卡方法
35
String readTF();
36
//写入TF卡功能
37
void writeTF(String msg);
38
}
39
40
//TF卡实现类
41
public class TFCardImpl implements TFCard {
42
43
public String readTF() {
44
String msg ="tf card read msg : hello word tf card";
45
return msg;
46
}
47
48
public void writeTF(String msg) {
49
System.out.println("tf card write a msg : " + msg);
50
}
51
}
52
53
//定义适配器类(SD兼容TF)
54
public class SDAdapterTF extends TFCardImpl implements SDCard {
55
56
public String readSD() {
57
System.out.println("adapter read tf card ");
58
return readTF();
59
}
60
61
public void writeSD(String msg) {
62
System.out.println("adapter write tf card");
63
writeTF(msg);
64
}
65
}
66
67
//测试类
68
public class Client {
69
public static void main(String[] args) {
70
Computer computer = new Computer();
71
SDCard sdCard = new SDCardImpl();
72
System.out.println(computer.readSD(sdCard));
73
74
System.out.println("------------");
75
76
SDAdapterTF adapter = new SDAdapterTF();
77
System.out.println(computer.readSD(adapter));
78
}
79
}
类适配器模式违背了合成复用原则。类适配器是客户类有一个接口规范的情况下可用,反之不可用。
5.2.4 对象适配器模式
实现方式:对象适配器模式可釆用将现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口。
【例】读卡器
我们使用对象适配器模式将读卡器的案例进行改写。类图如下:
代码如下:
类适配器模式的代码,我们只需要修改适配器类(SDAdapterTF)和测试类。
1
//创建适配器对象(SD兼容TF)
2
public class SDAdapterTF implements SDCard {
3
4
private TFCard tfCard;
5
6
public SDAdapterTF(TFCard tfCard) {
7
this.tfCard = tfCard;
8
}
9
10
public String readSD() {
11
System.out.println("adapter read tf card ");
12
return tfCard.readTF();
13
}
14
15
public void writeSD(String msg) {
16
System.out.println("adapter write tf card");
17
tfCard.writeTF(msg);
18
}
19
}
20
21
//测试类
22
public class Client {
23
public static void main(String[] args) {
24
Computer computer = new Computer();
25
SDCard sdCard = new SDCardImpl();
26
System.out.println(computer.readSD(sdCard));
27
28
System.out.println("------------");
29
30
TFCard tfCard = new TFCardImpl();
31
SDAdapterTF adapter = new SDAdapterTF(tfCard);
32
System.out.println(computer.readSD(adapter));
33
}
34
}
注意:还有一个适配器模式是接口适配器模式。当不希望实现一个接口中所有的方法时,可以创建一个抽象类Adapter ,实现所有方法。而此时我们只需要继承该抽象类即可。
5.2.5 应用场景
- 以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致。
- 使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同。
5.2.6 JDK源码解析
Reader(字符流)、InputStream(字节流)的适配使用的是InputStreamReader。
InputStreamReader继承自java.io包中的Reader,对他中的抽象的未实现的方法给出实现。如:
1
public int read() throws IOException {
2
return sd.read();
3
}
4
5
public int read(char cbuf[], int offset, int length) throws IOException {
6
return sd.read(cbuf, offset, length);
7
}
如上代码中的sd(StreamDecoder类对象),在Sun的JDK实现中,实际的方法实现是对sun.nio.cs.StreamDecoder类的同名方法的调用封装。类结构图如下:
从上图可以看出:
- InputStreamReader是对同样实现了Reader的StreamDecoder的封装。
- StreamDecoder不是Java SE API中的内容,是Sun JDK给出的自身实现。但我们知道他们对构造方法中的字节流类(InputStream)进行封装,并通过该类进行了字节流和字符流之间的解码转换。
结论:
从表层来看,InputStreamReader做了InputStream字节流类到Reader字符流之间的转换。而从如上Sun JDK中的实现类关系结构中可以看出,是StreamDecoder的设计实现在实际上采用了适配器模式。
5.3 装饰者模式
5.3.1 概述
我们先来看一个快餐店的例子。
快餐店有炒面、炒饭这些快餐,可以额外附加鸡蛋、火腿、培根这些配菜,当然加配菜需要额外加钱,每个配菜的价钱通常不太一样,那么计算总价就会显得比较麻烦。
使用继承的方式存在的问题:
-
扩展性不好
如果要再加一种配料(火腿肠),我们就会发现需要给FriedRice和FriedNoodles分别定义一个子类。如果要新增一个快餐品类(炒河粉)的话,就需要定义更多的子类。
-
产生过多的子类
定义:
指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式。
5.3.2 结构
装饰(Decorator)模式中的角色:
- 抽象构件(Component)角色 :定义一个抽象接口以规范准备接收附加责任的对象。
- 具体构件(Concrete Component)角色 :实现抽象构件,通过装饰角色为其添加一些职责。
- 抽象装饰(Decorator)角色 : 继承或实现抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
- 具体装饰(ConcreteDecorator)角色 :实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
5.3.3 案例
我们使用装饰者模式对快餐店案例进行改进,体会装饰者模式的精髓。
类图如下:
代码如下:
1
//快餐接口
2
public abstract class FastFood {
3
private float price;
4
private String desc;
5
6
public FastFood() {
7
}
8
9
public FastFood(float price, String desc) {
10
this.price = price;
11
this.desc = desc;
12
}
13
14
public void setPrice(float price) {
15
this.price = price;
16
}
17
18
public float getPrice() {
19
return price;
20
}
21
22
public String getDesc() {
23
return desc;
24
}
25
26
public void setDesc(String desc) {
27
this.desc = desc;
28
}
29
30
public abstract float cost(); //获取价格
31
}
32
33
//炒饭
34
public class FriedRice extends FastFood {
35
36
public FriedRice() {
37
super(10, "炒饭");
38
}
39
40
public float cost() {
41
return getPrice();
42
}
43
}
44
45
//炒面
46
public class FriedNoodles extends FastFood {
47
48
public FriedNoodles() {
49
super(12, "炒面");
50
}
51
52
public float cost() {
53
return getPrice();
54
}
55
}
56
57
//配料类
58
public abstract class Garnish extends FastFood {
59
60
private FastFood fastFood;
61
62
public FastFood getFastFood() {
63
return fastFood;
64
}
65
66
public void setFastFood(FastFood fastFood) {
67
this.fastFood = fastFood;
68
}
69
70
public Garnish(FastFood fastFood, float price, String desc) {
71
super(price,desc);
72
this.fastFood = fastFood;
73
}
74
}
75
76
//鸡蛋配料
77
public class Egg extends Garnish {
78
79
public Egg(FastFood fastFood) {
80
super(fastFood,1,"鸡蛋");
81
}
82
83
public float cost() {
84
return getPrice() + getFastFood().getPrice();
85
}
86
87
@Override
88
public String getDesc() {
89
return super.getDesc() + getFastFood().getDesc();
90
}
91
}
92
93
//培根配料
94
public class Bacon extends Garnish {
95
96
public Bacon(FastFood fastFood) {
97
98
super(fastFood,2,"培根");
99
}
100
101
@Override
102
public float cost() {
103
return getPrice() + getFastFood().getPrice();
104
}
105
106
@Override
107
public String getDesc() {
108
return super.getDesc() + getFastFood().getDesc();
109
}
110
}
111
112
//测试类
113
public class Client {
114
public static void main(String[] args) {
115
//点一份炒饭
116
FastFood food = new FriedRice();
117
//花费的价格
118
System.out.println(food.getDesc() + " " + food.cost() + "元");
119
120
System.out.println("========");
121
//点一份加鸡蛋的炒饭
122
FastFood food1 = new FriedRice();
123
124
food1 = new Egg(food1);
125
//花费的价格
126
System.out.println(food1.getDesc() + " " + food1.cost() + "元");
127
128
System.out.println("========");
129
//点一份加培根的炒面
130
FastFood food2 = new FriedNoodles();
131
food2 = new Bacon(food2);
132
//花费的价格
133
System.out.println(food2.getDesc() + " " + food2.cost() + "元");
134
}
135
}
好处:
- 饰者模式可以带来比继承更加灵活性的扩展功能,使用更加方便,可以通过组合不同的装饰者对象来获取具有不同行为状态的多样化的结果。装饰者模式比继承更具良好的扩展性,完美的遵循开闭原则,继承是静态的附加责任,装饰者则是动态的附加责任。
- 装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。
5.3.4 使用场景
-
当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。
不能采用继承的情况主要有两类:
- 第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;
- 第二类是因为类定义不能继承(如final类)
-
在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
-
当对象的功能要求可以动态地添加,也可以再动态地撤销时。
5.3.5 JDK源码解析
IO流中的包装类使用到了装饰者模式。BufferedInputStream,BufferedOutputStream,BufferedReader,BufferedWriter。
我们以BufferedWriter举例来说明,先看看如何使用BufferedWriter
1
public class Demo {
2
public static void main(String[] args) throws Exception{
3
//创建BufferedWriter对象
4
//创建FileWriter对象
5
FileWriter fw = new FileWriter("C:\Users\Think\Desktop\a.txt");
6
BufferedWriter bw = new BufferedWriter(fw);
7
8
//写数据
9
bw.write("hello Buffered");
10
11
bw.close();
12
}
13
}
使用起来感觉确实像是装饰者模式,接下来看它们的结构:
小结:
BufferedWriter使用装饰者模式对Writer子实现类进行了增强,添加了缓冲区,提高了写数据的效率。
5.3.6 代理和装饰者的区别
静态代理和装饰者模式的区别:
-
相同点:
- 都要实现与目标类相同的业务接口
- 在两个类中都要声明目标对象
- 都可以在不修改目标类的前提下增强目标方法
-
不同点:
- 目的不同 装饰者是为了增强目标对象 静态代理是为了保护和隐藏目标对象
- 获取目标对象构建的地方不同 装饰者是由外界传递进来,可以通过构造方法传递 静态代理是在代理类内部创建,以此来隐藏目标对象
5.4 桥接模式
5.4.1 概述
现在有一个需求,需要创建不同的图形,并且每个图形都有可能会有不同的颜色。我们可以利用继承的方式来设计类的关系:
我们可以发现有很多的类,假如我们再增加一个形状或再增加一种颜色,就需要创建更多的类。
试想,在一个有多种可能会变化的维度的系统中,用继承方式会造成类爆炸,扩展起来不灵活。每次在一个维度上新增一个具体实现都要增加多个子类。为了更加灵活的设计系统,我们此时可以考虑使用桥接模式。
定义:
将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
5.4.2 结构
桥接(Bridge)模式包含以下主要角色:
- 抽象化(Abstraction)角色 :定义抽象类,并包含一个对实现化对象的引用。
- 扩展抽象化(Refined Abstraction)角色 :是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
- 实现化(Implementor)角色 :定义实现化角色的接口,供扩展抽象化角色调用。
- 具体实现化(Concrete Implementor)角色 :给出实现化角色接口的具体实现。
5.4.3 案例
【例】视频播放器
需要开发一个跨平台视频播放器,可以在不同操作系统平台(如Windows、Mac、Linux等)上播放多种格式的视频文件,常见的视频格式包括RMVB、AVI、WMV等。该播放器包含了两个维度,适合使用桥接模式。
类图如下:
代码如下:
1
//视频文件
2
public interface VideoFile {
3
void decode(String fileName);
4
}
5
6
//avi文件
7
public class AVIFile implements VideoFile {
8
public void decode(String fileName) {
9
System.out.println("avi视频文件:"+ fileName);
10
}
11
}
12
13
//rmvb文件
14
public class REVBBFile implements VideoFile {
15
16
public void decode(String fileName) {
17
System.out.println("rmvb文件:" + fileName);
18
}
19
}
20
21
//操作系统版本
22
public abstract class OperatingSystemVersion {
23
24
protected VideoFile videoFile;
25
26
public OperatingSystemVersion(VideoFile videoFile) {
27
this.videoFile = videoFile;
28
}
29
30
public abstract void play(String fileName);
31
}
32
33
//Windows版本
34
public class Windows extends OperatingSystem {
35
36
public Windows(VideoFile videoFile) {
37
super(videoFile);
38
}
39
40
public void play(String fileName) {
41
videoFile.decode(fileName);
42
}
43
}
44
45
//mac版本
46
public class Mac extends OperatingSystemVersion {
47
48
public Mac(VideoFile videoFile) {
49
super(videoFile);
50
}
51
52
public void play(String fileName) {
53
videoFile.decode(fileName);
54
}
55
}
56
57
//测试类
58
public class Client {
59
public static void main(String[] args) {
60
OperatingSystem os = new Windows(new AVIFile());
61
os.play("战狼3");
62
}
63
}
好处:
-
桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。
如:如果现在还有一种视频文件类型wmv,我们只需要再定义一个类实现VideoFile接口即可,其他类不需要发生变化。
-
实现细节对客户透明
5.4.4 使用场景
- 当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时。
- 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。
- 当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时。避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。
5.5 外观模式
5.5.1 概述
有些人可能炒过股票,但其实大部分人都不太懂,这种没有足够了解证券知识的情况下做股票是很容易亏钱的,刚开始炒股肯定都会想,如果有个懂行的帮帮手就好,其实基金就是个好帮手,支付宝里就有许多的基金,它将投资者分散的资金集中起来,交由专业的经理人进行管理,投资于股票、债券、外汇等领域,而基金投资的收益归持有者所有,管理机构收取一定比例的托管管理费用。
定义:
又名门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体的细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。
外观(Facade)模式是“迪米特法则”的典型应用
5.5.2 结构
外观(Facade)模式包含以下主要角色:
- 外观(Facade)角色:为多个子系统对外提供一个共同的接口。
- 子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它。
5.5.3 案例
【例】智能家电控制
小明的爷爷已经60岁了,一个人在家生活:每次都需要打开灯、打开电视、打开空调;睡觉时关闭灯、关闭电视、关闭空调;操作起来都比较麻烦。所以小明给爷爷买了智能音箱,可以通过语音直接控制这些智能家电的开启和关闭。类图如下:
代码如下:
1
//灯类
2
public class Light {
3
public void on() {
4
System.out.println("打开了灯....");
5
}
6
7
public void off() {
8
System.out.println("关闭了灯....");
9
}
10
}
11
12
//电视类
13
public class TV {
14
public void on() {
15
System.out.println("打开了电视....");
16
}
17
18
public void off() {
19
System.out.println("关闭了电视....");
20
}
21
}
22
23
//控制类
24
public class AirCondition {
25
public void on() {
26
System.out.println("打开了空调....");
27
}
28
29
public void off() {
30
System.out.println("关闭了空调....");
31
}
32
}
33
34
//智能音箱
35
public class SmartAppliancesFacade {
36
37
private Light light;
38
private TV tv;
39
private AirCondition airCondition;
40
41
public SmartAppliancesFacade() {
42
light = new Light();
43
tv = new TV();
44
airCondition = new AirCondition();
45
}
46
47
public void say(String message) {
48
if(message.contains("打开")) {
49
on();
50
} else if(message.contains("关闭")) {
51
off();
52
} else {
53
System.out.println("我还听不懂你说的!!!");
54
}
55
}
56
57
//起床后一键开电器
58
private void on() {
59
System.out.println("起床了");
60
light.on();
61
tv.on();
62
airCondition.on();
63
}
64
65
//睡觉一键关电器
66
private void off() {
67
System.out.println("睡觉了");
68
light.off();
69
tv.off();
70
airCondition.off();
71
}
72
}
73
74
//测试类
75
public class Client {
76
public static void main(String[] args) {
77
//创建外观对象
78
SmartAppliancesFacade facade = new SmartAppliancesFacade();
79
//客户端直接与外观对象进行交互
80
facade.say("打开家电");
81
facade.say("关闭家电");
82
}
83
}
好处:
- 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
- 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。
缺点:
- 不符合开闭原则,修改很麻烦
5.5.4 使用场景
- 对分层结构系统构建时,使用外观模式定义子系统中每层的入口点可以简化子系统之间的依赖关系。
- 当一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问。
- 当客户端与多个子系统之间存在很大的联系时,引入外观模式可将它们分离,从而提高子系统的独立性和可移植性。
5.5.5 源码解析
使用tomcat作为web容器时,接收浏览器发送过来的请求,tomcat会将请求信息封装成ServletRequest对象,如下图①处对象。但是大家想想ServletRequest是一个接口,它还有一个子接口HttpServletRequest,而我们知道该request对象肯定是一个HttpServletRequest对象的子实现类对象,到底是哪个类的对象呢?可以通过输出request对象,我们就会发现是一个名为RequestFacade的类的对象。
RequestFacade类就使用了外观模式。先看结构图:
为什么在此处使用外观模式呢?
定义 RequestFacade 类,分别实现 ServletRequest ,同时定义私有成员变量 Request ,并且方法的实现调用 Request 的实现。然后,将 RequestFacade上转为 ServletRequest 传给 servlet 的 service 方法,这样即使在 servlet 中被下转为 RequestFacade ,也不能访问私有成员变量对象中的方法。既用了 Request ,又能防止其中方法被不合理的访问。
5,结构型模式
5.6 组合模式
5.6.1 概述
对于这个图片肯定会非常熟悉,上图我们可以看做是一个文件系统,对于这样的结构我们称之为树形结构。在树形结构中可以通过调用某个方法来遍历整个树,当我们找到某个叶子节点后,就可以对叶子节点进行相关的操作。可以将这颗树理解成一个大的容器,容器里面包含很多的成员对象,这些成员对象即可是容器对象也可以是叶子对象。但是由于容器对象和叶子对象在功能上面的区别,使得我们在使用的过程中必须要区分容器对象和叶子对象,但是这样就会给客户带来不必要的麻烦,作为客户而已,它始终希望能够一致的对待容器对象和叶子对象。
定义:
又名部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。
5.6.2 结构
组合模式主要包含三种角色:
- 抽象根节点(Component):定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性。
- 树枝节点(Composite):定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构。
- 叶子节点(Leaf):叶子节点对象,其下再无分支,是系统层次遍历的最小单位。
5.6.3 案例实现
【例】软件菜单
如下图,我们在访问别的一些管理系统时,经常可以看到类似的菜单。一个菜单可以包含菜单项(菜单项是指不再包含其他内容的菜单条目),也可以包含带有其他菜单项的菜单,因此使用组合模式描述菜单就很恰当,我们的需求是针对一个菜单,打印出其包含的所有菜单以及菜单项的名称。
要实现该案例,我们先画出类图:
代码实现:
不管是菜单还是菜单项,都应该继承自统一的接口,这里姑且将这个统一的接口称为菜单组件。
1
//菜单组件 不管是菜单还是菜单项,都应该继承该类
2
public abstract class MenuComponent {
3
4
protected String name;
5
protected int level;
6
7
//添加菜单
8
public void add(MenuComponent menuComponent){
9
throw new UnsupportedOperationException();
10
}
11
12
//移除菜单
13
public void remove(MenuComponent menuComponent){
14
throw new UnsupportedOperationException();
15
}
16
17
//获取指定的子菜单
18
public MenuComponent getChild(int i){
19
throw new UnsupportedOperationException();
20
}
21
22
//获取菜单名称
23
public String getName(){
24
return name;
25
}
26
27
public void print(){
28
throw new UnsupportedOperationException();
29
}
30
}
这里的MenuComponent定义为抽象类,因为有一些共有的属性和行为要在该类中实现,Menu和MenuItem类就可以只覆盖自己感兴趣的方法,而不用搭理不需要或者不感兴趣的方法,举例来说,Menu类可以包含子菜单,因此需要覆盖add()、remove()、getChild()方法,但是MenuItem就不应该有这些方法。这里给出的默认实现是抛出异常,你也可以根据自己的需要改写默认实现。
1
public class Menu extends MenuComponent {
2
3
private List<MenuComponent> menuComponentList;
4
5
public Menu(String name,int level){
6
this.level = level;
7
this.name = name;
8
menuComponentList = new ArrayList<MenuComponent>();
9
}
10
11
@Override
12
public void add(MenuComponent menuComponent) {
13
menuComponentList.add(menuComponent);
14
}
15
16
@Override
17
public void remove(MenuComponent menuComponent) {
18
menuComponentList.remove(menuComponent);
19
}
20
21
@Override
22
public MenuComponent getChild(int i) {
23
return menuComponentList.get(i);
24
}
25
26
@Override
27
public void print() {
28
29
for (int i = 1; i < level; i++) {
30
System.out.print("--");
31
}
32
System.out.println(name);
33
for (MenuComponent menuComponent : menuComponentList) {
34
menuComponent.print();
35
}
36
}
37
}
Menu类已经实现了除了getName方法的其他所有方法,因为Menu类具有添加菜单,移除菜单和获取子菜单的功能。
1
public class MenuItem extends MenuComponent {
2
3
public MenuItem(String name,int level) {
4
this.name = name;
5
this.level = level;
6
}
7
8
@Override
9
public void print() {
10
for (int i = 1; i < level; i++) {
11
System.out.print("--");
12
}
13
System.out.println(name);
14
}
15
}
MenuItem是菜单项,不能再有子菜单,所以添加菜单,移除菜单和获取子菜单的功能并不能实现。
5.6.4 组合模式的分类
在使用组合模式时,根据抽象构件类的定义形式,我们可将组合模式分为透明组合模式和安全组合模式两种形式。
-
透明组合模式
透明组合模式中,抽象根节点角色中声明了所有用于管理成员对象的方法,比如在示例中
MenuComponent声明了add、remove、getChild方法,这样做的好处是确保所有的构件类都有相同的接口。透明组合模式也是组合模式的标准形式。透明组合模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的,叶子对象不可能有下一个层次的对象,即不可能包含成员对象,因此为其提供 add()、remove() 等方法是没有意义的,这在编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(如果没有提供相应的错误处理代码)
-
安全组合模式
在安全组合模式中,在抽象构件角色中没有声明任何用于管理成员对象的方法,而是在树枝节点
Menu类中声明并实现这些方法。安全组合模式的缺点是不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件。
5.6.5 优点
- 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制。
- 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码。
- 在组合模式中增加新的树枝节点和叶子节点都很方便,无须对现有类库进行任何修改,符合“开闭原则”。
- 组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子节点和树枝节点的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。
5.6.6 使用场景
组合模式正是应树形结构而生,所以组合模式的使用场景就是出现树形结构的地方。比如:文件目录显示,多级目录呈现等树形结构数据的操作。
5.7 享元模式
5.7.1 概述
定义:
运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似对象的开销,从而提高系统资源的利用率。
5.7.2 结构
享元(Flyweight )模式中存在以下两种状态:
- 内部状态,即不会随着环境的改变而改变的可共享部分。
- 外部状态,指随环境改变而改变的不可以共享的部分。享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。
享元模式的主要有以下角色:
- 抽象享元角色(Flyweight):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。
- 具体享元(Concrete Flyweight)角色 :它实现了抽象享元类,称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。
- 非享元(Unsharable Flyweight)角色 :并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
- 享元工厂(Flyweight Factory)角色 :负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。
5.7.3 案例实现
【例】俄罗斯方块
下面的图片是众所周知的俄罗斯方块中的一个个方块,如果在俄罗斯方块这个游戏中,每个不同的方块都是一个实例对象,这些对象就要占用很多的内存空间,下面利用享元模式进行实现。
先来看类图:
代码如下:
俄罗斯方块有不同的形状,我们可以对这些形状向上抽取出AbstractBox,用来定义共性的属性和行为。
1
public abstract class AbstractBox {
2
public abstract String getShape();
3
4
public void display(String color) {
5
System.out.println("方块形状:" + this.getShape() + " 颜色:" + color);
6
}
7
}
接下来就是定义不同的形状了,IBox类、LBox类、OBox类等。
1
public class IBox extends AbstractBox {
2
3
@Override
4
public String getShape() {
5
return "I";
6
}
7
}
8
9
public class LBox extends AbstractBox {
10
11
@Override
12
public String getShape() {
13
return "L";
14
}
15
}
16
17
public class OBox extends AbstractBox {
18
19
@Override
20
public String getShape() {
21
return "O";
22
}
23
}
提供了一个工厂类(BoxFactory),用来管理享元对象(也就是AbstractBox子类对象),该工厂类对象只需要一个,所以可以使用单例模式。并给工厂类提供一个获取形状的方法。
1
public class BoxFactory {
2
3
private static HashMap<String, AbstractBox> map;
4
5
private BoxFactory() {
6
map = new HashMap<String, AbstractBox>();
7
AbstractBox iBox = new IBox();
8
AbstractBox lBox = new LBox();
9
AbstractBox oBox = new OBox();
10
map.put("I", iBox);
11
map.put("L", lBox);
12
map.put("O", oBox);
13
}
14
15
public static final BoxFactory getInstance() {
16
return SingletonHolder.INSTANCE;
17
}
18
19
private static class SingletonHolder {
20
private static final BoxFactory INSTANCE = new BoxFactory();
21
}
22
23
public AbstractBox getBox(String key) {
24
return map.get(key);
25
}
26
}
5.7.5 优缺点和使用场景
1,优点
- 极大减少内存中相似或相同对象数量,节约系统资源,提供系统性能
- 享元模式中的外部状态相对独立,且不影响内部状态
2,缺点:
为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂
3,使用场景:
- 一个系统有大量相同或者相似的对象,造成内存的大量耗费。
- 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
- 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。
5.7.6 JDK源码解析
Integer类使用了享元模式。我们先看下面的例子:
1
public class Demo {
2
public static void main(String[] args) {
3
Integer i1 = 127;
4
Integer i2 = 127;
5
6
System.out.println("i1和i2对象是否是同一个对象?" + (i1 == i2));
7
8
Integer i3 = 128;
9
Integer i4 = 128;
10
11
System.out.println("i3和i4对象是否是同一个对象?" + (i3 == i4));
12
}
13
}
运行上面代码,结果如下:
为什么第一个输出语句输出的是true,第二个输出语句输出的是false?通过反编译软件进行反编译,代码如下:
1
public class Demo {
2
public static void main(String[] args) {
3
Integer i1 = Integer.valueOf((int)127);
4
Integer i2 Integer.valueOf((int)127);
5
System.out.println((String)new StringBuilder().append((String)"i1\u548ci2\u5bf9\u8c61\u662f\u5426\u662f\u540c\u4e00\u4e2a\u5bf9\u8c61\uff1f").append((boolean)(i1 == i2)).toString());
6
Integer i3 = Integer.valueOf((int)128);
7
Integer i4 = Integer.valueOf((int)128);
8
System.out.println((String)new StringBuilder().append((String)"i3\u548ci4\u5bf9\u8c61\u662f\u5426\u662f\u540c\u4e00\u4e2a\u5bf9\u8c61\uff1f").append((boolean)(i3 == i4)).toString());
9
}
10
}
上面代码可以看到,直接给Integer类型的变量赋值基本数据类型数据的操作底层使用的是 valueOf() ,所以只需要看该方法即可
1
public final class Integer extends Number implements Comparable<Integer> {
2
3
public static Integer valueOf(int i) {
4
if (i >= IntegerCache.low && i <= IntegerCache.high)
5
return IntegerCache.cache[i + (-IntegerCache.low)];
6
return new Integer(i);
7
}
8
9
private static class IntegerCache {
10
static final int low = -128;
11
static final int high;
12
static final Integer cache[];
13
14
static {
15
int h = 127;
16
String integerCacheHighPropValue =
17
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
18
if (integerCacheHighPropValue != null) {
19
try {
20
int i = parseInt(integerCacheHighPropValue);
21
i = Math.max(i, 127);
22
// Maximum array size is Integer.MAX_VALUE
23
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
24
} catch( NumberFormatException nfe) {
25
}
26
}
27
high = h;
28
cache = new Integer[(high - low) + 1];
29
int j = low;
30
for(int k = 0; k < cache.length; k++)
31
cache[k] = new Integer(j++);
32
// range [-128, 127] must be interned (JLS7 5.1.7)
33
assert IntegerCache.high >= 127;
34
}
35
36
private IntegerCache() {}
37
}
38
}
可以看到 Integer 默认先创建并缓存 -128 ~ 127 之间数的 Integer 对象,当调用 valueOf 时如果参数在 -128 ~ 127 之间则计算下标并从缓存中返回,否则创建一个新的 Integer 对象。
6,行为型模式
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。
行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。
行为型模式分为:
- 模板方法模式
- 策略模式
- 命令模式
- 职责链模式
- 状态模式
- 观察者模式
- 中介者模式
- 迭代器模式
- 访问者模式
- 备忘录模式
- 解释器模式
以上 11 种行为型模式,除了模板方法模式和解释器模式是类行为型模式,其他的全部属于对象行为型模式。
6.1 模板方法模式
6.1.1 概述
在面向对象程序设计过程中,程序员常常会遇到这种情况:设计一个系统时知道了算法所需的关键步骤,而且确定了这些步骤的执行顺序,但某些步骤的具体实现还未知,或者说某些步骤的实现与具体的环境相关。
例如,去银行办理业务一般要经过以下4个流程:取号、排队、办理具体业务、对银行工作人员进行评分等,其中取号、排队和对银行工作人员进行评分的业务对每个客户是一样的,可以在父类中实现,但是办理具体业务却因人而异,它可能是存款、取款或者转账等,可以延迟到子类中实现。
定义:
定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
6.1.2 结构
模板方法(Template Method)模式包含以下主要角色:
-
抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。
-
模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
-
基本方法:是实现算法各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三种:
-
抽象方法(Abstract Method) :一个抽象方法由抽象类声明、由其具体子类实现。
-
具体方法(Concrete Method) :一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。
-
钩子方法(Hook Method) :在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。
一般钩子方法是用于判断的逻辑方法,这类方法名一般为isXxx,返回值类型为boolean类型。
-
-
-
具体子类(Concrete Class):实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。
6.1.3 案例实现
【例】炒菜
炒菜的步骤是固定的,分为倒油、热油、倒蔬菜、倒调料品、翻炒等步骤。现通过模板方法模式来用代码模拟。类图如下:
代码如下:
1
public abstract class AbstractClass {
2
3
public final void cookProcess() {
4
//第一步:倒油
5
this.pourOil();
6
//第二步:热油
7
this.heatOil();
8
//第三步:倒蔬菜
9
this.pourVegetable();
10
//第四步:倒调味料
11
this.pourSauce();
12
//第五步:翻炒
13
this.fry();
14
}
15
16
public void pourOil() {
17
System.out.println("倒油");
18
}
19
20
//第二步:热油是一样的,所以直接实现
21
public void heatOil() {
22
System.out.println("热油");
23
}
24
25
//第三步:倒蔬菜是不一样的(一个下包菜,一个是下菜心)
26
public abstract void pourVegetable();
27
28
//第四步:倒调味料是不一样
29
public abstract void pourSauce();
30
31
32
//第五步:翻炒是一样的,所以直接实现
33
public void fry(){
34
System.out.println("炒啊炒啊炒到熟啊");
35
}
36
}
37
38
public class ConcreteClass_BaoCai extends AbstractClass {
39
40
@Override
41
public void pourVegetable() {
42
System.out.println("下锅的蔬菜是包菜");
43
}
44
45
@Override
46
public void pourSauce() {
47
System.out.println("下锅的酱料是辣椒");
48
}
49
}
50
51
public class ConcreteClass_CaiXin extends AbstractClass {
52
@Override
53
public void pourVegetable() {
54
System.out.println("下锅的蔬菜是菜心");
55
}
56
57
@Override
58
public void pourSauce() {
59
System.out.println("下锅的酱料是蒜蓉");
60
}
61
}
62
63
public class Client {
64
public static void main(String[] args) {
65
//炒手撕包菜
66
ConcreteClass_BaoCai baoCai = new ConcreteClass_BaoCai();
67
baoCai.cookProcess();
68
69
//炒蒜蓉菜心
70
ConcreteClass_CaiXin caiXin = new ConcreteClass_CaiXin();
71
caiXin.cookProcess();
72
}
73
}
注意:为防止恶意操作,一般模板方法都加上 final 关键词。
6.1.3 优缺点
优点:
-
提高代码复用性
将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中。
-
实现了反向控制
通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制 ,并符合“开闭原则”。
缺点:
- 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象。
- 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。
6.1.4 适用场景
- 算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
- 需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。
6.1.5 JDK源码解析
InputStream类就使用了模板方法模式。在InputStream类中定义了多个 read() 方法,如下:
1
public abstract class InputStream implements Closeable {
2
//抽象方法,要求子类必须重写
3
public abstract int read() throws IOException;
4
5
public int read(byte b[]) throws IOException {
6
return read(b, 0, b.length);
7
}
8
9
public int read(byte b[], int off, int len) throws IOException {
10
if (b == null) {
11
throw new NullPointerException();
12
} else if (off < 0 || len < 0 || len > b.length - off) {
13
throw new IndexOutOfBoundsException();
14
} else if (len == 0) {
15
return 0;
16
}
17
18
int c = read(); //调用了无参的read方法,该方法是每次读取一个字节数据
19
if (c == -1) {
20
return -1;
21
}
22
b[off] = (byte)c;
23
24
int i = 1;
25
try {
26
for (; i < len ; i++) {
27
c = read();
28
if (c == -1) {
29
break;
30
}
31
b[off + i] = (byte)c;
32
}
33
} catch (IOException ee) {
34
}
35
return i;
36
}
37
}
从上面代码可以看到,无参的 read() 方法是抽象方法,要求子类必须实现。而 read(byte b[]) 方法调用了 read(byte b[], int off, int len) 方法,所以在此处重点看的方法是带三个参数的方法。
在该方法中第18行、27行,可以看到调用了无参的抽象的 read() 方法。
总结如下: 在InputStream父类中已经定义好了读取一个字节数组数据的方法是每次读取一个字节,并将其存储到数组的第一个索引位置,读取len个字节数据。具体如何读取一个字节数据呢?由子类实现。
6.2 策略模式
6.2.1 概述
先看下面的图片,我们去旅游选择出行模式有很多种,可以骑自行车、可以坐汽车、可以坐火车、可以坐飞机。
作为一个程序猿,开发需要选择一款开发工具,当然可以进行代码开发的工具有很多,可以选择Idea进行开发,也可以使用eclipse进行开发,也可以使用其他的一些开发工具。
定义:
该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
6.2.2 结构
策略模式的主要角色如下:
- 抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
- 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。
- 环境(Context)类:持有一个策略类的引用,最终给客户端调用。
6.2.3 案例实现
【例】促销活动
一家百货公司在定年度的促销活动。针对不同的节日(春节、中秋节、圣诞节)推出不同的促销活动,由促销员将促销活动展示给客户。类图如下:
代码如下:
定义百货公司所有促销活动的共同接口
1
public interface Strategy {
2
void show();
3
}
定义具体策略角色(Concrete Strategy):每个节日具体的促销活动
1
//为春节准备的促销活动A
2
public class StrategyA implements Strategy {
3
4
public void show() {
5
System.out.println("买一送一");
6
}
7
}
8
9
//为中秋准备的促销活动B
10
public class StrategyB implements Strategy {
11
12
public void show() {
13
System.out.println("满200元减50元");
14
}
15
}
16
17
//为圣诞准备的促销活动C
18
public class StrategyC implements Strategy {
19
20
public void show() {
21
System.out.println("满1000元加一元换购任意200元以下商品");
22
}
23
}
定义环境角色(Context):用于连接上下文,即把促销活动推销给客户,这里可以理解为销售员
1
public class SalesMan {
2
//持有抽象策略角色的引用
3
private Strategy strategy;
4
5
public SalesMan(Strategy strategy) {
6
this.strategy = strategy;
7
}
8
9
//向客户展示促销活动
10
public void salesManShow(){
11
strategy.show();
12
}
13
}
6.2.4 优缺点
1,优点:
-
策略类之间可以自由切换
由于策略类都实现同一个接口,所以使它们之间可以自由切换。
-
易于扩展
增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合“开闭原则“
-
避免使用多重条件选择语句(if else),充分体现面向对象设计思想。
2,缺点:
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
- 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。
6.2.5 使用场景
- 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中。
- 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句。
- 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时。
- 系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏与算法相关的数据结构。
- 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。
6.2.6 JDK源码解析
Comparator 中的策略模式。在Arrays类中有一个 sort() 方法,如下:
1
public class Arrays{
2
public static <T> void sort(T[] a, Comparator<? super T> c) {
3
if (c == null) {
4
sort(a);
5
} else {
6
if (LegacyMergeSort.userRequested)
7
legacyMergeSort(a, c);
8
else
9
TimSort.sort(a, 0, a.length, c, null, 0, 0);
10
}
11
}
12
}
Arrays就是一个环境角色类,这个sort方法可以传一个新策略让Arrays根据这个策略来进行排序。就比如下面的测试类。
1
public class demo {
2
public static void main(String[] args) {
3
4
Integer[] data = {12, 2, 3, 2, 4, 5, 1};
5
// 实现降序排序
6
Arrays.sort(data, new Comparator<Integer>() {
7
public int compare(Integer o1, Integer o2) {
8
return o2 - o1;
9
}
10
});
11
System.out.println(Arrays.toString(data)); //[12, 5, 4, 3, 2, 2, 1]
12
}
13
}
这里我们在调用Arrays的sort方法时,第二个参数传递的是Comparator接口的子实现类对象。所以Comparator充当的是抽象策略角色,而具体的子实现类充当的是具体策略角色。环境角色类(Arrays)应该持有抽象策略的引用来调用。那么,Arrays类的sort方法到底有没有使用Comparator子实现类中的 compare() 方法吗?让我们继续查看TimSort类的 sort() 方法,代码如下:
1
class TimSort<T> {
2
static <T> void sort(T[] a, int lo, int hi, Comparator<? super T> c,
3
T[] work, int workBase, int workLen) {
4
assert c != null && a != null && lo >= 0 && lo <= hi && hi <= a.length;
5
6
int nRemaining = hi - lo;
7
if (nRemaining < 2)
8
return; // Arrays of size 0 and 1 are always sorted
9
10
// If array is small, do a "mini-TimSort" with no merges
11
if (nRemaining < MIN_MERGE) {
12
int initRunLen = countRunAndMakeAscending(a, lo, hi, c);
13
binarySort(a, lo, hi, lo + initRunLen, c);
14
return;
15
}
16
...
17
}
18
19
private static <T> int countRunAndMakeAscending(T[] a, int lo, int hi,Comparator<? super T> c) {
20
assert lo < hi;
21
int runHi = lo + 1;
22
if (runHi == hi)
23
return 1;
24
25
// Find end of run, and reverse range if descending
26
if (c.compare(a[runHi++], a[lo]) < 0) { // Descending
27
while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0)
28
runHi++;
29
reverseRange(a, lo, runHi);
30
} else { // Ascending
31
while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0)
32
runHi++;
33
}
34
35
return runHi - lo;
36
}
37
}
上面的代码中最终会跑到 countRunAndMakeAscending() 这个方法中。我们可以看见,只用了compare方法,所以在调用Arrays.sort方法只传具体compare重写方法的类对象就行,这也是Comparator接口中必须要子类实现的一个方法。
6.3 命令模式
6.3.1 概述
日常生活中,我们出去吃饭都会遇到下面的场景。
定义:
将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行存储、传递、调用、增加与管理。
6.3.2 结构
命令模式包含以下主要角色:
- 抽象命令类(Command)角色: 定义命令的接口,声明执行的方法。
- 具体命令(Concrete Command)角色:具体的命令,实现命令接口;通常会持有接收者,并调用接收者的功能来完成命令要执行的操作。
- 实现者/接收者(Receiver)角色: 接收者,真正执行命令的对象。任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。
- 调用者/请求者(Invoker)角色: 要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。这个是客户端真正触发命令并要求命令执行相应操作的地方,也就是说相当于使用命令对象的入口。
6.3.3 案例实现
将上面的案例用代码实现,那我们就需要分析命令模式的角色在该案例中由谁来充当。
服务员: 就是调用者角色,由她来发起命令。
资深大厨: 就是接收者角色,真正命令执行的对象。
订单: 命令中包含订单。
类图如下:
代码如下:
1
public interface Command {
2
void execute();//只需要定义一个统一的执行方法
3
}
4
5
public class OrderCommand implements Command {
6
7
//持有接受者对象
8
private SeniorChef receiver;
9
private Order order;
10
11
public OrderCommand(SeniorChef receiver, Order order){
12
this.receiver = receiver;
13
this.order = order;
14
}
15
16
public void execute() {
17
System.out.println(order.getDiningTable() + "桌的订单:");
18
Set<String> keys = order.getFoodDic().keySet();
19
for (String key : keys) {
20
receiver.makeFood(order.getFoodDic().get(key),key);
21
}
22
23
try {
24
Thread.sleep(100);//停顿一下 模拟做饭的过程
25
} catch (InterruptedException e) {
26
e.printStackTrace();
27
}
28
29
30
System.out.println(order.getDiningTable() + "桌的饭弄好了");
31
}
32
}
33
34
public class Order {
35
// 餐桌号码
36
private int diningTable;
37
38
// 用来存储餐名并记录份数
39
private Map<String, Integer> foodDic = new HashMap<String, Integer>();
40
41
public int getDiningTable() {
42
return diningTable;
43
}
44
45
public void setDiningTable(int diningTable) {
46
this.diningTable = diningTable;
47
}
48
49
public Map<String, Integer> getFoodDic() {
50
return foodDic;
51
}
52
53
public void setFoodDic(String name, int num) {
54
foodDic.put(name,num);
55
}
56
}
57
58
// 资深大厨类 是命令的Receiver
59
public class SeniorChef {
60
61
public void makeFood(int num,String foodName) {
62
System.out.println(num + "份" + foodName);
63
}
64
}
65
66
public class Waitor {
67
68
private ArrayList<Command> commands;//可以持有很多的命令对象
69
70
public Waitor() {
71
commands = new ArrayList();
72
}
73
74
public void setCommand(Command cmd){
75
commands.add(cmd);
76
}
77
78
// 发出命令 喊 订单来了,厨师开始执行
79
public void orderUp() {
80
System.out.println("美女服务员:叮咚,大厨,新订单来了.......");
81
for (int i = 0; i < commands.size(); i++) {
82
Command cmd = commands.get(i);
83
if (cmd != null) {
84
cmd.execute();
85
}
86
}
87
}
88
}
89
90
public class Client {
91
public static void main(String[] args) {
92
//创建2个order
93
Order order1 = new Order();
94
order1.setDiningTable(1);
95
order1.getFoodDic().put("西红柿鸡蛋面",1);
96
order1.getFoodDic().put("小杯可乐",2);
97
98
Order order2 = new Order();
99
order2.setDiningTable(3);
100
order2.getFoodDic().put("尖椒肉丝盖饭",1);
101
order2.getFoodDic().put("小杯雪碧",1);
102
103
//创建接收者
104
SeniorChef receiver=new SeniorChef();
105
//将订单和接收者封装成命令对象
106
OrderCommand cmd1 = new OrderCommand(receiver, order1);
107
OrderCommand cmd2 = new OrderCommand(receiver, order2);
108
//创建调用者 waitor
109
Waitor invoker = new Waitor();
110
invoker.setCommand(cmd1);
111
invoker.setCommand(cmd2);
112
113
//将订单带到柜台 并向厨师喊 订单来了
114
invoker.orderUp();
115
}
116
}
6.3.4 优缺点
1,优点:
- 降低系统的耦合度。命令模式能将调用操作的对象与实现该操作的对象解耦。
- 增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,它满足“开闭原则”,对扩展比较灵活。
- 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
- 方便实现 Undo 和 Redo 操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。
2,缺点:
- 使用命令模式可能会导致某些系统有过多的具体命令类。
- 系统结构更加复杂。
6.3.5 使用场景
- 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
- 系统需要在不同的时间指定请求、将请求排队和执行请求。
- 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
6.3.6 JDK源码解析
Runable是一个典型命令模式,Runnable担当命令的角色,Thread充当的是调用者,start方法就是其执行方法
1
//命令接口(抽象命令角色)
2
public interface Runnable {
3
public abstract void run();
4
}
5
6
//调用者
7
public class Thread implements Runnable {
8
private Runnable target;
9
10
public synchronized void start() {
11
if (threadStatus != 0)
12
throw new IllegalThreadStateException();
13
14
group.add(this);
15
16
boolean started = false;
17
try {
18
start0();
19
started = true;
20
} finally {
21
try {
22
if (!started) {
23
group.threadStartFailed(this);
24
}
25
} catch (Throwable ignore) {
26
}
27
}
28
}
29
30
private native void start0();
31
}
会调用一个native方法start0(),调用系统方法,开启一个线程。而接收者是对程序员开放的,可以自己定义接收者。
1
/**
2
* jdk Runnable 命令模式
3
* TurnOffThread : 属于具体
4
*/
5
public class TurnOffThread implements Runnable{
6
private Receiver receiver;
7
8
public TurnOffThread(Receiver receiver) {
9
this.receiver = receiver;
10
}
11
public void run() {
12
receiver.turnOFF();
13
}
14
}
1
/**
2
* 测试类
3
*/
4
public class Demo {
5
public static void main(String[] args) {
6
Receiver receiver = new Receiver();
7
TurnOffThread turnOffThread = new TurnOffThread(receiver);
8
Thread thread = new Thread(turnOffThread);
9
thread.start();
10
}
11
}
6.4 责任链模式
6.4.1 概述
在现实生活中,常常会出现这样的事例:一个请求有多个对象可以处理,但每个对象的处理条件或权限不同。例如,公司员工请假,可批假的领导有部门负责人、副总经理、总经理等,但每个领导能批准的天数不同,员工必须根据自己要请假的天数去找不同的领导签名,也就是说员工必须记住每个领导的姓名、电话和地址等信息,这增加了难度。这样的例子还有很多,如找领导出差报销、生活中的“击鼓传花”游戏等。
定义:
又名职责链模式,为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
6.4.2 结构
职责链模式主要包含以下角色:
- 抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。
- 具体处理者(Concrete Handler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。
- 客户类(Client)角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。
6.4.3 案例实现
现需要开发一个请假流程控制系统。请假一天以下的假只需要小组长同意即可;请假1天到3天的假还需要部门经理同意;请求3天到7天还需要总经理同意才行。
类图如下:
代码如下:
1
//请假条
2
public class LeaveRequest {
3
private String name;//姓名
4
private int num;//请假天数
5
private String content;//请假内容
6
7
public LeaveRequest(String name, int num, String content) {
8
this.name = name;
9
this.num = num;
10
this.content = content;
11
}
12
13
public String getName() {
14
return name;
15
}
16
17
public int getNum() {
18
return num;
19
}
20
21
public String getContent() {
22
return content;
23
}
24
}
25
26
//处理者抽象类
27
public abstract class Handler {
28
protected final static int NUM_ONE = 1;
29
protected final static int NUM_THREE = 3;
30
protected final static int NUM_SEVEN = 7;
31
32
//该领导处理的请假天数区间
33
private int numStart;
34
private int numEnd;
35
36
//领导上面还有领导
37
private Handler nextHandler;
38
39
//设置请假天数范围 上不封顶
40
public Handler(int numStart) {
41
this.numStart = numStart;
42
}
43
44
//设置请假天数范围
45
public Handler(int numStart, int numEnd) {
46
this.numStart = numStart;
47
this.numEnd = numEnd;
48
}
49
50
//设置上级领导
51
public void setNextHandler(Handler nextHandler){
52
this.nextHandler = nextHandler;
53
}
54
55
//提交请假条
56
public final void submit(LeaveRequest leave){
57
if(0 == this.numStart){
58
return;
59
}
60
61
//如果请假天数达到该领导者的处理要求
62
if(leave.getNum() >= this.numStart){
63
this.handleLeave(leave);
64
65
//如果还有上级 并且请假天数超过了当前领导的处理范围
66
if(null != this.nextHandler && leave.getNum() > numEnd){
67
this.nextHandler.submit(leave);//继续提交
68
} else {
69
System.out.println("流程结束");
70
}
71
}
72
}
73
74
//各级领导处理请假条方法
75
protected abstract void handleLeave(LeaveRequest leave);
76
}
77
78
//小组长
79
public class GroupLeader extends Handler {
80
public GroupLeader() {
81
//小组长处理1-3天的请假
82
super(Handler.NUM_ONE, Handler.NUM_THREE);
83
}
84
85
@Override
86
protected void handleLeave(LeaveRequest leave) {
87
System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "。");
88
System.out.println("小组长审批:同意。");
89
}
90
}
91
92
//部门经理
93
public class Manager extends Handler {
94
public Manager() {
95
//部门经理处理3-7天的请假
96
super(Handler.NUM_THREE, Handler.NUM_SEVEN);
97
}
98
99
@Override
100
protected void handleLeave(LeaveRequest leave) {
101
System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "。");
102
System.out.println("部门经理审批:同意。");
103
}
104
}
105
106
//总经理
107
public class GeneralManager extends Handler {
108
public GeneralManager() {
109
//部门经理处理7天以上的请假
110
super(Handler.NUM_SEVEN);
111
}
112
113
@Override
114
protected void handleLeave(LeaveRequest leave) {
115
System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "。");
116
System.out.println("总经理审批:同意。");
117
}
118
}
119
120
//测试类
121
public class Client {
122
public static void main(String[] args) {
123
//请假条来一张
124
LeaveRequest leave = new LeaveRequest("小花",5,"身体不适");
125
126
//各位领导
127
GroupLeader groupLeader = new GroupLeader();
128
Manager manager = new Manager();
129
GeneralManager generalManager = new GeneralManager();
130
131
groupLeader.setNextHandler(manager);//小组长的领导是部门经理
132
manager.setNextHandler(generalManager);//部门经理的领导是总经理
133
//之所以在这里设置上级领导,是因为可以根据实际需求来更改设置,如果实战中上级领导人都是固定的,则可以移到领导实现类中。
134
135
//提交申请
136
groupLeader.submit(leave);
137
}
138
}
6.4.4 优缺点
1,优点:
-
降低了对象之间的耦合度
该模式降低了请求发送者和接收者的耦合度。
-
增强了系统的可扩展性
可以根据需要增加新的请求处理类,满足开闭原则。
-
增强了给对象指派职责的灵活性
当工作流程发生变化,可以动态地改变链内的成员或者修改它们的次序,也可动态地新增或者删除责任。
-
责任链简化了对象之间的连接
一个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。
-
责任分担
每个类只需要处理自己该处理的工作,不能处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。
2,缺点:
- 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。
- 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
- 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。
6.4.5 源码解析
在javaWeb应用开发中,FilterChain是职责链(过滤器)模式的典型应用,以下是Filter的模拟实现分析:
-
模拟web请求Request以及web响应Response
1 public interface Request{ 2 3 } 4 5 public interface Response{ 6 7 } -
模拟web过滤器Filter
1 public interface Filter { 2 public void doFilter(Request req,Response res,FilterChain c); 3 } -
模拟实现具体过滤器
1 public class FirstFilter implements Filter { 2 @Override 3 public void doFilter(Request request, Response response, FilterChain chain) { 4 5 System.out.println("过滤器1 前置处理"); 6 7 // 先执行所有request再倒序执行所有response 8 chain.doFilter(request, response); 9 10 System.out.println("过滤器1 后置处理"); 11 } 12 } 13 14 public class SecondFilter implements Filter { 15 @Override 16 public void doFilter(Request request, Response response, FilterChain chain) { 17 18 System.out.println("过滤器2 前置处理"); 19 20 // 先执行所有request再倒序执行所有response 21 chain.doFilter(request, response); 22 23 System.out.println("过滤器2 后置处理"); 24 } 25 } -
模拟实现过滤器链FilterChain
1 public class FilterChain { 2 3 private List<Filter> filters = new ArrayList<Filter>(); 4 5 private int index = 0; 6 7 // 链式调用 8 public FilterChain addFilter(Filter filter) { 9 this.filters.add(filter); 10 return this; 11 } 12 13 public void doFilter(Request request, Response response) { 14 if (index == filters.size()) { 15 return; 16 } 17 Filter filter = filters.get(index); 18 index++; 19 filter.doFilter(request, response, this); 20 } 21 } -
测试类
1 public class Client { 2 public static void main(String[] args) { 3 Request req = null; 4 Response res = null ; 5 6 FilterChain filterChain = new FilterChain(); 7 filterChain.addFilter(new FirstFilter()).addFilter(new SecondFilter()); 8 filterChain.doFilter(req,res); 9 } 10 }