场景

网络请求中,我们一般使用 axios 库,支持用 Promise 风格调用。

1axios
2  .get('/api/user', {
3    params: {
4      ID: '123',
5    },
6  })
7  .then((response) => {
8    console.log(response)
9  })
10  .catch((error) => {
11    console.log(error)
12  })
13
14axios
15  .post(
16    '/api/user',
17    {
18      firstName: 'wind',
19      lastName: 'liang',
20    },
21    {
22      headers: { 'Content-Type': 'application/json' },
23    }
24  )
25  .then((response) => {
26    console.log(response)
27  })
28  .catch((error) => {
29    console.log(error)
30  })

可以看到上边的 getpost 传参并不统一,使用起来会比较繁琐,post 还需要手动传递 headers

为了解决这些问题,我们可以通过外观(门面)模式来解决。

外观(门面)模式

模式定义

看下 维基百科 的定义。

The facade pattern (also spelled façade) is a software-design pattern commonly used in object-oriented programming. Analogous to a facade in architecture, a facade is an object that serves as a front-facing interface masking more complex underlying or structural code.

外观模式相当于为一个相对复杂的接口或者结构提供一个上层接口供用户使用,看一下 UML 类图。

image-20220215084348154

举一个简单例子,比如开电脑是一个复杂的过程,我们可以封装成一个函数来实现:

1class CPU {
2  freeze(): void { /* 冻结CPU */ }
3  jump(position: number): void { /* 跳转到指定位置 */ }
4  execute(): void { /* 执行指令 */ }
5}
6
7class Memory {
8  load(position: number, data: Uint8Array): void {
9    /* 加载数据到内存 */
10  }
11}
12
13class HardDrive {
14  read(lba: number, size: number): Uint8Array {
15    /* 从硬盘读取数据 */
16    return new Uint8Array(size);
17  }
18}
19
20/* 外观模式 */
21
22class Computer {
23  cpu: CPU;
24  memory: Memory;
25  hardDrive: HardDrive;
26
27  constructor() {
28    this.cpu = new CPU();
29    this.memory = new Memory();
30    this.hardDrive = new HardDrive();
31  }
32
33  startComputer(): void {
34    this.cpu.freeze(); // 冻结CPU
35    this.memory.load(BOOT_ADDRESS, this.hardDrive.read(BOOT_SECTOR, SECTOR_SIZE)); // 加载启动数据到内存
36    this.cpu.jump(BOOT_ADDRESS); // 跳转到启动地址
37    this.cpu.execute(); // 执行启动程序
38  }
39}
40
41/* 客户端 */
42
43class You {
44  static main(): void {
45    const facade = new Computer();
46    facade.startComputer();
47  }
48}
49
50// 常量定义
51const BOOT_ADDRESS = 0x0; // 启动地址
52const BOOT_SECTOR = 0x0; // 启动扇区
53const SECTOR_SIZE = 512; // 扇区大小
54
55You.main(); // 启动计算机

UML 类图中外观模式会和很多 class 交互,但在 js 中可能会很少遇到这种情况,通常是当参数比较复杂或者某个功能使用起来比较麻烦的时候我们就可以通过外观模式进行简化。

代码实现

对于开头 axios 的问题,我们可以对 axios 进行一层封装。

1// request.js
2import axios from 'axios'
3export const get = function (url, params) {
4  return axios.get(url, { params })
5}
6
7export const post = function (url, params) {
8  return axios.post(url, { ...params }, { headers: { 'Content-Type': 'application/json' } })
9}

然后引用 request.js 进行调用。

1import { get, post } from './request'
2
3get('/api/user', {
4  ID: '123',
5})
6  .then((response) => {
7    console.log(response)
8  })
9  .catch((error) => {
10    console.log(error)
11  })
12
13post('/api/user', {
14  firstName: 'wind',
15  lastName: 'liang',
16})
17  .then((response) => {
18    console.log(response)
19  })
20  .catch((error) => {
21    console.log(error)
22  })

上边的封装只是为了演示外观模式的使用,实际项目中封装的会更加全面

通过门面模式除了简化了我们的调用,还有一个好处就是将底层调用封装了起来,未来如果底层需要变化,比如上边的 axios 替换为 fetch ,我们只需要去修改 request.js 即可,业务方无需感知。

更多场景

外观模式说的宽泛的话就是将复杂的调用包装一层变的简单些。

平时用到的 VuetemplateReactjsx ,也可以认为使用了外观模式,他们都将底层 dom 创建封装起来,使得我们编写页面会变得更加简单。

易混设计模式

前边讲到的 代理模式适配器模式模版方法 结构上和外观模式看起来都有些像,区别就在于他们的意图不同:

  • 适配器模式是为了解决两个对象之间不匹配的问题,而原对象又不适合直接修改,此时可以使用适配器模式进行一层转换。
  • 代理模式是为了增强原对象的功能,提供的接口不会改变。
  • 模版模式是将不同功能组合在一起,只提供框架,具体实现还需要调用者传进来。
  • 外观模式是将比较复杂的调用进行一层封装,提供一个新的接口供用户使用。

总结

外观模式是一种比较自然和直观的设计模式,通常在某个功能复杂或者使用频率较高的情况下会被考虑使用。通过外观模式,我们可以将一组相关联的接口和实现进行封装,提供一个简化的接口给客户端使用,从而减少客户端与子系统之间的直接交互,降低了系统的复杂度。

除了简化接口和降低复杂度外,外观模式还具有一个重要的好处,即能够更好地应对底层实现的变化。因为客户端只依赖于外观类提供的接口,而不需要了解底层子系统的具体实现细节,所以当底层实现发生变化时,只需调整外观类而不影响客户端的代码。

总而言之,外观模式提供了一种简单且灵活的方式来管理复杂系统的接口和实现,使得代码更加清晰易懂,并且有助于未来系统的维护和扩展。