结构型模式

一、概述

结构型模式(Structural Pattern)关注如何将现有类或对象组织在一起形成更加强大的结构
不同的结构型模式从不同的角度来组合类和对象,他们的尽可能满足各种面向对象设计原则的同时,为类或对象的组合提供一系列巧妙的解决方案
结构型模式可以描述两种不同的东西——类与类的实例(对象)
因此结构型模式也被分为两种,分别是类结构型模式对象结构型模式

类结构型模式关心类的组合,由多个类组合成一个强大的系统,一般只存在继承和实现关系

对象结构型模式中关心类与对象的组合,通过关联关系在一个类中定义另外一个类的实例对象,如何通过该对象调用相应的方法

根据合成复用原则,在系统中尽量使用关联关系来代替继承关系,因此大部分结构型模式都是对象结构型模式

结构型模式共有七种:

设计原则名称 定义 使用频率
适配器模式
(Adapter Pattern)
将一个类的接口转换成客户希望的另一个接口。适配器模式让那些不兼容的类可以一起工作 ⭐⭐⭐⭐
桥接模式
(Bridge Pattern)
将抽象部分与它的实现部分解耦,使得两者能够独立变化 ⭐⭐⭐
组合模式
(Composite Pattern)
组合多个对象形成树形结构以表示具有部分-整体关系的层次结构。组合模式让客户端可以统一对待单个对象和组合对象 ⭐⭐⭐⭐
装饰模式
(Decorator Pattern)
动态地给一个对象增加一些额外的职责。就扩展功能而言,装饰模式提供了一种比使用子类更加灵活的代替方案 ⭐⭐⭐
外观模式
(Facade Pattern)
为子系统的一组接口提供一个统一的入口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用 ⭐⭐⭐⭐⭐
享元模式
(Flyweight Pattern)
运用共享技术有效地支持大量细粒度对象的复用
代理模式
(Proxy Pattern)
给某一个对象提供一个代理或占位符,并由代理对象来控制原对象的访问 ⭐⭐⭐⭐

二、适配器模式

算法适配。 现有一个接口DataOperation定义了排序方法sort(int[])和查找方法search(int[],int),已知类QuickSort的quickSort(int[])方法实现了快速排序算法,类BinarySearch的binarySearch(int[],int)方法实现了二分查找算法。现使用适配器模式设计一个系统,在不修改源代码的情况下将类QuickSort和类BinarySearch的方法适配到DataOperation接口中

1. 目标类

1
2
3
4
public interface DataOperation {
int[] sort(int[] a);
int search(int[] a,int b);
}

2. 适配者

2.1 BinarySearch

1
2
3
4
5
public class BinarySearch {
public int binarySearch(int[] a,int b){
return Arrays.binarySearch(a, b);
}
}

2.2 QuickSort

1
2
3
4
5
6
public class QuickSort {
public int[] quickSort(int[] a) {
Arrays.sort(a);
return a;
}
}

3. 适配器

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

private final BinarySearch binarySearch;

private final QuickSort quickSort;

public Adapter() {
binarySearch = new BinarySearch();
quickSort = new QuickSort();
}

@Override
public int[] sort(int[] a) {
return quickSort.quickSort(a);
}

@Override
public int search(int[] a, int b) {
return binarySearch.binarySearch(a, b);
}

}

4. 客户端代码

1
2
3
4
5
6
Adapter adapter = new Adapter();         
int[] a = {1,2,5,6,3,7,5,9,3,10};
System.out.println(adapter.search(a, 9));
for (int i : adapter.sort(a)) {
System.out.print(i+" ");
}

5. 优缺点和适用场景

5.1 优点

  • 将目标类和适配者类解耦,无须修改原有结构
  • 增加了类的透明性和复用性,具体业务都封装在适配者类中,并且适配者类可以重复适用
  • 灵活性和扩张性都非常好,只需要灵活更换适配器即可

5.2 缺点

由于类适配器模式的缺点较多,根据合成复用原则也适用较少,所以只总结了对象适配器模式的缺点:

  • 实现过程较为复杂,在该模式下如果适配器中需要置换适配者类的方法,那么就需要更改原代码,这样不符合开闭原则,所以需要另外建子类完成,增加系统复杂度


    5.3 适用场景

  • 系统中需要使用一些现有的类,而这些类的接口(例如方法名)不符合系统的需要,甚至没有这些类的源代码

  • 想创建一个可以重复使用的类,用于和一些彼此之间没有太大关联的类(包括在将来想引进的类)一起工作

三、桥接模式

毛笔和蜡笔是两种常见的文具,它们都归属于画笔。假如需要大、中、小 3 种型号的画笔,能够绘制 12 种不同的颜色,如果使用蜡笔,需要准备 3 × 12 = 36 支,但如果使用毛笔,只需要提供 3 种型号的毛笔,外加一个包含 12 种颜色的调色板,涉及的对象个数仅为 3 + 12 = 15,远小于 36,却能实现与 36 支蜡笔同样的功能。
请使用桥接模式实现毛笔绘制的系统结构

1. 抽象类

1.1 WritingBrush

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class WritingBrush {

protected Color color;

protected abstract String draw();

public Color getColor() {
return color;
}

public void setColor(Color color) {
this.color = color;
}

}

2. 具体抽象类

2.1 BigWritingBrush

1
2
3
4
5
6
7
public class BigWritingBrush extends WritingBrush {
@Override
protected String draw() {
return "使用大号毛笔,染上" + color.coloured() + "后绘画";
}
}

2.2 MidWritingBrush

1
2
3
4
5
6
7
public class MidWritingBrush extends WritingBrush {
@Override
protected String draw() {
return "使用中号毛笔,染上" + color.coloured() + "后绘画";
}
}

3. 实现类接口

3.1 Color

1
2
3
public interface Color {
String coloured();
}

4. 具体实现类

4.1 Red

1
2
3
4
5
6
public class Red implements Color {
@Override
public String coloured() {
return "红色";
}
}

4.2 Green

1
2
3
4
5
6
public class Green implements Color {
@Override
public String coloured() {
return "绿色";
}
}

5. 客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Client {
public static void main(String[] args) {
WritingBrush bigWritingBrush = new BigWritingBrush();

Color red = new Red();
Color green = new Green();

bigWritingBrush.setColor(red);
System.out.println(bigWritingBrush.draw());

bigWritingBrush.setColor(green);
System.out.println(bigWritingBrush.draw());
}
}

6. 优缺点和适用场景

6.1 优点

  • 解耦。分离抽象接口及其实现部分

  • 取代多层继承方案。更贴和合成复用原则

  • 提高可扩展性。在抽象和接口两个维度中进行维护时均不需要修改任何一个维度,符合开闭原则

    6.2 缺点

  • 增加系统的理解和设计难度

  • 需要正确地识别出抽象和实现两个维度

    6.3 适用场景

  • 如果一个系统需要在抽象化和具体化之间增加更多的灵活性,避免在两个层次之间建立静态的继承关系,通过调节模式可以使他们在抽象层建立一个关联关系

  • 抽象部分和实现部分可以用继承的方式独立扩张,而不互相影响,在程序运行时,可以动态地加一个抽象化子类的对象,和一个实现化子类的对象进行组合,即系统需要对抽象化对象和实现化对象进行动态耦合

  • 一个类存在两个或多个独立变化的维度,且这两个或多个维度都需要独立进行扩张

  • 对于那些不希望使用继承或因为多层继承导致系统类的个数急剧增加的系统,桥接模式尤为使用

四、组合模式

某软件公司要开发一个杀毒软件,该软件既可以对某个文件夹(Folder)杀毒,也可以对某个指定的文件(File)杀毒。该软件还可以根据各类文件的特点为不同类型的文件提供不同德杀毒方式,例如图像文件(ImageFile)和文本文件(TextFile)的杀毒方式就有所差异。
使用组合模式来设计该杀毒软件的整体框架

以下采用的是安全组合模式,还有一种透明组合模式,但安全性不高,存在无意义的方法

1. 抽象构件

1.1 KillVirus

1
2
3
public interface KillVirus {
void killVirus();
}

1.2 AbstractFile

1
2
public abstract class AbstractFile implements KillVirus {
}

2. 叶子构件

2.1 File

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

protected String name;

public File(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public void killVirus() {

}

// 文件特有的方法,而文件夹没有,比如修改扩展名
}

2.2 ImageFile

1
2
3
4
5
6
7
8
9
10
11
public class ImageFile extends File {

public ImageFile(String name) {
super(name);
}

@Override
public void killVirus() {
System.out.println("- - - - 对图片文件'" + name + "'进行杀毒");
}
}

2.3 TextFile

1
2
3
4
5
6
7
8
9
10
11
public class TextFile extends File {

public TextFile(String name) {
super(name);
}

@Override
public void killVirus() {
System.out.println("- - - - 对文本文件'" + name + "'进行杀毒");
}
}

3. 容器构件

3.1 Folder

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 Folder extends AbstractFile {

private ArrayList<AbstractFile> files = new ArrayList<>();

private String name;

public Folder(String name) {
this.name = name;
}

public void add(AbstractFile file) {
files.add(file);
}

public void remove(AbstractFile file) {
files.remove(file);
}

@Override
public void killVirus() {
System.out.println("* * * * 对文件夹'" + name + "'进行杀毒");
for (AbstractFile file : files) {
file.killVirus();
}
}
}

4. 客户端代码

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
public class Client {
public static void main(String[] args) {
Folder folder1, folder2;

File file1, file2, file3, file4;

file1 = new ImageFile("小龙女.jpg");
file2 = new ImageFile("张无忌.png");
file3 = new TextFile("葵花宝典.doc");
file4 = new TextFile("九阳真经.txt");

folder1 = new Folder("图片文件夹");
folder2 = new Folder("文本文件夹");

folder1.add(file1);
folder1.add(file2);

folder2.add(file3);
folder2.add(file4);

folder1.add(folder2);

folder1.killVirus();
}
}

5. 优缺点和适用场景

5.1 优点

  • 为树形结构的面向对象实现提供了一种灵活的解决方案,通过对叶子对象和容器对象的递归组合可以形成复杂的树形结构,但对树形结构的控制却非常简单

    5.2 缺点

  • 很难对容器中的构建类进行类型限制。比如上述例子中想针对一个文件夹只存图片文件,因为容器类和叶子类它们都源于同一个抽象层,所以较难实现

    5.3 适用场景

  • 在具有整体和部分的层次结构中希望通过一种方式忽略整体与部分的差异,并且客户端可以一致地对待它们

  • 在一个使用面向对象语言开发的系统中需要处理一个树形结构

  • 在一个系统中能够分离出叶子对象和容器对象,而且它们的类型不固定,需要增加一些新的类型

五、装饰模式

某构件库提供了大量的基本构建,如窗体、文本框、列表框等,由于在使用该构建库时用户经常要求定制一些特殊的显示效果,如带滚动条的窗体、带黑色边框的文本框、既带滚动条又带黑色边框的列表框等,因此经常需要对该构件库进行扩展以增强其功能
请使用装饰模式来设计该图形节面构件库

1. 抽象构建

1.1 Component

1
2
3
public abstract class Component {
abstract void display();
}

2. 具体构件

2.1 TextBox

1
2
3
4
5
6
public class TextBox extends Component {
@Override
void display() {
System.out.println("显示文本框");
}
}

2.2 Window

1
2
3
4
5
6
public class Window extends Component {
@Override
void display() {
System.out.println("显示窗体");
}
}

3. 抽象装饰类

3.1 ComponentDecorator

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ComponentDecorator extends Component {

private Component component;

public ComponentDecorator(Component component) {
this.component = component;
}

@Override
void display() {
component.display();
}
}

4. 具体装饰类

4.1 BlackBorderDecorator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class BlackBorderDecorator extends ComponentDecorator {

public BlackBorderDecorator(Component component) {
super(component);
}

@Override
void display() {
setBlackBorder();
super.display();
}

public void setBlackBorder() {
System.out.println("增加黑边框");
}

}

4.2 ScrollBarDecorator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ScrollBarDecorator extends ComponentDecorator {

public ScrollBarDecorator(Component component) {
super(component);
}

@Override
void display() {
setScrollBar();
super.display();
}

public void setScrollBar() {
System.out.println("增加滚动条");
}

}

5. 客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Client {
public static void main(String[] args) {
Component textBox, window;
textBox = new TextBox();
window = new Window();

textBox.display();
window.display();

ComponentDecorator blackBorderDeco, scrollBarDeco;
blackBorderDeco = new BlackBorderDecorator(textBox);
scrollBarDeco = new ScrollBarDecorator(window);

blackBorderDeco.display();
scrollBarDeco.display();
}
}

6. 优缺点和适用场景

6.1 优点

  • 对于扩展一个对象的功能,装饰模式比继承更加灵活,不会导致类的个数急剧增加

  • 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的具体装饰类,从而实现不同的行为

  • 可以对一个对象进行多次装饰,通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合,得到功能强大的对象

  • 具体构建类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构建类和具体装饰类,原有类库代码无需改变,符合开闭原则

    6.2 缺点

  • 在一定程度上会影响程序的性能。因为适用装饰模式时会产生很多小对象,势必会占用更多的系统资源

  • 系统较为复杂。虽然提供了一种比继承更加灵活、机动的解决方案,但同时也意味着更容易出错,排错更加困难

    6.3 适用场景

  • 在不影响其他对象的情况下以动态、透明的方式给单个对象添加职责

  • 当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时

而不能采用继承的方式主要有两种情况:
第一类:系统存在大量的独立的扩展,为支持每一种扩展或者扩展之间的组合将产生大量的子类,使得子类数目爆炸增长
第二类:该类被定义为不能继承类例如被 final 字段修饰

六、外观模式

在电脑主机中只需要按下主机的开机按钮,即可调用其他硬件设备的启动方法,如内存的自检(check)、CPU的运行(run)、硬盘的读取(read)、操作系统的载入(load)等,如果某一过程发生错误则电脑启动失败
使用外观模式模拟该过程

1. 外观角色

1.1 Boot

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
public class Boot {
private int flag = 1;
private Lock lock = new ReentrantLock();

private Check check = new Check();
private Load load = new Load();
private Read read = new Read();
private Run run = new Run();

private Condition checkCondition = lock.newCondition();
private Condition loadCondition = lock.newCondition();
private Condition readCondition = lock.newCondition();
private Condition runCondition = lock.newCondition();

public void check(){
lock.lock();
try {
while (flag != 1){
checkCondition.await();
}
check.check();
flag = 2;
loadCondition.signal();
} catch (InterruptedException e) {
System.out.println("内存检测失败!");
} finally {
lock.unlock();
}
}

public void load(){
lock.lock();
try {
while (flag != 2){
loadCondition.await();
}
load.load();
flag = 3;
readCondition.signal();
} catch (InterruptedException e) {
System.out.println("载入操作系统失败!");
} finally {
lock.unlock();
}
}

public void read(){
lock.lock();
try {
while (flag != 3){
readCondition.await();
}
read.read();
flag = 4;
runCondition.signal();
} catch (InterruptedException e) {
System.out.println("读取硬盘失败!");
} finally {
lock.unlock();
}
}

public void run(){
lock.lock();
try {
while (flag != 4){
runCondition.await();
}
run.run();
flag = 0;
System.out.println("启动成功!");
} catch (InterruptedException e) {
System.out.println("运行CPU!");
} finally {
lock.unlock();
}
}

public void boot(){
check();
load();
read();
run();
}
}

2. 子系统角色

2.1 Check

1
2
3
4
5
6
7
8
9
10
public class Check {
public void check(){
System.out.println("正在对内存检测");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("内存检测失败!");
}
}
}

2.2 Load

1
2
3
4
5
6
7
8
9
10
public class Load {
public void load(){
System.out.println("正在载入操作系统");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("载入操作系统失败!");
}
}
}

2.3 Read

1
2
3
4
5
6
7
8
9
10
public class Read {
public void read(){
System.out.println("正在读取硬盘");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("读取硬盘失败!");
}
}
}

2.4 Run

1
2
3
4
5
6
7
8
9
10
public class Run {
public void run(){
System.out.println("正在运行CPU");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("运行CPU失败!");
}
}
}

3. 客户端代码

1
2
3
4
public static void main(String[] args) {
Boot boot = new Boot();
boot.boot();
}

4. 优缺点和适用场景

4.1 优点

  • 对客户端屏蔽了子系统组件

  • 实现了子系统和客户端之间的松耦合关系

  • 子系统对象的修改不会影响到其他子系统对象

    4.2 缺点

  • 不能限制客户端直接适用子系统类

  • 如果设计不当,增加新的子系统类困难需要修改外观类的源代码

    4.3 适用场景

  • 当需要访问一系列复杂的子系统时,需要提供一个简单的入口、

  • 客户端与多个子系统之间存在很大的依赖性,需要减低客户端和子系统之间的耦合度

七、享元模式

开发一个围棋软件,但围棋棋盘中包含大量的黑子和白子,它们的形状、大小一模一样,只是出现的位置不同而已。如果将每一个棋子作为一个独立的对象存储在内存中,将导致该围棋软件在运行时所需要的内存空间较大,那么如何降低运行代价、提供系统性能是需要解决的一个问题
为了解决该问题,适用享元模式设计该围棋软件的系统架构

1. 抽象享元类

1.1 GoChessPiece

1
2
3
4
5
6
7
8
public abstract class GoChessPiece {

public abstract String getColor();

public void display(Coordinates coordinates) {
System.out.println("棋子颜色:" + getColor() + "棋子位置:" + coordinates.getX() + "," + coordinates.getY());
}
}

2. 具体享元类

2.1 BlackGoChessPiece

1
2
3
4
5
6
7
public class BlackGoChessPiece extends GoChessPiece {

@Override
public String getColor() {
return "黑色";
}
}

2.2 WhiteGoChessPiece

1
2
3
4
5
6
7
public class WhiteGoChessPiece extends GoChessPiece {

@Override
public String getColor() {
return "白色";
}
}

3. 享元工厂类

3.1 GoChessPieceFactory

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
public class GoChessPieceFactory {

/**
* IoDH单例模式
*/
private GoChessPieceFactory() {}

private static class HolderClass {
private final static GoChessPieceFactory instance = new GoChessPieceFactory();
}

public static GoChessPieceFactory getInstance() {
return HolderClass.instance;
}

/**
* 享元池
*/
private static ConcurrentHashMap<String, GoChessPiece> flyweightPool;

static {
flyweightPool = new ConcurrentHashMap<>();
GoChessPiece black, white;
black = new BlackGoChessPiece();
white = new WhiteGoChessPiece();

flyweightPool.put("b", black);
flyweightPool.put("w", white);
}

public static GoChessPiece getGoChessPiece(String color) {
return flyweightPool.get(color);
}
}

4. 客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Client {
public static void main(String[] args) {
GoChessPiece black1, black2, black3, white1, white2;

black1 = GoChessPieceFactory.getGoChessPiece("b");
black2 = GoChessPieceFactory.getGoChessPiece("b");
black3 = GoChessPieceFactory.getGoChessPiece("b");

white1 = GoChessPieceFactory.getGoChessPiece("w");
white2 = GoChessPieceFactory.getGoChessPiece("w");

black1.display(new Coordinates(1,2));
black2.display(new Coordinates(2,3));
black3.display(new Coordinates(3,4));

white1.display(new Coordinates(5, 4));
white2.display(new Coordinates(4, 5));

}
}

5. 优缺点和适用场景

5.1 优点

  • 减少内存中的对象数量

  • 耦合低,享元对象可以在不同的环境下适用

    5.2 缺点

  • 提高系统复杂度

    5.3 适用场景

  • 一个系统中有大量相同或者相似的对象,造成内存的大量耗费

  • 需要使用一个享元池存储需要被频繁适用的享元对象

八、代理模式

在某应用软件中需要记录业务方法的调用日志,在不修改现有业务类的基础上为每一个类提供一个日志记录代理类,在中输出日志,如在业务方法method()调用之前输出“方法method()被调用,调用时间为2012-10-10 10:10:10”

1. 抽象主题角色

1.1 UserService

1
2
3
4
5
6
public interface UserService {
void select();
void del();
void update();
void insert();
}

2. 代理主题角色

2.1 UserServiceProxy

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
public class UserServiceProxy implements InvocationHandler {

private Object target;

public UserServiceProxy(Object target) {
this.target = target;
}

public void setTarget(Object target) {
this.target = target;
}

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

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log(method.getName());
method.invoke(target, args);
return null;
}

public void log(String methodName) {
System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss:SSS")) + "调用了" + methodName);
}

}

3. 真实主题角色

3.1 UserServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class UserServiceImpl implements UserService {
@Override
public void select(){
System.out.println("执行查询用户");
}

@Override
public void del(){
System.out.println("执行删除用户");
}

@Override
public void update(){
System.out.println("执行修改用户");
}

@Override
public void insert(){
System.out.println("执行插入用户");
}

}

4. 客户端代码

1
2
3
4
5
6
public static void main(String[] args) {
UserService userService = new UserServiceImpl();
UserServiceProxy userServiceProxy = new UserServiceProxy(userService);
UserService proxy = (UserService) userServiceProxy.getProxy();
proxy.select();
}

5. 优缺点和适用场景

5.1 优点

  • 能够协调使用调用者和被调用者,降低了系统耦合度

  • 具有较好的灵活性和扩展性,可以进行 AOP

  • 此外,不同类型的代理模式具有独特的优点,具体请君网上查找

    5.2 缺点

  • 需要进行额外的工作,并且较为复杂

    5.3 适用场景

  • AOP等等

  • 好多😀,例如远程代理、缓冲代理、权限代理、智能引用代理


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!