适配器模式

思维导图

大概流程:

代码实现

这里情景是 ChinaLapTop 需求 Electric220V 但是没有,只有现成的 Electric110V,那么我们就建立一个适配器 Adapter,将被适配者 Electric110V 适配为 Electric220V。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class UseComputer {
public static void main(String[] args) {
ChinaLapTop laptop=new ChinaLapTop();
// 这里模拟从中国带去美国的笔记本,需要 220V 的电源(ChinaElectric)
// 但是没有,只有 110V 的电源(AmericaElectric)

// 使用适配器将已有的资源转化为需要的资源
Electric110V electric=new Electric110V();
Adapter adapter = new Adapter(electric);
laptop.PowerOn(adapter);
}
}

// 我们需求的 ChinaElectric
interface Electric220V {
int getPower();
}

// 适配器转化结果为 ChinaElectric
class Adapter implements Electric220V {
private Electric110V USelc;

public Adapter(Electric110V USelc) {
this.USelc = USelc;
}

@Override
public int getPower() {
return USelc.getPower()*2;
}
}

// 已有资源,被适配者 Adaptee
class Electric110V {

public int getPower() {
return 110;
}
}

// 资源需求者
class ChinaLapTop {

public ChinaLapTop() {
System.out.println("I hava a China LapTop!");
}

public void PowerOn(Electric220V electric) {
electric.getPower();
System.out.println("Power on succeed!");

}
}

桥接模式

概述

参考文章:设计模式 – 桥接模式(Bridge)

桥接模式就是为了解决因为排列组合事物所有的属性而导致子类爆炸的情况。

比如汽车,汽车的车型有:载货汽车,牵引车,客车,轿车等等,汽车的发动机有:柴油发动机,汽油发动机,电动发动机。那么我们如果要具体的汽车,就需要把这些汽车的组合都写成类,一共就要 4*3=12 种。而且一旦有新的动力来源或者车型增加,那么增加的子类就更多了。为了解决这个问题,我们可以将这些属性抽象(从众多的事物中抽取出共同的、本质性的特征)为不同的维度,比如汽车里面的车型为一个维度,发动机类型为一个维度。然后通过组合两个维度来顶替一个具体的实现类来使用,这样就只需要 4+3=7 个类即可,将复杂度从 m*n 降到了 m+n 。

如何实现两个不同维度的组合呢?这就要说桥接模式了,用它来将两个不同的维度联系(桥接)起来。首先我们将事物属性抽象为不同的维度之后,选择其中一个维度作为抽象维度,剩下的其他维度作为实现维度。这里的选择没有具体的要求,根据取舍来选择。我们将上面的汽车属性抽象为维度:车型维度和发动机维度。然后选取车型作为抽象维度,发动机作为实现维度。

实现维度的每一个类都需要具体代码实现自己的功能,并让抽象维度去调用(桥接),这样来实现维度之间的结合。比如实现维度的所有类都需要用实际代码实现 getPower() 方法,不同的类实现不同,有的是通过烧柴油,有的是通过烧汽油。而抽象维度内部定义的统一方法的实现则是通过调用实现维度实现的方法完成的。比如抽象维度里面的越野车,里面的 Drive() 方法获取动力就是依靠实现类的方法 getPower()

因此,这里的抽象维度的 抽象 就仅仅是方法内部的 一部分 需要其他人实现,是特殊意义上的抽象类和抽象方法(这个方法内部全都需要他人实现)。和 从众多的事物中抽取出共同的、本质性的特征 这个概念关系无关。

桥接的对象是两个维度,其中必定有一个维度为抽象维度,另外一个维度为实现维度。桥接之后的整体可以作为一个新的实现维度,因为不需要调用其他外部实现维度就可以实现自己的方法,所以整体可以作为新的实现维度给抽象维度调用。

因此,拥有复杂属性的事物,就可以用桥接组合来实现。比如一个抽象维度建立多个桥接到实现维度上,而实现维度也可以拆分为另一个抽象维度和实现维度的整体(发动机维度)。比如下图。

实例

在 Java 中的 JDBC 编程中就用到了桥接模式,实现数据库的驱动有很多,不可能让我们写的程序对每一种数据库都搞一个版本,那么我们可以将我们的程序和数据库驱动分离为两个维度,我们持有数据库驱动的接口,调用接口里面的方法来实现我们程序(抽象维度)。而实现维度为满足驱动接口的不同数据库的驱动。

当然这里实际的 JDBC 比桥接还多了一个设计模式,那就是抽象类获取实现类实例的时候,用到了工厂方法模式,获取实例交给工厂 DirverManager 来实现。

思维导图

UML 图

代码实现

对于桥接模型的实际实现代码,有两种不同的方式,之间的区别在于如何获取实现类的实例,分为为 组合 和 聚合。

组合实例:内部持有的实例是内部直接实例化出来(通过传入参数判断实例化对象)的,一旦组合类销毁,组合的实例会因为没有引用而销毁。

聚合实例:内部持有的实例是外部实例化(自己控制实例化对象)再传进来的,如果聚合类销毁,可能并不会影响聚合的实例,取决于外部是否还有引用指向此实例。

我们用会随着聚合类一起销毁的聚合方法来实现桥接吧。

下面实现手机抽象类和若干零部件的实现类之间的桥接。手机作为抽象类有小米子类,华为子类,桥接屏幕、和电池。屏幕又作为一个整体分为屏幕材质抽象类和分辨率抽象类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
public class Bridging {
public static void main(String[] args) {
Phone RedmiK30s = new MiPhone(new B5000mah(), new LCD(new FHD()));
Phone HuaweiMate40Pro = new HuaweiPhone(new B4400mah(),new OLED(new QHD()));
RedmiK30s.LightUp();
HuaweiMate40Pro.LightUp();
}
}

// 抽象类手机
abstract class Phone {

protected Bettary bettary;
protected Screen screen;

public Phone(Bettary bettary, Screen screen) {
this.bettary = bettary;
this.screen = screen;
}

public abstract void LightUp();
}

// 抽象子类小米手机
class MiPhone extends Phone {

public MiPhone(Bettary bettary, Screen screen) {
super(bettary, screen);
}

@Override //小米手机业务代码
public void LightUp() {
System.out.println("The Mi Phone is trying LightUp.");
int power = bettary.GetPower();
screen.returnLight(power);
}
}

// 抽象子类华为手机
class HuaweiPhone extends Phone {

public HuaweiPhone(Bettary bettary, Screen screen) {
super(bettary, screen);
}

@Override // 华为手机业务代码
public void LightUp() {
System.out.println("The HuaweiPhone is trying LightUp.");
int power = bettary.GetPower();
screen.returnLight(power);
}
}

// 单独来说作为抽象类,和它的实现类 Resolution 一起就成了 Phone 的实现类了
abstract class Screen {

protected Resolution resolution;

public Screen(Resolution resolution) {
this.resolution = resolution;
}

public abstract void returnLight(int power);
}

class OLED extends Screen {

public OLED(Resolution resolution) {
super(resolution);
}

@Override
public void returnLight(int power) {
int pixel = resolution.retrunPixel();
System.out.println("Using LCD Screen with "+pixel+" pixels,powered by "+power+"mah bettary");
}
}

class LCD extends Screen {

public LCD(Resolution resolution) {
super(resolution);
}

@Override
public void returnLight(int power) {
int pixel = resolution.retrunPixel();
System.out.println("Using LCD Screen with "+pixel+" pixels,powered by "+power+"mah bettary");
}
}

// Screen 的实现类接口
interface Resolution {
int retrunPixel();
}

class QHD implements Resolution {

@Override
public int retrunPixel() {
return 2772*1344;
}
}

class FHD implements Resolution {

@Override
public int retrunPixel() {
return 2400*1080;
}
}

// Phone 的其中一个实现类接口
interface Bettary {
int GetPower();
}

class B4400mah implements Bettary {

@Override
public int GetPower() {
return 4400;
}
}

class B5000mah implements Bettary {

@Override
public int GetPower() {
return 5000;
}
}

代理模式

代理模式思维导图

很水的思维导图

静态代理阐述

下面的描述是对以前学习代理的时候,留下来的笔记的改正版。

当我们调用以前写过的类去完成一些任务的时候,发现这个类没有办法满足我们的需求,而需要添加一些功能。如果要去修改这些代码,不但不符合开闭原则,还有可能因为修改代码让其他调用这个类的地方出现错误。在调用者这里完成需求的话,又会让调用者变得极其复杂,不符合单一职责原则。这个时候,我们就可以再新建一个类,作为调用者的代理类,在这个类里面添加拓展功能的实现代码,并代替调用者调用目标类的方法。这样这个代理类就代替我们实现了所有的业务需求。

通过建立一个代理,来帮助我们和目标类交互并处理结果,拓展业务的代码交给代理类即可。

这就是静态代理: 代理类 = 目标类 + 增强代码

preview

我们直接调用代理类即可,代理类会代替我们访问目标类并完善交互结果。

UML 图

静态代理代码实现

这里模拟的情景为买手机,买手机我们可以直接去手机工厂购买,但是这对于普通消费者来说,还是太不友好了。于是这里引入一个代理类:手机商店,然消费者去手机商店买。而手机商店会去代替消费者去手机工厂购买,并且还提供了额外的一些服务,如咨询机型服务,砍价服务,赠送礼品服务等等。这里消费者就是调用者,而手机商店就是消费者的代理类,代替消费者和目标类(手机工厂)交互,并完善交互结果。

手机工厂及其接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
interface PhoneFactory {
String GetPhone(int Money,String Model);
}

class MiPhoneFactory implements PhoneFactory {

@Override
public String GetPhone(int Money, String Model) {
switch (Model){
case "Redmi K30s Ultra" -> {
if(Money == 2200) return "Redmi K30s Ultra";
else return null;
}

case "Mi11" -> {
if(Money == 3500) return "Mi11";
else return null;
}

case "Redmi k40" -> {
if(Money == 1700) return "Redmi K40";
else return null;
}

default -> {
return null;
}
}
}
}

class HuaweiPhoneFactory implements PhoneFactory {

@Override
public String GetPhone(int Money, String Model) {
switch (Model){
case "Huawei P40" -> {
if(Money == 3000) return "Huawei P40";
else return null;
}

case "Huawei Mate40" -> {
if(Money == 4100) return "Huawei Mate40";
else return null;
}

case "Huawei Mate40Pro" -> {
if(Money == 5500) return "Huawei Mate40Pro";
else return null;
}

default -> {
return null;
}
}
}
}

代理类手机商店,及其包括的一些额外服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class PhoneShopProxy {
private PhoneFactory pf;
private boolean Coupon=false;

public String Consult(String Band,int AllMoney) {
String Phone=null;
if(Band.equals("XiaoMi")) {
if(Coupon) AllMoney+=100;
this.pf=new MiPhoneFactory();
if(AllMoney>3999) Phone="Mi11";
else if(AllMoney>=2599) Phone="Redmi K30s Ultra";
else if(AllMoney>=1999) Phone="Redmi k40";
else Phone=null;

if(Phone==null) System.out.println("请多带些钱来把!");
else System.out.println("我们建议你购买:"+Phone);

} else if(Band.equals("Huawei")) {
if(Coupon) AllMoney+=100;
this.pf=new HuaweiPhoneFactory();
if(AllMoney>5999) Phone="Huawei Mate40Pro";
else if(AllMoney>=4999) Phone="Huawei Mate40";
else if(AllMoney>=3999) Phone="Huawei P40";
else Phone=null;

if(Phone==null) System.out.println("钱不够呢,请多带些钱来把!");
else System.out.println("我们建议你购买:"+Phone);

} else {
System.out.println("这里没有你想要的手机呢");
}
return Phone;
}

public void Bargain() {
this.Coupon=true;
System.out.println("我们这已经是成本价了,赔钱给你便宜 100 块吧。");
}

public String Gift() {
return "充电宝,贴膜,手机壳,耳机,电饭煲";
}

public void BuyPhone(String PhoneName) {
String Phone=null;
switch (PhoneName) {
case "Mi11" -> {
Phone = pf.GetPhone(3500, PhoneName);
}
case "Redmi K30s Ultra" -> {
Phone = pf.GetPhone(2200, PhoneName);
}
case "Redmi k40" -> {
Phone = pf.GetPhone(1700, PhoneName);
}
case "Huawei P40" -> {
Phone = pf.GetPhone(3000, PhoneName);
}
case "Huawei Mate40" -> {
Phone = pf.GetPhone(4000, PhoneName);
}
case "Huawei Mate40Pro" -> {
Phone = pf.GetPhone(5000, PhoneName);
}
}
if(Phone==null) System.out.println("购买失败,型号错误");
else System.out.println("购买成功,"+Phone+" 礼品:"+Gift());
}
}

购买手机的实际消费者(被代理类):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class BuyPhone {
public static void main(String[] args) {
PhoneShopProxy shop1=new PhoneShopProxy();
String Phone1=shop1.Consult("XiaoMi", 2000);
if(Phone1!=null) {
shop1.Bargain();
shop1.BuyPhone(Phone1);
}

System.out.println();

PhoneShopProxy shop2=new PhoneShopProxy();
String Phone2=shop2.Consult("Huawei", 2000);
if(Phone2!=null) {
shop2.Bargain();
shop2.BuyPhone(Phone2);
}
}
}

最后运行结果:

1
2
3
4
5
我们建议你购买:Redmi k40
我们这已经是成本价了,赔钱给你便宜 100 块吧。
购买成功,手机: Redmi K40 礼品:充电宝,贴膜,手机壳,耳机,电饭煲

钱不够呢,请多带些钱来把!

动态代理阐述

前面说类静态代理,说是静态代理是因为这些过程在运行的时候都是不变的,编译生成 .class 文件是在 JVM 运行之前完成的。但是动态代理的代理类,是在 JVM 运行中生成的 .class 的。

动态代理有什么好处吗,为什么在 JVM 运行中生成有什么用啊?

当然有用,它最大的用处就是在运行中生成。在静态代理中,如果我们需要对大量的目标类进行编写增强代码相似的代理类来代替消费者访问,重复的工作就太多了。于是我们想要在程序运行的时候,根据实际所对应的目标类能自动生成消费者需要的代理类,我们只用写一次代理类模板,就能直接访问并完善所有的目标类,那就太方便了。想到动态代理可以在程序运行中生成代理类,这不就是我们想要的嘛,写一个代理类模板,运行的时候依次生成所有访问目标类的代理类,这样以后修改增强代码只需要在代理类模板里修改即可。

看下图,这里静态代理和动态代理最大的区别就是多了一个中间处理方法 invoke() ,这个invoke() 里面就是用来写增强代码的地方,里面对目标类的各种交互利用反射来完成。只需要将目标类传进代理生成器,就能利用反射生成一个访问目标类的代理类,最后只要操作这个生成的代理类代替我们和目标类交互即可。修改代码只修改 invoke()

preview

为了能生成代理类,就需要有模板 InvocationHandler.invoke() ,这个是我们自己通过重写实现的,然后需要一个代理类生成器:Proxy.newProxyInstance() ,最后,只需要将模板和数据塞入代理类生成器,就能量产代理类了。

动态代理代码实现

这里将不同接口: PhoneFactory、LapTopFactory 作为目标类(也是只有动态代理才可以使用的),通过动态代理来生成代理类,供代替消费者买手机。

产品的不同接口和实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface PhoneFactory {
void GetPhone();
} // 接口一

class MiPhoneFactory implements PhoneFactory{

@Override
public void GetPhone() {
System.out.println("生产成功:小米手机");
}
}

interface LapTopFactory {
void GetLapTop();
} // 接口二

class MiLapTopFactory implements LapTopFactory {

@Override
public void GetLapTop() {
System.out.println("生产成功:小米笔记本");
}
}

生成代理类的 InvocationHandler 模板,以及生成方法 Proxy.newProxyInstance() 这里将实现模板和调用生成方法两个集成到了同一个类中。

具体的代码解释可以看以前学习反射的时候写的动态代理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class DynamicProxy implements InvocationHandler {
private Object target;

public Object getProxy(Object target) {
this.target=target;
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),this);
}

@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
Object result=null;
try {
System.out.println("正在前往工厂购买产品");
result=method.invoke(this.target, args);
System.out.println("正在将产品运往商店");
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
}

调用者使用代理类来和 target 交互,这里一定要注意交互不同的 target 的时候,如果使用写法一就需要声明不同的 DynamicProxy 啊!理由看上面的代码注释。如果想要节省资源就要用写法二。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class BuyProduct {
public static void main(String[] args) {
MiPhoneFactory pf=new MiPhoneFactory();
MiLapTopFactory lf=new MiLapTopFactory();

// 写法一:
DynamicProxy dp1=new DynamicProxy();
DynamicProxy dp2=new DynamicProxy();
PhoneFactory phoneproxy=(PhoneFactory) dp1.getProxy(pf);
LapTopFactory laptopfactory=(LapTopFactory) dp2.getProxy(lf);
phoneproxy.GetPhone();
System.out.println();
laptopfactory.GetLapTop();

// 写法二:
DynamicProxy dp=new DynamicProxy();
PhoneFactory phoneproxy=(PhoneFactory) dp.getProxy(pf);
phoneproxy.GetPhone();
System.out.println();
LapTopFactory laptopfactory=(LapTopFactory) dp.getProxy(lf);
laptopfactory.GetLapTop();



}
}

最后的输出:

1
2
3
4
5
6
7
正在前往工厂购买产品
生产成功:小米手机
正在将产品运往商店

正在前往工厂购买产品
生产成功:小米笔记本
正在将产品运往商店

记录一个小错误:

因为:在用 Proxy.newInstaceProxy() 获取代理类的时候,传入的 this 为本 DynamicProxy 实例。本实例的属性 target 的内存地址是固定的,只能保存一个 target。所以生成的代理类调用的目标类 target 的时候,是一个 DynamicProxy 对应一个 target 的。不同的目标类同时使用需要声明不同的 DynamicProxy,不同的目标类依次使用只需要声明一个 DynamicProxy。

下面表示用一个 DynamicProxy 生成器生成访问不同目标类的两种代理类,并同时使用的错误:(没错我就是刚犯!)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 错误写法:
public class BuyProduct {
public static void main(String[] args) {
MiPhoneFactory pf=new MiPhoneFactory();
MiLapTopFactory lf=new MiLapTopFactory();

DynamicProxy dp=new DynamicProxy();
PhoneFactory phoneproxy=(PhoneFactory) dp.getProxy(pf); // 在 target 属性内存中写入 pf
LapTopFactory laptopfactory=(LapTopFactory) dp.getProxy(lf); // 在 target 属性内存中 覆盖 pf 写入 lf
phoneproxy.GetPhone(); // 此时 dp 实例中 target 属性的内存中的实例为 lf,但 lf 为 LapTopFactory 没有 GEtPhone() 方法。
// 就会抛出 object is not an instance of declaring class 这样的错误,毕竟 lf 的确不是 PhoneFactory 的实例,没有 GetPhone() 方法。

System.out.println();
laptopfactory.GetLapTop();
}
}

装饰器模式

概述

装饰器模式故名思意,是对一个类进行装饰(增加小功能)。而装饰有一个特点,那就是装饰完一个物品之后,得到的结果还是该物品,可以像原来一样使用。

装饰器模式在 Java IO 的 FilterInputStream 和 FileterOutPutStream 中有使用过。请先阅读下面博文了解: Filter 模式

以 FilterInputStream 为例,这个装饰器父类是 InputStream 的子类,也就是说,对于每一种具体的不同装饰子类,也同样是 InputSream 的子类。而在父类 FilterInputSream 中,有一个叫 in 的属性类型为 InputStream,就是专门用来保存被装饰物。

然后重写 InputSream 留下的 read() 方法,重写主要是添加一些装饰的功能,比如对输入进来的数据进行 base64 解码。那么就可以用被装饰物 InputStream 本来的 read() 方法先读入数据,然后手写 base64 解码代码,最后输出到 byte[] 数组中。

装饰类像是将被装饰类包裹起来(被装饰类作为装饰类内部的一个实例),从而完成对原有功能的完善。

结构

  1. 抽象构件 (InputSream) 角色:定义一个抽象接口以规范准备接收附加功能的对象。
  2. 具体构件 (FileInputSream extends InputSream)角色:实现抽象构件,通过装饰角色为其添加一些功能。
  3. 抽象装饰 (FilterInputSream extends InputStream) 角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
  4. 具体装饰 (Base64InputStream extends FilterInputSream) 角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的功能。

UML 图

装饰器、桥接之间的异同

到了这里应该能很清楚的明白和桥接模式之间的异同点了。

相同点:

  1. 两者都是为了解决因为多个元素排列组合导致的子类爆炸的问题。
  2. 两者解决方法都是通过一个维度持有另外一个维度的实例,来实现功能的组合的。

不同点:

  1. 桥接模式的实现是高等级类持有低等级类的实例,来实现低等级功能附加到高等级身上。
  2. 装饰器模式是低等级装饰者持有高等级被装饰者,装饰者加上内部的被装饰者作为一个整体又是一个被装饰者,就像月饼包装一样,一层套一层,是包装包含月饼。

代码实现

除了 Java 的 IO 实现了装饰器模式,我们自己也可以自己实现装饰器模式,就拿饼来举个例子,我们有很多饼,河南鸡蛋灌饼,山东杂粮煎饼等等。同时可以给这些并加很多配料(装饰),如辣条,鸡蛋,香肠,等等。我们就可以用修饰器模式解决这些组合的问题。

首先写我们想要装饰的东西,饼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
interface Pancake {
void makePancake();
void addThings();
void getPancake();
}

class HenanPancake implements Pancake {

@Override
public void makePancake() {
System.out.println("开始制作鸡蛋灌饼");
System.out.println("煎好白面饼了");
}

@Override
public void addThings() {
System.out.println("正在添加鸡蛋");
System.out.println("正在添加生菜");
}

@Override
public void getPancake() {
System.out.println("正在打包鸡蛋灌饼");
System.out.println("请拿走鸡蛋灌饼");
}

}

class ShanDongPancake implements Pancake {

@Override
public void makePancake() {
System.out.println("开始制作杂粮煎饼");
System.out.println("煎好杂粮饼了");
}

@Override
public void addThings() {
System.out.println("正在添加鸡蛋");
System.out.println("正在添加生菜");
}

@Override
public void getPancake() {
System.out.println("正在打包杂粮煎饼");
System.out.println("请拿走杂粮煎饼");
}

}

然后写装饰饼的各种配料类:
写一个父类的原因除了复用代码以外,还有封装 Pancake,只暴漏指定的方法给装饰器使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
abstract class Things implements Pancake {

private Pancake pancake;

Things(Pancake pancake) {
this.pancake=pancake;
}

@Override
public void makePancake() {
pancake.makePancake();
}

@Override
public void addThings() {
pancake.addThings();
}

@Override
public void getPancake() {
pancake.getPancake();
}

}

class Egg extends Things {

Egg(Pancake pancake) {
super(pancake);
}

@Override
public void addThings() {
super.addThings();
System.out.println("正在添加鸡蛋");
}

}

class Suasage extends Things {

Suasage(Pancake pancake) {
super(pancake);
}

@Override
public void addThings() {
super.addThings();
System.out.println("正在添加香肠");
}

}

class Spicy extends Things {

Spicy(Pancake pancake) {
super(pancake);
}

@Override
public void addThings() {
super.addThings();
System.out.println("正在添加辣条");
}

}

然后消费者就可以随便添加想要的配料了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Consumer {

public static void main(String[] args) {
// 两个蛋,一包辣条,一根香肠的河南鸡蛋灌饼
Pancake Henan=new HenanPancake();
Pancake eggHenan=new Egg(Henan);
Pancake spicyEggHenan=new Spicy(eggHenan);
Pancake suasageSpicyEggHenan=new Suasage(spicyEggHenan);
suasageSpicyEggHenan.makePancake();
suasageSpicyEggHenan.addThings();
suasageSpicyEggHenan.getPancake();

System.out.println();

// 两包辣条,一根肠的山东杂粮煎饼
Pancake Shandong=new Spicy(new Spicy(new Suasage(new ShanDongPancake())));
Shandong.makePancake();
Shandong.addThings();
Shandong.getPancake();
}

}

然后输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
开始制作鸡蛋灌饼
煎好白面饼了
正在添加鸡蛋
正在添加生菜
正在添加鸡蛋
正在添加辣条
正在添加香肠
正在打包鸡蛋灌饼
请拿走鸡蛋灌饼

开始制作杂粮煎饼
煎好杂粮饼了
正在添加鸡蛋
正在添加生菜
正在添加香肠
正在添加辣条
正在添加辣条
正在打包杂粮煎饼
请拿走杂粮煎饼

外观模式

概述

外观模式其实就是将操作者和被操作对象之间,增加一个中间类,作为被操作对象的外观,来方便操作者调用。

生活中最明显的例子就是小爱捷径,对于拥有上百个智能家居的人来说,对于一个情景的控制就会很麻烦,比如一起床就需要控制拉开窗帘、打开灯、打开电饭煲热粥、打开热水器准备洗澡、略微提高空调的温度,查询今天的天气,打开语音播报的功能。

对于这种一个行为需要和大量类进行交互的情况,为了简化代码,可以将这些大量类的多个交互简化成一个交互。getUp() 一个方法帮我们完成这些复杂的类交互。对于调用者只需要调用这一个方法,就能完成所有的事情。

优缺点

优点:

  1. 就是大大简化了代码,简化了可以统一封装的大量类交互,隐藏了细节。
  2. 降低了耦合度,符合最小认知原则。

缺点:

  1. 一旦新增加操作就需要增加外观类的方法,不符合开闭原则。
  2. 因为外观类的引入增加了项目的复杂度。

享元模式

概述

享元模式其实就对象实例的缓存机制,对于一些可以重复利用的实例,我们可以将其加入到对应生成工厂的缓存里面,来提高重复利用程度。

对于比较简单的一些享元,比如 String,Byte 这些所有内容都不会变的,那么就直接去缓存查找,是否有已经存在的实例,如果有,那么直接返回,如果没有,创建一个新的实例并添加到缓存中。比如 String 对于一个字符串的不同引用来说,都会指向一个内存地址,这就是因为缓存返回的都是同一个一样的对象。而 Byte 也同样是,使用静态方法获取实例的时候,内部就使用了缓存,来共享相同的已存在实例:

1
2
3
4
public static Byte valueOf(byte b) {
final int offset = 128;
return ByteCache.cache[(int)b + offset];
}

对于一些比较复杂的享元,那就是一部分可以共享,而有一些实际操作是不一样的。比如线程池里面的线程实例是可以共享的,但是运行时候需要的 Runnable 实例是非共享的(自定义的运行内容),JDBC 连接池里面和数据库保持连接的 Connection 是可以共享的(数据库地址,账号,密码不变),而执行的 Statement 是不可以共享的。

所以你可以发现,写 Java 的程序员就很好的意识到了这些,特意的将可以资源消耗大,但是需要频繁使用的实例拆分开来,拆分为可共享部分和不可共享部分。可共享部分利用线程池、连接池来进行重复利用的资源优化,不可共享部分则(Runnable,Statement)通过外部构建并传入,或者传入参数内部构建,来组合成完整的功能。

代码实现

我们来写一个简单的享元模式的图书馆吧,而图书馆里面的图书就是可以共享的资源:

图书实例(其实不嫌麻烦可以写成接口,然后搞不同种类的书,如杂志、小说、漫画等等,不过这样的话就需要为 Book 单独建立一个生成工厂了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Book {

private String name;

Book(String name) {
System.out.println("购买图书:"+name);
this.name=name;
}

public String getName() {
return this.name;
}

public void Look() {
System.out.println("正在阅读:"+name);
}

}

用来管理图书实例的图书馆:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Library {
private Map<String,List<Book>> books=new HashMap<>();

public Book getBook(String name) {
Book book;
if(books.containsKey(name)) {
List<Book> Lbook=books.get(name);
if(Lbook.isEmpty()) {
book=new Book(name);
} else {
book=Lbook.get(0);
Lbook.remove(book);
}

} else {
books.put(name,new ArrayList<>());
book=new Book(name);
}
System.out.println("借阅图书:"+name);
return book;
}

public void backBook(Book book) {
System.out.println("归还图书:"+book.getName());
books.get(book.getName()).add(book);
}

}

读者开始借书读书:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Reader {
public static void main(String[] args) {
Library lib=new Library();

Book book1=lib.getBook("CSAPP");
System.out.println("图书编号:"+book1);
book1.Look();
lib.backBook(book1);

System.out.println();

Book book2=lib.getBook("CSAPP");
System.out.println("图书编号:"+book2);
book2.Look();
lib.backBook(book2);
}
}

最后借阅结果,发现两次借阅获得的书是同一本(同一个内存地址中的实例):

1
2
3
4
5
6
7
8
9
10
购买图书:CSAPP
借阅图书:CSAPP
图书编号:Java.VScodeProject.Book@1fe20588
正在阅读:CSAPP
归还图书:CSAPP

借阅图书:CSAPP
图书编号:Java.VScodeProject.Book@1fe20588
正在阅读:CSAPP
归还图书:CSAPP

组合模式

概述

组合模式(又叫整体-部分模式):将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。

举个例子,比如文件夹和文件的关系,文件夹就是由子文件夹和文件构成的,文件夹和文件组合在一起变成整体,文件夹和文件都可以作为部分,而且之间可以组成树形的结构模型。

而组合模式的核心就是将这些相似的东西:文件和文件夹当作一个东西处理,它们都是 File 类的实例,里面同时包含了各自的方法,使用的时候需要鉴别方法是否对当前对象可以使用。

比如一个文件的 File 类,使用 listFile() 返回的就是 null 空引用,所以缺点就是不太安全,但是优点就是方便操作,管他是文件还是文件夹,都是 File 的实例。

UML 图

代码实现

定义统一的一个接口,接口包含所有整体和部分的方法。然后整体类和部分类分别实现接口的代码,要求整体类的构造方法可以或者 add() 方法可以传入类型为接口类型(这样整体类对象和部分类对象都可以作为它的一部分)

比如文件夹系统,分为文件夹类(整体类)和文件类(部分类),一起实现同一个接口 File,这个接口定义里文件操作和文件夹操作的所有方法。而构造文件夹类的实例的时候,可以传入文件夹实例和文件实例,作为当前文件夹的内部部分。

1
具体代码就不写了,参考 Java 里面的 File 类就行了。

最后

哇哇哇,结构型模式笔记终于写完了,写到了后面连思维导图都不想搞了,主要是拖得时间太长了,里面内容也很多,而且思维导图对于这种类型的知识点来说没啥用。但是想想看自己学的时间还是太长了,所以要不停的提高学习效率啊!不然后端开发的知识点这么多,这样学下去怕不是得学到毕业……

呼~ 长呼一口气,准备开始行为型模式的学习吧,加油啊!