结构型-外观模式(Facade)

3/24/2022 设计模式

摘要

JDK:1.8.0_202

# 一:场景问题

# 1.1 组装电脑

组装电脑,通常会有两种方案

方案一:去电子市场把自己需要的配件都买回来,然后自己组装。这个方案好是好,但是需要对各种配件都要比较熟悉,这样才能选择最合适的配件,而且还要考虑配件之间的兼容性。如图所示:

客户完全自己组装电脑

方案二:就是到电子市场,找一家专业装机的公司,把具体的要求一讲,然后就等着拿电脑就好了。当然价格会比自己全部DIY贵一些,但综合起来这还算是个不错的选择,估计也是大多数人的选择。如图所示:

找专业的装机公司组装电脑

这个专业的装机公司就相当于本章的主角——Facade。有了它,我们就不用自己去跟众多卖配件的公司打交道,只需要跟装机公司交互就可以了,由装机公司去跟各个卖配件的公司交互,并组装好电脑返回给我们。

把上面的过程抽象一下,如果把电子市场看成是一个系统,而各个卖配件的公司看成是模块的话,就类似于出现了这样一种情况:

客户端为了完成某个功能,需要去调用某个系统中的多个模块,把它们称为是A模块、B模块和C模块吧,对于客户端而言,那就需要知道A、B、C这三个模块的功能,还需要知道如何组合这多个模块提供的功能来实现自己所需要的功能,非常麻烦

要是有一个简单的方式能让客户端去实现相同的功能多好啊,这样:

客户端就不用跟系统中的多个模块交互,而且客户端也不需要知道那么多模块的细节功能了,实现这个功能的就是Facade

# 二:外观模式

# 2.1 定义:

外观模式:为了系统中的一组接口提供一个一致的界面。Facade 模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

这里先对两个词进行一下说明,一个是界面,一个是接口

一提到界面,估计很多朋友的第一反应就是图形界面(GUI)。其实在这里提到的界面,主要指的是从一个组件外部来看这个组件,能够看到什么,这就是这个组件的界面,也就是所说的外观

比如:你从一个类外部来看这个类,那么这个类的public方法就是这个类的外观,因为你从类外部来看这个类,就能看到这些。

再比如:你从一个模块外部来看这个模块,那么这个模块对外的接口就是这个模块的外观,因为你就只能看到这些接口,其它的模块内部实现的东西是被接口封装隔离了的。

一提到接口,做Java的朋友的第一反应就是interface。其实在这里提到的接口,主要是指的外部和内部交互的这么一个通道,通常是指的一些方法,可以是类的方法,也可以是interface的方法。也就是说,这里说的接口,并不等价于interface,也有可能是一个类。

# 2.2 解决思路

客户端想要操作更简单点,那就根据客户端的需要来给客户端定义一个简单的接口,然后让客户端调用这个接口,剩下的事情就不用客户端管,这样客户端就变得简单了。

当然,这里所说的接口就是客户端和被访问的系统之间的一个通道,并不一定是指Java的interface。事实上,这里所说的接口,在外观模式里面,通常指的是类,这个类被称为 "外观"。

外观模式就是通过引入这么一个外观类,在这个类里面定义客户端想要的简单的方法,然后在这些方法的实现里面,由外观类再去分别调用内部的多个模块来实现功能,从而让客户端变得简单,这样一来,客户端就只需要和外观类交互就可以了。

# 2.3 模式结构和说明

外观模式的结构如图所示:

  • Facade:定义子系统的多个模块对外的高层接口,通常需要调用内部多个模块,从而把客户的请求代理给适当的子系统对象。
  • 模块:接受Facade对象的委派,真正实现功能,各个模块之间可能有交互。但是请注意,Facade对象知道各个模块,但是各个模块不应该知道Facade对象

# 2.4 案例

结构如图所示:

结构

各个产品接口:

public class CPU {

    public void choice() {
        System.out.println("选择 CPU");
    }

}
1
2
3
4
5
6
7
public class Mainboard {

    public void choice() {
        System.out.println("选择 主板");
    }

}
1
2
3
4
5
6
7
public class RAM {

    public void choice() {
        System.out.println("选择 内存");
    }

}
1
2
3
4
5
6
7
public class PowerSupply {

    public void choice() {
        System.out.println("选择 电源");
    }

}
1
2
3
4
5
6
7
public class Chassis {

    public void choice() {
        System.out.println("选择 机箱");
    }

}
1
2
3
4
5
6
7

定义外观对象:(Facade.java)

/**
 * 装机公司
 */
public class Facade {

    public void assemble() {
        // 1. 根据需求选购材料
        System.out.println("===选购器件===");
        new CPU().choice();
        new Mainboard().choice();
        new RAM().choice();
        new PowerSupply().choice();
        new Chassis().choice();
        // 2. 组装电脑
        System.out.println("===电脑组装===");
    }

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

客户端:(Client.java)

public class Client {
    public static void main(String[] args) {
        new Facade().assemble();
    }
}
1
2
3
4
5

# 三:模式讲解

# 3.1 认识外观模式

1. 外观模式的目的

外观模式的目的不是给子系统添加新的功能接口,而是为了让外部减少与子系统内多个模块的交互,松散耦合,从而让外部能够更简单的使用子系统。

这点要特别注意,因为外观是当作子系统对外的接口出现的,虽然也可以在这里定义一些子系统没有的功能,但不建议这么做**。外观应该是包装已有的功能,它主要负责组合已有功能来实现客户需要,而不是添加新的实现**。

2. 使用外观跟不使用相比有何变化

看到Facade的实现,可能有些朋友会说,这不就是把原来在客户端的代码搬到Facade里面了吗?没有什么大变化啊?

没错,说的很对,表面上看就是把客户端的代码搬到Facade里面了,但实质是发生了变化的,请思考:Facade到底位于何处呢?是位于客户端还是在由A、B、C模块组成的系统这边呢?

答案肯定是在系统这边,这有什么不一样吗?

当然有了,如果Facade在系统这边,那么它就相当于屏蔽了外部客户端和系统内部模块的交互,从而把A、B、C模块组合成为一个整体对外,不但方便了客户端的调用,而且封装了系统内部的细节功能,也就是说Facade与各个模块交互的过程已经是内部实现了。这样一来,如果今后调用模块的算法发生了变化,比如变化成要先调用B,然后调用A,那么只需要修改Facade的实现就可以了。

另外一个好处,Facade的功能可以被很多个客户端调用,也就是说Facade可以实现功能的共享,也就是实现复用。同样的调用代码就只用在Facade里面写一次就好了,而不用在多个调用的地方重复写。

还有一个潜在的好处,对使用Facade的人员来说,Facade大大节省了他们的学习成本,他们只需要了解Facade即可,无需再深入到子系统内部,去了解每个模块的细节,也不用和这多个模块交互,从而使得开发简单,学习也容易。

3. 有外观,但是可以不使用

虽然有了外观,如果有需要,外部还是可以绕开Facade,直接调用某个具体模块的接口,这样就能实现兼顾组合功能和细节功能。比如在客户端就想要使用A模块的功能,那么就不需要使用Facade,可以直接调用A模块的接口。

4. 外观提供了缺省的功能实现

现在的系统是越做越大、越来越复杂,对软件的要求也就更高。为了提高系统的可重用性,通常会把一个大的系统分成很多个子系统,再把一个子系统分成很多更小的子系统,一直分下去,分到一个一个小的模块,这样一来,子系统的重用性会得到加强,也更容易对子系统进行定制和使用。

但是这也带来一个问题,如果用户不需要对子系统进行定制,仅仅就是想要使用它们来完成一定的功能,那么使用起来会比较麻烦,需要跟这多个模块交互。

外观对象就可以为用户提供一个简单的、缺省的实现,这个实现对大多数的用户来说都是已经足够了的。但是外观并不限制那些需要更多定制功能的用户,直接越过外观去访问内部的模块的功能

# 3.2 外观模式的实现

1. Facade的实现

对于一个子系统而言,外观类不需要很多,通常可以实现成为一个单例

也可以直接把外观中的方法实现成为静态的方法,这样就可以不需要创建外观对象的实例而直接就可以调用,这种实现相当于把外观类当成一个辅助工具类实现

2. Facade可以实现成为interface

虽然Facade通常直接实现成为类,但是也可以把Facade实现成为真正的interface,只是这样会增加系统的复杂程度,因为这样会需要一个Facade的实现,还需要一个来获取Facade接口对象的工厂

3. Facade实现成为interface的附带好处

如果把Facade实现成为接口,还附带一个功能,就是能够有选择性的暴露接口方法,尽量减少模块对子系统外提供的接口方法

换句话说,一个模块的接口里面定义的方法可以分成两部分,一部分是给子系统外部使用的,一部分是子系统内部的模块间相互调用时使用的。有了Facade接口,那么用于子系统内部的接口功能就不用暴露给子系统外部了。

# 3.3 优缺点

1. 松散耦合

外观模式松散了客户端与子系统的耦合关系,让子系统内部的模块能更容易扩展和维护。

2. 简单易用

外观模式让子系统更加易用,客户端不再需要了解子系统内部的实现,也不需要跟众多子系统内部的模块进行交互,只需要跟外观交互就可以了,相当于外观类为外部客户端使用子系统提供了一站式服务。

3. 更好的划分访问层次

通过合理使用Facade,可以帮助我们更好的划分访问的层次。有些方法是对系统外的,有些方法是系统内部使用的。把需要暴露给外部的功能集中到外观中,这样既方便客户端使用,也很好的隐藏了内部的细节。

4. 过多的或者是不太合理的Facade也容易让人迷惑,到底是调用Facade好呢,还是直接调用模块好。

# 3.4 思考外观模式

1. 外观模式的本质

外观模式的本质:封装交互,简化调用

Facade封装了子系统外部和子系统内多个模块的交互过程,从而简化外部的调用。通过外观,子系统为外部提供一些高层的接口,以方便它们的使用。

2. 对设计原则的体现

外观模式很好的体现了 "最少知识原则"

如果不使用外观模式,客户端通常需要和子系统内部的多个模块交互,也就是说客户端会有很多的朋友,客户端和这些模块之间都有依赖关系,任意一个模块的变动都可能会引起客户端的变动。

使用外观模式过后,客户端只需要和外观类交互,也就是说客户端只有外观类这一个朋友,客户端就不需要去关心子系统内部模块的变动情况了,客户端只是和这个外观类有依赖关系。

这样一来,客户端不但简单,而且这个系统会更有弹性。当系统内部多个模块发生变化的时候,这个变化可以被这个外观类吸收和消化,并不需要影响到客户端,换句话说就是:可以在不影响客户端的情况下,实现系统内部的维护和扩展

3. 何时选用外观模式

如果你希望为一个复杂的子系统提供一个简单接口的时候,可以考虑使用外观模式,使用外观对象来实现大部分客户需要的功能,从而简化客户的使用;

如果想要让客户程序和抽象类的实现部分松散耦合,可以考虑使用外观模式,使用外观对象来将这个子系统与它的客户分离开来,从而提高子系统的独立性和可移植性;

如果构建多层结构的系统,可以考虑使用外观模式,使用外观对象作为每层的入口,这样可以简化层间调用,也可以松散层次之间的依赖关系;

# 3.5 相关模式

1. 外观模式和中介者模式

这两个模式非常类似,但是有本质的区别。

中介者模式主要用来封装多个对象之间相互的交互,多用在系统内部的多个模块之间;外观模式封装的是单向的交互,是从客户端访问系统的调用,没有从系统中来访问客户端的调用

中介者模式的实现里面,是需要实现具体的交互功能的;而外观模式的实现里面,一般是组合调用或是转调内部实现的功能,通常外观模式本身并不实现这些功能。

中介者模式的目的主要是松散多个模块之间的耦合,把这些耦合关系全部放到中介者中去实现;而外观模式的目的是简化客户端的调用,这点和中介者模式也不同。

2. 外观模式和单例模式

通常一个子系统只需要一个外观实例,所以外观模式可以和单例模式组合使用,把Facade类实现成为单例。当然,也可以跟前面示例的那样,把外观类的构造方法私有化,然后把提供给客户端的方法实现成为静态的。

3. 外观模式和抽象工厂模式

外观模式的外观类通常需要和系统内部的多个模块交互,每个模块一般都有自己的接口,所以在外观类的具体实现里面,需要获取这些接口,然后组合这些接口来完成客户端的功能

那么怎么获取这些接口呢?就可以和抽象工厂一起使用,外观类通过抽象工厂来获取所需要的接口,而抽象工厂也可以把模块内部的实现对Facade进行屏蔽,也就是说Facade也仅仅只是知道它从模块中获取的它需要的功能,模块内部的细节,Facade也不知道了

# 四:参考文献

最后更新: 3/26/2022, 1:36:30 PM