0%

设计模式之结构型模式

结构型模式主要总结了一些类或对象组合在一起的经典结构,这些经典的结构可以解决特定应用场景的问题。

代理模式

代理模式在不改变原始类的情况下,通过添加代理类来给原始类添加附加功能。代理模式经常用在比如监控,统计,鉴权这样的非业务逻辑之中。同时RPC框架也可以看成一种代理模式,被称为远程代理,在使用RPC的时候,不需要考虑与服务器交互的细节,只关注业务逻辑即可。

代理模式通过针对每个类都创建一个代理类,具体实现代码如下所示:

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
//java 11
interface A {
void doSomething();
}

class ANeedProxy implements A {
@Override
public void doSomething() {
//TODO
}
}

class AProxy implements A {
private final A a;

AProxy(A a) {
this.a = a;
}

@Override
public void doSomething() {
if (checkPermission()) {
this.a.doSomething();
} else {
throw new SecurityException("Forbidden");
}
}

private boolean checkPermission() {
return Math.random() < 0.5;
}

public static void main(String[] args) {
AProxy proxy = new AProxy(new ANeedProxy());
proxy.doSomething();
}
}

在上面代码之中,代理类AProxy和原始类B都实现了A接口,在使用的时候将B对象使用构造函数传入AProxy中,然后调用原始类来执行代码,同时在调用代码之前检查相关的权限信息。

动态代理

在刚才实现的代理模式中,需要将原始类中的所有方法都重新实现一遍。同时如果有多个类需要使用代理,那么就需要写多个代理类,会导致类的个数不必要的增多,可以使用动态代理来解决这一个问题。Java本身就已经提供了动态代理的语法,使用jdk动态代理实现的代码如下:

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
//java 11
class ADynamicProxyHandler implements InvocationHandler {
private final Object proxiedObject;

ADynamicProxyHandler(Object proxiedObject) {
this.proxiedObject = proxiedObject;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (checkPermission()) {
return method.invoke(proxiedObject, args);
} else {
throw new SecurityException("Forbidden");
}
}

private boolean checkPermission() {
return Math.random() < 0.5;
}

public static void main(String[] args) {
B b = new B();
ADynamicProxyHandler handler = new ADynamicProxyHandler(b);
A a = (A) Proxy.newProxyInstance(b.getClass().getClassLoader(), b.getClass().getInterfaces(), handler);
a.doSomething();
}
}

动态代理十分有用,可以帮助在编写代码时减少模板代码,减少维护和开发的成本,Spring AOP的底层实现原理就是基于这样的动态代理的。

桥接模式

桥接模式是将抽象部分与它的实现部分分离,使它们都可以独立地变化,其思路是不要过度使用继承,而是优先拆分部分组件,通过组合的方式来进行扩展。桥接模式的目的是为了避免直接继承带来的子类爆炸。

例如如果对于图形类而言,有是什么形状这一维度,也有颜色这一维度,那么就可以使用桥接模式来编写代码。具体代码如下:

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
//kotlin 1.4.0
abstract class Color protected constructor(protected val name: String) {
abstract fun bepaint(shape: String)
}

class Grey : Color("灰色") {
override fun bepaint(shape: String) = println("灰色的$shape");
}

class Green : Color("绿色") {
override fun bepaint(shape: String) = println("绿色的$shape");
}

abstract class Shape protected constructor(protected val color: Color) {
abstract fun draw()
}

class Circle(color: Color) : Shape(color) {
override fun draw() = color.bepaint("正方形")
}

class Rectangle(color: Color) : Shape(color) {
override fun draw() = color.bepaint("长方形")
}

fun main() {
val white = Grey()
val rectangle = Rectangle(white)
rectangle.draw()
}

在实际使用中,如果出现一个类出现多个可以单个可以单独变化的维度,那么可以使用桥接模式进行设计。

装饰器模式

装饰器模式能够在运行器动态的给某个对象增加功能,同时将核心功能与附加功能分开。Java标准库的IO库就是装饰器模式的经典案例。

例如我们需要给FileInputStream添加缓存功能以及GZIP解压缩功能,那么实现的代码如下:

1
2
3
4
5
6
7
8
9
10
//java 11
public class Decorator {
public static void main(String[] args) throws IOException {
InputStream in = new GZIPInputStream( //提供解压缩
new BufferedInputStream( //提供缓存
new FileInputStream("a.txt")
)
);
}
}

这样的代码就是典型的装饰器模式的代码。通过查看源码可以看到GZIPInputStream,BufferedInputStream,FileInputStream都是InputStream的子类。在这个例子中,核心功能指的就是FileInputStream这个真正读取数据的源头,附加工作指的是缓冲和压缩这两个功能。如果我们仍然需要新增附加功能,就可以继承InputStream独立的进行扩展。

在装饰器模式中,装饰器类和原始类都继承自同样的父类,装饰器模式有个特点就是可以对原始类嵌套使用多个装饰器。

适配器模式

适配器模式用于将不兼容的接口转换为可兼容的接口,让原本不能一起工作的类一起工作。适配器模式可以通过继承以及组合的方式进行实现,通过继承来实现适配器模式的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//java 11
interface ITarget{
void fa();
}
public class Adaptee {
void f1();
}

public class Adaptor extends Adaptee implements ITarget{
@Override
void fa(){
super.f1();
}
}

在这段代码之中,ITarget表示要转化成的接口的定义,Adaptee是一组不兼容的ITarget接口的类,Adaptor将Adaptee转化成为符合ITarget接口定义的类。

在实际应用之中,适配器模式可以看作一种补救设计上缺陷的方法。例如Java中有很多日志框架例如log4j,logback,大部分的日志都提供了相似的功能但没有实现统一的接口。Slf4j提供了打印日志的统一接口规范,像log4j这样的日志框架需要将接口改为符合slf4j的接口规范,就使用到了适配器模式。

门面模式

门面模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。例如一个系统A,提供了a、b、c、d 四个接口。系统B需要调用a、b、d三个接口,利用门面模式,可以提供一个包含a、b、d三个接口调用的门面接口x,供B系统使用,这就是门面模式。

门面模式不仅让子系统更加的易用,有时还可以解决性能上的问题。例如客户端APP之前可能需要使用三次调用服务器端的接口才能获取到想要的数据,而使用门面模式之后,就可以只发送一次请求,提升了响应速度。

组合模式

组合模式将对象组织成为树形的结构,表示部分与整体之间的层次结构。例如对于文件系统,可以将文件和目录进行区分,定义成为File和Directory两个类,在下面的代码中实现了打印当前目录下递归打印文件的功能,就使用了组合模式。

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
//java 11
abstract class FileNode {
protected final String path;
protected FileNode(String path) {
this.path = path;
}
abstract List<FileNode> list();
abstract String toTreeString();
}

class File extends FileNode {
File(String path) {
super(path);
}
@Override List<FileNode> list() {
return Collections.emptyList();
}

@Override String toTreeString() {
return super.path + "\n";
}
}

class Directory extends FileNode {
Directory(String path) {
super(path);
}
@Override List<FileNode> list() {
var file = new java.io.File(super.path);
return Arrays.stream(file.listFiles()).map(f -> {
if (f.isFile()) {
return new File(f.getPath());
} else {
return new Directory(f.getPath());
}
}).collect(Collectors.toList());
}
@Override String toTreeString() {
StringBuilder ret = new StringBuilder();
ret.append(super.path).append("\n");
list().forEach(node -> ret.append(node.toTreeString()));
return ret.toString();
}
}

组合模式将一组对象抽象成树形结构,将单个对象和组合对象都看做树中的节点,以统一处理逻辑,并且它利用树形结构的特点,递归地处理每个子树,依次简化代码实现。使用场景必须要能够表示成为树形结构是其前提,因此其使用场景比较有局限性。

享元模式

享元模式指的是复用被共享的变量,其意图在于复用对象,节省内存。如果在一个系统中存在大量重复对象,且这些对象是不可变变量,就可以将其设计为享元,在内存中只保留一份实例,供多处代码引用。因此,享元模式就是通过工厂方法创建对象,在工厂方法内部,很可能返回缓存的实例,而不是新创建实例,从而实现不可变实例的复用。

享元模式在Java标准库中有许多的的应用,例如Integer.valueOf这个静态工厂创建实例,当传入的int范围在-128到+127的范围之间时,会直接返回缓存的Integer实例:

1
2
3
4
5
6
7
8
9
10
11
//java 11
public class Main {
public static void main(String[] args) {
Integer n1 = Integer.valueOf(100);
Integer n2 = Integer.valueOf(100);
System.out.println(n1 == n2); // true
Integer n3 = Integer.valueOf(200);
Integer n4 = Integer.valueOf(200);
System.out.println(n3 == n4); // false
}
}