爱看书的阿东

赐他一块白色石头,石头上写着新名

浅谈设计模式 - 外观模式(九)

浅谈设计模式 - 外观模式(九)

前言:

外观模式可以说是最容易理解,也是最容易掌握的一个设计模式了,概念比较简单,主要作用是将一堆复杂的接口和功能进行简化设计,让接口的功能更加简单,从另一个角度来看,外观是对原有的旧系统提供了一个门户,当其他所有的系统接入旧系统的时候,不需要纠结旧接口的功能实现,而只要关心和外观对象打交道,而外观模式很好的将两个系统之间构建沟通的桥梁。

文章目的

  1. 了解什么是外观模式
  2. 外观模式和迪米特法则
  3. 外观模式的一些实际应用场景

什么是外观模式?

定义:定义一个统一的接口,访问子系统当中的一群接口,外部定义高层接口简化接口的调用,对于复杂系统的功能只提供最少的接口对外调用,隐藏大量实现类的实现细节。外观模式更像是构建一个统一门户,让子系统复杂错综的方法进行整合,当一个系统依赖许多子系统的方法的时候,外观模式将会派上用处。

外观是一种结构型模式,他让子系统和外部系统之间建立一层门户接口。

外观模式结构图

生活当中的模式理解:

如果用生活的案例比喻的话,我们最为常见的网上购物便是一个很好的例子,我们在网络上进行网上购物的时候,通常是由购物平台与我们交互,当我们下订单并且要求商家发货之后,商家会联系仓库打包,银行扣款,送货员配送,物流运输…..这中间的所有步骤都与客户无关,客户需要关心什么时候会配送,以及需要支付多少钱即可。

购物平台案例

从上面这个案例也可以看到,对于客户来说,最关心的门面这一块,如果购物平台崩溃,那么就直接无法交互,但是对于门面这一边来说,事情就比较复杂了,可以看到任何一个环节出错都有可能造成无法满足客户的要求,门面几乎是与所有的子系统连在了一起**。

外观模式的优缺点:

优点:

  1. 让接口方法更加简单,将子系统与外界交互的任务进行聚合
  2. 统一子系统,提供门户并且整合子系统的方法

缺点:

  1. 子系统的任意改动加直接影响外观接口的行为,同时外观的接口方法变动也可能出现意想不到的情况
  2. 外观类的接口修改对于程序的影响较大,同时不符合“开放-关闭”的原则

外观模式的特点:

  • 外观模式通常只需要一个 单例的类就足够了,因为在遵从迪米特
  • 外观更像是与所有子系统绑定的顶层系统

外观模式和适配器模式的关系

外观模式和适配器模式有点儿像,但是实际上两者侧重点是完全不一样的,**外观的意图是包装接口,而适配器的意图是转换大量的接口**

外观模式和迪米特原则

什么是迪米特原则?

一个类应该只知道最少的事情。意味着接口的职责应该越简单越好。

外观模式是对迪米特法则最好应用

由于外观模式将子系统当中大量复杂的类和方法进行整合,将所有的子系统功能结合到了自己的门面当中对外使用。所以外观模式是最能体现迪米特法则的一种设计模式。

案例

外观模式另一种使用方式:

下面的内容来自《How tomcat work》这本书的一个章节,我们忽略关于`HttpRequest`和`HttpResponse`以及处理http请求的所有细节部分。来看看外观模式的一种巧妙的用法

这次的案例比较特殊,我们直接从servlet开始。

(一)我们先从servlet入手,我们先来看一下`servlet`这个接口的定义,我们重点关注`service`这个方法:
1
2
3
4
5
6
7
8
9
10
11
12
public interface Servlet {
void init(ServletConfig var1) throws ServletException;

ServletConfig getServletConfig();

// 重点关注
void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;

String getServletInfo();

void destroy();
}
(二)接着我们依照上面这个接口方法的要求,动手实现一个我们自己的`HttpRequest`和`HttpResponse`,对于下面的两个类,我们不需要关注细节,只需要大概看一眼一些方法和实现。

HttpRequest.java

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
public class HttpRequest implements ServletRequest {

/**
* 缓冲区的大小为 1M
*/
private static final int BUFFER_COUNT = 1024;

/**
* 请求路径
*/
private String uri;

/**
* 请求流
*/
private InputStream inputStream;

public HttpRequest(InputStream inputStream) {
this.inputStream = inputStream;
}

/**
* 解析inputstream 对于内容进行解析
*/
public void parse() {
// 字符串缓冲池
StringBuffer stringBuffer = new StringBuffer(BUFFER_COUNT);

byte[] byteBuffer = new byte[BUFFER_COUNT];

if (inputStream == null) {
System.err.println("未找到套接字");
return;
}

int read = 0;
try {
// 读取数据到byte数组
read = inputStream.read(byteBuffer);
} catch (IOException e) {
e.printStackTrace();
System.exit(-1);
}
//读取byte数组的数据进入到stringbuffer
for (int i = 0; i < read; i++) {
stringBuffer.append((char)byteBuffer[i]);
}
// 打印stringbuffer
System.err.println(stringBuffer.toString());
// 获取uri
uri = parseUri(stringBuffer.toString());
}

/**
* 解析请求,获取请求Uri
* @param requestString 需要处理的uri
*/
public String parseUri(String requestString){
// 建立index1 和 2
int index1, index2;
// 获取到第一个空行
index1 = requestString.indexOf(' ');
if(index1 != -1){
// 从index1 开始找
index2 = requestString.indexOf(' ', index1 + 1);
if(index2 > index1){
// 获取请求路径
return requestString.substring(index1 + 1, index2);
}
}
return null;

}

// 此处省略大量覆写方法
//.....
}

HttpResponse.java

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
public class HttpResponse implements ServletResponse {


/**
* 组合httprequest
* 根据request返回对应到信息
*/
private HttpRequest request;

/**
* 输出流
*/
private OutputStream outputStream;

PrintWriter writer;

/**
* 缓冲区大小
*/
private static final int BUFFER_COUNT = 1024;


public HttpResponse(OutputStream outputStream) {
this.outputStream = outputStream;
}

/**
* 设置静态资源
*/
public void setResource() throws IOException {
String errMsg = "404 msg";
// 字节缓存区
byte[] bytes = new byte[BUFFER_COUNT];
// 读取静态资源
File file = new File(Constants.WEBROOT, request.getUri());
if (file.exists()) {
// 文件流
try {
FileInputStream fileInputStream = new FileInputStream(file);
// 读取字节
int ch = fileInputStream.read(bytes, 0, BUFFER_COUNT);
// 输出
while (ch != -1) {
// 写入流
outputStream.write(bytes, 0, ch);
// 重复读取数据到缓冲区
ch = fileInputStream.read(bytes, 0, BUFFER_COUNT);
}

} catch (IOException e) {
System.err.println(e.getMessage());
} finally {
if (outputStream != null) {
outputStream.close();
}
}
} else {
try {
outputStream.write(errMsg.getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
if (outputStream != null) {
outputStream.close();
}
}
}
}
// 此处省略大量覆写方法
//.....
}
(三)我们定义了请求request和响应respnse之后,构建一个servlet处理器,来统一处理客户端发送过来的http请求,所以我们需要一个`ServletProcessor`处理器类:

可以看到,下面的servlet.service(requestFace, responseFace);当中,使用的不是上面定义的HttpRequestHttpResponse,而是使用了两个RequestFaceResponseFace,这是为什么呢?

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
public class ServletProcess {
public void process(HttpRequest request, HttpResponse response) {
String uri = request.getUri();
String serveletName = uri.substring(uri.lastIndexOf("/") + 1);
// 创建Url加载器
URL[] urls = new URL[1];
File classPath = new File(Constants.WEBROOT);
URLClassLoader loader = null;
try {
URLStreamHandler streamHandler = null;
String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)).toString();
urls[0] = new URL(null, repository, streamHandler);
loader = new URLClassLoader(urls);
} catch (IOException e) {
System.err.println(e.getMessage());
}
Class myclass = null;
try {
myclass = loader.loadClass(serveletName);
} catch (ClassNotFoundException e) {
System.err.println(e.getMessage());
}

Servlet servlet = null;

//===== 下面的部分是重点 =====
//使用face 封装
RequestFace requestFace = new RequestFace(request);
ResponseFace responseFace = new ResponseFace(response);
try {
servlet = (Servlet) myclass.newInstance();
servlet.service(requestFace, responseFace);

} catch (InstantiationException | IllegalAccessException | ServletException | IOException e) {
System.err.println(e.getMessage());
}

}
}
这两个外观类从代码上来看看起来是可有可无的存在,因为它看上去既没有影响代码的扩展(至少从处理器这一块看来)。我们先设想一下如果不使用这个外观会出现什么情况?我们通过`service()`方法,为`servlet`定义请求的处理的整个细节,但是很显然,**我们暴露了`HttpRequest`和`HttpResponse`**,如果servlet在处理完成之后,将这两个对象分发给外部系统,那么就意味着外部系统可以通过这两个对象直接拿到整个servlet进而可以设法窥探到请求的细节并且寻找漏洞!!!这里加入外观类的作用就是为了保护我们上面所述的两个对象,接着再公布一下外观类的方法,就可以很清楚的了解到这样做的效果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class RequestFace implements ServletRequest {

private ServletRequest servletRequest;

public RequestFace(HttpRequest servletRequest) {
this.servletRequest = servletRequest;
}
}

public class RequestFace implements ServletRequest {

private ServletRequest servletRequest;

public RequestFace(HttpRequest servletRequest) {
this.servletRequest = servletRequest;
}

//.... 省略一堆方法
}

从代码结构来看,这一块代码的行为和装饰器模式有点儿相似的意思,但是实际上两者的思考方式是完全不相干的,装饰器模式是隐蔽的为目标类更多的功能侧重于让整个对象的某种行为得到更好的加强,而外观模式则更加突出在于隐蔽子系统的一些内部细节,这里的代码很好的体现了这一点,当使用了门面之后,诸如HttpRequest当中的parse()被外观的设计很好的隐藏了起来,即使拿到外观对象,也无法直接访问HttpRequest,外观在这里充当了一层保护层

总结:

外观模式算是整个设计模式当中最好理解的一个设计模式,他在系统当中的应用还是十分重要的,外观有点像披着“羊皮”的狼,看似简单的接口背后,却是隐藏着各种丰富的接口和功能。