设计模式:外观模式(Facade)

当一个系统功能越来越强,子系统越来越多时,客户对系统的访问会随之变得越来越复杂。

特别是当前微服务架构的流行,子系统数量快速膨胀,这时要完成一笔业务,需要跨越多个子系统;如果子系统内部发生改变,客户端也要跟着改变,这违背了 开闭原则,也违背了迪米特法则

所以就有必要是为多个子系统提供一个统一的门面(外观对象),客户端只与 门面 通信,通过 门面 来隐藏系统的复杂性,降低耦合度,这就是外观模式的作用。

外观模式在实际生活中的典型应用,如政务中心的 一个窗口办理一站式审批。原本群众需要来回跑多个部门才可能办理成功的业务,现由一个窗口统一办理,不同部门的差异通过这一个办理窗口隐藏和屏蔽。

在日常编码工作中,通常都在无意中使用了外观模式。当是高层模块需要调度多个子系统时,都会自觉地创建一个新的类封装这些子系统,提供精简的接口,让高层模块可以更加容易地间接调用这些子系统的功能。尤其是现阶段各种第三方SDK、开源类库,很大概率都会使用外观模式。

模式定义

外观(Facade)模式:又称为门面模式,是一种 对象结构型 模式。 外观模式定义了一个高层接口(即为多个子系统提供统一的门面),客户端与通过 门面 与子系统通信,使这些子系统更加容易被访问。

外部客户端不关心内部子系统的具体细节,这样大大降低应用程序的复杂度,提高了程序的可维护性。

模式分析

外观模式是 迪米特法则 的典型应用,通过引入一个新的外观类可以降低原有系统的复杂度,同时降低客户类与子系统类的耦合度。

由客户端与多个子系统联系,引入外观模式后,客户端与一个新的外观类联系。外观模式是实现代码重构以便达到迪米特法则要求的一个强有力的武器。

外观模式要求客户端与子系统的通信必须通过一个统一的外观对象来进行,外观类将客户端与子系统的内部复杂性隔离开,使得客户端只需要与外观对象打交道,而不需要与子系统内部的很多对象打交道。

外观模式的目的在于降低系统的复杂程度,很大程度上提高了客户端使用的便捷性,使得客户端无需关心子系统的工作细节,通过外观对象即可调用相关功能。

外观模式的实现关键代码:是在客户端与复杂系统之间再加一层,在这一层处理好调用顺序,依赖关系等。

模式优缺点

优点

  • 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
  • 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。
  • 降低了大型软件系统中的编译依赖性,简化了系统在不同平台之间的移植过程,因为编译一个子系统不会影响其他的子系统,也不会影响外观对象。
  • 可以选择性地暴露方法。子系统中定义的方式可分为两部分,一部分是给子系统外部使用,一部分是子系统内部相互调用。有了 Facade 类,就可以只暴露给外部使用的方法。

缺点

  • 不能很好地限制客户使用子系统类,很容易带来未知风险。

  • 最大缺点是违背了开闭原则,当增加或移除子系统时需要修改外观类。

    可以通过引入抽象外观类在一定程度上解决该问题,客户端针对抽象外观类编程。对于新的业务需求,不修改原有外观类,而对应增加一个新的具体外观类,由新的具体外观类来关联新的子系统对象,同时通过修改配置文件来达到不修改源代码并更换外观类的目的。

模式结构

外观(Facade)模式包含以下主要角色。

  • 外观(Facade)角色:为多个子系统对外提供一个共同的接口。
  • 子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它。
  • 客户(Client)角色:通过一个外观角色访问各个子系统的功能。

结构图与代码

完整版

结构图

外观模式结构图

示例代码

子系统:SubSystemA,SubSystemB,SubSystemC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SubSystemA {

public void operationA() {
System.out.println("----->SubSystemA");
}
}

public class SubSystemB {

public void operationB() {
System.out.println("----->SubSystemB");
}
}

public class SubSystemC {

public void operationC() {
System.out.println("----->SubSystemC");
}
}

门面对象:门面对象提供一个对外的统一接口,在接口中调用多个子系统

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

private static SubSystemA systemA = new SubSystemA();
private static SubSystemB systemB = new SubSystemB();
private static SubSystemC systemC = new SubSystemC();

public void operation() {
//调用多个子系统
systemA.operationA();
systemB.operationB();
systemC.operationC();
}
}

客户端:只与门面对象通信

1
2
3
4
5
6
7
public class Client {

public static void main(String[] args) {
Facade facade = new Facade();
facade.operation();
}
}

抽象版

当有两个不同的高层模块需要访问子系统,已存在的门面对象不满足时,可以增加门面对象,新门面对象内还可以委托给已存在的门面对象来处理

结构图

抽象外观类

示例代码

1
2
3
4
5
6
7
8
9
10
public class Facade2 {

private Facade facade = new Facade();
private SubSystemD systemD = new SubSystemD();

public void operation() {
this.facade.operation();
this.systemD.operationD();
}
}

注意事项

  • 在外观模式中,通常只需要一个外观类,并且此外观类只有一个实例,意味着它是一个单例类。在很多情况下,为了节约系统资源,一般将外观设计为单例类。

    但这不意味着整个系统只能有一个外观类,在一个系统中可以设计多个外观类,每个外观类负责和一些特定的子系统交互,向客户端提供相应的业务功能。

  • 当一个门面对象非常庞大时,虽然门面对象都是非常简单的委托操作,也建议拆分成多个门面,方便以后的维护和扩展,通常会按功能来拆分。

  • 门面模式目的是为子系统提供一个集中化和简化的沟通渠道,而不是向子系统加入新的行为,不能通过外观类来实现新的行为,不能出现具体的业务逻辑代码。

    在门面模式中,门面角色是稳定的,它是一个系统对象的接口,不应该经常变化。

应用场景

  • 为一个复杂系统提供一个给外部访问的简单接口。
  • 当客户端与多个子系统之间存在很大的依赖性时,引入外观模式将客户端与子系统解耦,提高子系统的独立性。
  • 在分层结构系统构建时,引入外观模式定义系统中每层入口点,层与层之间通过外观建立联系,降低层之间的耦合度。

典型应用

StandardSessionFacade

StandardSessionFacadeStandardSession 的外观对象,都实现了 HttpSession 接口。

客户端通过 HttpServletRequest.getSession() 获取 Session 对象,实际拿到的是 StandardSessionFacade。

1
HttpSession session = httpServletRequest.getSession();

StandardSessionFacade 是 StandardSession 的外观对象。通过外观对象与外部进行联系,外观对象对外提供统一的访问接口,这样就屏蔽了 StandardSession 只给内部使用的部分接口,HttpSession 在这里是一个 抽象外观

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
public class StandardSession implements HttpSession, Session, Serializable {
//....其它省略......

/**
* The facade associated with this session. NOTE: This value is not
* included in the serialized version of this object.
* 此 session 的外观, 对外委托给 StandardSessionFacade 外观对象
*/
protected transient StandardSessionFacade facade = null;

/**
* Return the <code>HttpSession</code> for which this object
* is the facade.
* 返回 HttpSession 对象(此对象)的外观对象
*/
@Override
public HttpSession getSession() {
if (facade == null) {
if (SecurityUtil.isPackageProtectionEnabled()) {
facade = AccessController.doPrivileged(new PrivilegedNewSessionFacade(this));
} else {
facade = new StandardSessionFacade(this);
}
}
return facade;
}
}

StandardSessionFacade 构造方法依赖了 HttpSession(StandardSession),实现的 HttpSession 接口的方法委托给 HttpSession 对象执行。

类图结构如下:

StandardSessionFacade外观对象

基本的代码结构如下:

Session:获取 Session

1
2
3
public interface Session {
public HttpSession getSession();
}

HttpSession:外观抽象,提供对外统一的接口

1
2
3
public interface HttpSession {
public String getId();
}

StandardSession:具体业务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class StandardSession implements HttpSession, Session {

private StandardSessionFacade facade;
private String id;

@Override
public HttpSession getSession() {
if (facade == null) {
// 返回的是外观对象
facade = new StandardSessionFacade(this);
}
return facade;
}

@Override
public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}
}

StandardSessionFacade:外观抽象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class StandardSessionFacade implements HttpSession {

private HttpSession httpSession;

public StandardSessionFacade(HttpSession httpSession) {
this.httpSession = httpSession;
}

@Override
public String getId() {
// 委托给具体业务对象执行
return httpSession.getId();
}
}

RequestFacade

HttpServletRequest 实际拿到的是 Request 的外观对象 RequestFacade。

外观对象 RequestFacade 的构造方法依赖了 Request 对象。Request 和 RequestFacade 都实现了 HttpServletRequest 接口中的方法,但外观对象 RequestFacade 中的方法是委托给 Request 对象执行的。

ApplicationContextFacade

Tomcat 的应用上下文 ApplicationContext 实现了 ServletContext,同样的要获取 ServletContext 上下文,实际拿到的是 ApplicationContext 的外观对象 ApplicationContextFacade。

相关参考

  1. 外观模式
  2. 外观模式详解
作者

光星

发布于

2021-06-16

更新于

2022-06-17

许可协议

评论