外观模式

1 实际场景

1.1 代码模板生成

很多平台代码工具可以根据配置生成相应的模板代码,用户则只需要关注特定的业务功能实现,降低用户使用的复杂度以及提高效率。

假设生成的模板代码都具有基本的三层架构,即:表现层、逻辑层和数据层,所以代码生成工具应该具有相应代码生成的处理模块,另外代码工具还需要有相应的配置管理模块,定义配置模板,用来决定生成代码的逻辑。

如何实现该代码生成工具?

1.2 非设计模式的解决方案

非常 straightforward 的实现方式,方便示例,其中每个模块的实现就简单示意。

1、 配置管理模块的配置对象定义

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
/**
* @author mark.hct
* @version ConfigModel.java v 0.1 2023/2/19 21:52 Exp $
* @description 示意配置描述的数据Model,实际场景的配置数据会很多
*/
public class ConfigModel {
/**
* 是否需要生成表现层,默认是true
*/
private boolean needGenPresentation = true;
/**
* 是否需要生成逻辑层,默认是true
*/
private boolean needGenBusiness = true;
/**
* 是否需要生成DAO,默认是true
*/
private boolean needGenDAO = true;

public boolean isNeedGenPresentation() {
return needGenPresentation;
}

public void setNeedGenPresentation(boolean needGenPresentation) {
this.needGenPresentation = needGenPresentation;
}

public boolean isNeedGenBusiness() {
return needGenBusiness;
}

public void setNeedGenBusiness(boolean needGenBusiness) {
this.needGenBusiness = needGenBusiness;
}

public boolean isNeedGenDAO() {
return needGenDAO;
}

public void setNeedGenDAO(boolean needGenDAO) {
this.needGenDAO = needGenDAO;
}
}

2、配置管理定义

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
/**
* @author mark.hct
* @version ConfigManager.java v 0.1 2023/2/19 21:54 Exp $
* @description 示意配置管理,就是负责读取配置文件,
* 并把配置文件的内容设置到配置Model中去,是个单例
*/
public class ConfigManager {
private static ConfigManager manager = null;
private static ConfigModel cm = null;

public static ConfigManager getInstance() {
if (manager == null) {
synchronized (ConfigManager.class) {
manager = new ConfigManager();
cm = new ConfigModel();
// 读取配置文件,把值设置到ConfigModel中去,此处省略
// ...
}
}
return manager;
}

/**
* 获取配置的数据
*
* @return 配置的数据
*/
public ConfigModel getConfigData() {
return cm;
}
}

3、各个模块的生成代码,通过获取配置文件的内容,按照配置生成相应的代码

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
/**
* @author mark.hct
* @version Presentation.java v 0.1 2023/2/19 22:00 Exp $
* @description 示意生成表现层的模块
*/
public class Presentation {
public void generate() {
//1:从配置管理里面获取相应的配置信息
ConfigModel cm = ConfigManager.getInstance().getConfigData();
if (cm.isNeedGenPresentation()) {
//2:按照要求去生成相应的代码,并保存成文件
System.out.println("正在生成表现层代码文件");
}
}
}


/**
* @author mark.hct
* @version Business.java v 0.1 2023/2/19 22:02 Exp $
* @description 示意生成逻辑层的模块
*/
public class Business {
public void generate() {
ConfigModel cm = ConfigManager.getInstance().getConfigData();
if (cm.isNeedGenBusiness()) {
System.out.println("正在生成逻辑层代码文件");
}
}
}


/**
* @author mark.hct
* @version DAO.java v 0.1 2023/2/19 22:03 Exp $
* @description 示意生成数据层的模块
*/
public class DAO {
public void generate() {
ConfigModel cm = ConfigManager.getInstance().getConfigData();
if (cm.isNeedGenDAO()) {
System.out.println("正在生成数据层代码文件");
}
}
}

4、客户端实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @author mark.hct
* @version Client.java v 0.1 2023/2/19 22:04 Exp $
* @description
*/
public class Client {
public static void main(String[] args) {
//现在没有配置文件,就直接使用默认的配置,通常情况下,三层都应该生成,
//也就是说客户端必须对这些模块都有了解,才能够正确使用它们
new Presentation().generate();
new Business().generate();
new DAO().generate();
}
}

1. 3 有何问题

如上述案例代码,客户端为了使用生成代码工具,需要与生成代码的内部多个模块交互。从用户的角度来说,代码生成工具的使用体验非常差,若工具内部某个模块发生变化,还需要引起客户端也随之变化。如何处理此问题?

2 外观模式

2.1 定义

外观模式(Facade Pattern)属于结构型模式。为子系统中的一组接口提供一致的界面,Facade 模式定义了一个高层接口,此接口可以使子系统更加容易使用。

此处的的界面指的是从一个组件外部来看这个组件,对外提供的接口,就是这个组件的界面,即:外观。

2.2 外观模式解决问题思路

上面案例中,用户需要更简单的操作,此时需要为客户端定义一个简单的接口,客户端调用这个接口就行。

此处提到的接口,不是指的是 interface,在外观模式中,一般指的是外观类,在这个类中定义客户端需要的方法,然后在方法实现里面,有外观类去分别调用多个内部模块来实现功能。

2.3 结构说明

image-20230219234426815 image-20230219234504594
  • Facade:定义子系统的多个模块对外的高层接口。需要调用内部多个模块,从而把客户的请求代理给适当的子系统的系统对象;
  • 模块:接受 Facade 对象的委派,实现具体的功能,各个模块之间可能有交互。其中 Facade 对象能感知各个模块,但是各个模块无法感知 Facade 对象的存在;

2.4 示例代码

1、内部多个模块的接口定义(可以是任意的功能方法

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
/**
* @author mark.hct
* @version AModuleApi.java v 0.1 2023/2/20 20:53 Exp $
* @description A模块的接口
*/
public interface AModuleApi {
/**
* 示意方法,A模块对外的一个功能方法
*/
void testA();
}

/**
* @author mark.hct
* @version BModuleApi.java v 0.1 2023/2/20 20:53 Exp $
* @description B模块的接口
*/
public interface BModuleApi {
void testB();
}

/**
* @author mark.hct
* @version CModuleApi.java v 0.1 2023/2/20 21:03 Exp $
* @description
*/
public interface CModuleApi {
void testC();
}

2、模块接口的实现

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
/**
* @author mark.hct
* @version AModuleImpl.java v 0.1 2023/2/20 22:52 Exp $
* @description
*/
public class AModuleImpl implements AModuleApi {
@Override
public void testA() {
System.out.println("现在在A模块里面操作testA方法");
}
}

/**
* @author mark.hct
* @version BModuleImpl.java v 0.1 2023/2/20 22:52 Exp $
* @description
*/
public class BModuleImpl implements BModuleApi {
@Override
public void testB() {
System.out.println("现在在B模块里面操作testB方法");
}
}

/**
* @author mark.hct
* @version CModuleImpl.java v 0.1 2023/2/20 22:53 Exp $
* @description
*/
public class CModuleImpl implements CModuleApi {
@Override
public void testC() {
System.out.println("现在在C模块里面操作testC方法");
}
}

3、定义外观对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @author mark.hct
* @version Facade.java v 0.1 2023/2/20 22:54 Exp $
* @description 外观对象
*/
public class Facade {
/**
* 示意方法,满足客户需要的功能
*/
public void test() {
//在内部实现的时候,可能会调用到内部的多个模块
AModuleApi a = new AModuleImpl();
a.testA();
BModuleApi b = new BModuleImpl();
b.testB();
CModuleApi c = new CModuleImpl();
c.testC();
}
}

4、客户端,直接使用外观对象

1
2
3
4
5
6
7
8
9
10
11
/**
* @author mark.hct
* @version Client.java v 0.1 2023/2/20 22:56 Exp $
* @description
*/
public class Client {
public static void main(String[] args) {
//使用Facade
new Facade().test();
}
}

2.5 重写方案

1、重写的方案中,子模块的 BusinessDAOPresentation 都是同上定义一致,需要添加一个 Facade 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author mark.hct
* @version Facade.java v 0.1 2023/2/20 23:18 Exp $
* @description 代码生成子系统的外观对象
*/
public class Facade {
/**
* 客户端需要的,一个简单的调用代码生成的功能
*/
public void generate() {
new Presentation().generate();
new Business().generate();
new DAO().generate();
}
}

2、客户端实现,不需要在与多个子系统内部模块交互,直接使用外观对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @author mark.hct
* @version Client.java v 0.1 2023/2/19 22:04 Exp $
* @description
*/
public class Client {
public static void main(String[] args) {
////现在没有配置文件,就直接使用默认的配置,通常情况下,三层都应该生成,
////也就是说客户端必须对这些模块都有了解,才能够正确使用它们
//new Presentation().generate();
//new Business().generate();
//new DAO().generate();

//使用Facade
new Facade().generate();
}
}

Facade 类也被称为子模块模块对外的接口,有了 Facade 类,客户端就不需要知道系统内部的实现细节,甚至客户端都不需要知道内部模块的存在,客户端只需要同 Facade 类交互就行,从而更好的实现了客户端和子系统中的模块的解耦,让客户端更容易的使用系统。

3 模式讲解

3.1 认识外观模式

3.1.1 目的

外观模式的目的是让外部减少与子系统内多个模块的交互,松耦合,从而让外部能更简单地使用子系统

值得注意的是,虽然在代码层面可以在外观类中定义额外的与子系统无关的功能方法,但是不建议这么做。外观应该包装已有的功能,它主要负责组合已有功能来实现客户需要,而不是添加新的实现

3.1.2 外观模式提供缺省的功能实现

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

但是随着带来一个问题,如果用户不需要对子系统进行定制,仅仅就是想要使用它们来完成一定的功能,那么使用起来会比较麻烦,需要跟这多个模块交互。外观对象就可以为用户提供一个简单的、缺省的实现,这个实现满足了大多数的用户的需求。但是外观并不限制那些需要更多定制功能的用户,直接越过外观去访问内部的模块的功能

3.2 实现方式

3.2.1 单例实现

对于一个子系统而言,外观类不需要很多,通常可以实现成为一个单例也可以直接把外观中的方法实现成为静态的方法,这样就可以不需要创建外观对象的实例而直接就可以调用,这种实现相当于把外观类当成一个辅助工具类实现。简要的示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Facade {
private Facade() {

}
public static void test(){
AModuleApi a = new AModuleImpl();
a.testA();
BModuleApi b = new BModuleImpl();
b.testB();
CModuleApi c = new CModuleImpl();
c.testC();
}
}

3.2.2 接口实现

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

image-20230223002858033

此时客户端需要先从工厂获取 Facade 的接口,才能调用对应功能。

接口实现的好处

好处:就是能够有选择性的暴露接口方法,尽量减少模块对子系统外提供的接口方法。即:一个模块的接口里面定义的方法可以分成两部分,一部分是给子系统外部使用的,一部分是子系统内部的模块间相互调用时使用的。有了 Facade 接口,则用于子系统内部的接口功能就不用暴露给子系统外部。

示例:

定义如下的 A、B、C 模块的接口:

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 interface AModuleApi {
//对子系统外部
public void a1();
//子系统内部使用
public void a2();
//子系统内部使用
public void a3();
}

public interface BModuleApi {
//对子系统外部
public void b1();
//子系统内部使用
public void b2();
//子系统内部使用
public void b3();
}
public interface CModuleApi {
//对子系统外部
public void c1();
//子系统内部使用
public void c2();
//子系统内部使用
public void c3();
}

Facade 接口:

1
2
3
4
5
6
public interface FacadeApi {
public void a1();
public void b1();
public void c1();
public void test();
}

在示例中,外部只需要有 Facade 接口,就不再需要其它的接口,这样就能有效地屏蔽内部的细节,可以避免客户端去调用 A 模块的接口时,发现了一些不需要它知道的接口,这会造成 “接口污染”

3.2.3 Facade 的方法实现

Facade 的方法实现中,一般是负责把客户端的请求转发给子系统内部的各个模块进行处理,Facade 的方法本身并不进行功能的处理,Facade 的方法的实现只是实现一个功能的组合调用。

但是 Facade 并不禁止在其中实现其他逻辑,但是不符合 Facade 的设计模式本意。

3.3 外观模式的优缺点

优点

  • 松散耦合

    外观模式松散了客户端与子系统的耦合关系,让子系统内部的模块能更容易扩展和维护。如果调用模块的算法发生了变化,比如变化成要先调用 B,然后调用 A,那么只需要修改 Facade 的实现就可以。

  • 简单易用

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

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

缺点

  • 增加系统复杂度

    Facade 过多的时候(可能有些不合理的设计),容易导致用户比较困惑,何时选择调用 Facade,何时选择调用内部模块。

3.4 思考

3.4.1 本质

封装交互,简化调用

3.4.2 设计原则

最少知识原则

用户无需了解系统内部实现细节,只需要了解外观类就行。且通过外观模式可以在不影响用户的情况下,对系统内部模块进行维护和扩展。

3.4.3 使用场景

  • 若需要为一个复杂系统提供一个简单接口时;
  • 若需要客户端程序与抽象类实现松散耦合,通过外观对象将子系统与客户端分离,同时也能提高系统子模块的独立性和可移植性;
  • 构建多层结构系统时,通过外观对象作为每层的入口,可以简化系统层间调用,同时也松散了层次之间的依赖关系;

3.5 相关模式

3.5.1 外观模式和中介者模式

  • 外观模式封装的是单方面的请求,指的是从客户端对系统的调用;
  • 中介者模式主要用来封装多个对象之间相互的交互,主要是在系统内部的多个模块之间的交互;
  • 外观模式一般是组合调用或者是对内部系统的子模块调用,在外观类中不实现这些功能;
  • 中介者模式中需要实现具体具体的交互功能;
  • 外观模式的目的是简化客户端的调用,不需要了解内部实现细节;
  • 中介者模式的目的是松散多个模块之间的耦合,具体的耦合关系在中介者中实现;

3.5.2 外观模式和单例模式

在实际的生产场景中,外观模式与单例模式组合使用,将 Facade 类实现为单例,节省系统资源。

3.5.3 外观模式和抽象工厂模式

外观模式需要与系统内部的多个模块交互,组合这些接口能力来完成客户需要的功能,其中每个模块都有自己定义的接口,这些接口的则可以使用抽象工厂来实现,将模块内部的实现对 Facade 进行屏蔽,即,Facade 只需要从模块中获取需要对应功能的接口,无需深入内部的实现细节。