【深入设计模式】随处可见的外观模式

138 阅读8分钟

在以前我们去银行处理业务、办证中心办证、医院看病都需要亲自一个窗口一个窗口的跑一遍,有的甚至需要跑几天才能把业务办理完成,非常的繁琐。而随着信息化的发展和业务的简化,我们甚至只需要在手机上点击操作便能够完成业务,虽然实际的流程还是那样不变,但是将原本复杂的办理流程,进行了封装,对于我们来说将以前挨个窗口跑的过程给省略了。这便是外观模式的体现,即对调用方隐藏子系统业务的调用和实现。

1. 外观模式

1.1 外观模式简介

不知道大家有没遇到过这样一种情况,在工程项目中一个方法的内部会去调用各种各样其他模块的方法,然而对于调用方来说这些并不关心被调用模块的方法的执行流程,仅仅只关心这些模块方法执行完成后的结果,并且把这些被调用模块的方法写在调用方代码中会让调用方的代码变得非常冗余。这个时候我们就可以使用外观模式在应对这样的情况。外观模式就是将多个方法的执行进行封装,并对调用客户端仅提供一个访问接口,从而达到减少客户端代码复杂度、隐藏多余方法调用复杂性的目的。而外观模式在我们日常开发中非常常见,也是规避复杂代码的常用手段之一。

1.2 外观模式结构

外观模式其实非常简单,就是封装模块调用代码,那么我们就来分析一下整个外观模式的结构。整个外观模式实际上就分为两个部分一个外观类以及若干子系统类

  • 外观类(Facade):提供给客户端使用,其内部封装子系统的调用逻辑
  • 子系统类(SubSystem):实现具体不同业务功能代码的集合,提供给外观类进行整合

那么我们用代码来对外观模式结构进行实现,首先创建一个外观类 Facade 和 4 个子系统类 SubSystem1、SubSystem2、SubSystem3、SubSystem4,并且在 Facade 类中提供两个方法 method1 和 method2,method1 调用 4 各子系统中的 subSystemMethod1,method2 调用 4 各子系统中的 subSystemMethod2。代码如下:

image-20220819162319146.png

外观类:

// 外观类
public class Facade {
    private SubSystem1 subSystem1;
    private SubSystem2 subSystem2;
    private SubSystem3 subSystem3;
    private SubSystem4 subSystem4;
    // 初始化外观类,并构建 4 个子系统对象
    public Facade() {
        subSystem1 = new SubSystem1();
        subSystem2 = new SubSystem2();
        subSystem3 = new SubSystem3();
        subSystem4 = new SubSystem4();
    }
    
    // 定义方法 1,调用 4 个子系统的方法 1
    public void method1() {
        subSystem1.subSystemMethod1();
        subSystem2.subSystemMethod1();
        subSystem3.subSystemMethod1();
        subSystem4.subSystemMethod1();
    }
​
    // 定义方法 2,调用 4 个子系统的方法 2
    public void method2() {
        subSystem1.subSystemMethod2();
        subSystem2.subSystemMethod2();
        subSystem3.subSystemMethod2();
        subSystem4.subSystemMethod2();
    }
}

子系统类:

// 子系统类 1
public class SubSystem1 {
    // 定义子系统 1 的方法 1
    public void subSystemMethod1(){
        System.out.println("子系统 1 的方法 1");
    }
    
    // 定义子系统 1 的方法 2
    public void subSystemMethod2(){
        System.out.println("子系统 1 的方法 2");
​
    }
}
​
// 子系统类 2
public class SubSystem2 {
    // 定义子系统 2 的方法 1
    public void subSystemMethod1() {
        System.out.println("子系统 2 的方法 1");
    }
​
    // 定义子系统 2 的方法 2
    public void subSystemMethod2() {
        System.out.println("子系统 2 的方法 2");
    }
}
​
// 子系统类 3
public class SubSystem3 {
    // 定义子系统 3 的方法 1
    public void subSystemMethod1() {
        System.out.println("子系统 3 的方法 1");
    }
​
    // 定义子系统 3 的方法 2
    public void subSystemMethod2() {
        System.out.println("子系统 3 的方法 2");
    }
}
​
// 子系统类 4
public class SubSystem4 {
    // 定义子系统 4 的方法 1
    public void subSystemMethod1() {
        System.out.println("子系统 4 的方法 1");
    }
​
    // 定义子系统 4 的方法 2
    public void subSystemMethod2() {
        System.out.println("子系统 4 的方法 2");
    }
}

下面我们开始编写客户端调用代码:

public static void main(String[] args) {
    Facade facade = new Facade();
    System.out.println("执行外观类方法 1.");
    facade.method1();
    System.out.println("======================");
    System.out.println("执行外观类方法 2.");
    facade.method2();
}

执行结果如下:

执行外观类方法 1.
子系统 1 的方法 1
子系统 2 的方法 1
子系统 3 的方法 1
子系统 4 的方法 1
======================
执行外观类方法 2.
子系统 1 的方法 2
子系统 2 的方法 2
子系统 3 的方法 2
子系统 4 的方法 2

从上面的代码和结果可以看出我们通过外观类 Facade 的方法 method1 和 method2 对子系统的方法进行封装,这样在客户端我们仅只需调用 Facade.method1 和 Facade.method2 便能够完成子系统方法的调用。相比于直接在调用端调用子系统方法精简了客户端代码,这样在客户端就省去了子系统初始化和子系统调用的步骤。

1.3 外观模式示例

在以前我们下班回家的时候,首先是开门开灯,然后走到窗前拉开窗帘打开窗,最后再打开时电视机坐在沙发上休息一会儿看看电视。我们需要亲自去完成这一连串的动作才能够坐下来看电视,转换成代码我们继续要构建灯(Light)、窗(Window)、电视(Television)三个类,并提供对应的开灯、打开窗帘、打开窗户和打开电视机方法,代码如下:

// Light 类,提供开灯方法
public class Light {
    public void turnOn() {
        System.out.println("开灯.");
    }
}
​
// Window 类,提供打开窗帘和窗户两个方法
public class Window {
    public void openWindow() {
        System.out.println("打开窗户.");
    }
​
    public void openCurtains() {
        System.out.println("打开窗帘.");
    }
}
// Television 类,提供打开电视方法
public class Television {
    public void turnOn(){
        System.out.println("打开电视机.");
    }
}

那么我们回家之后需要依次完成这些动作之后才能够坐在沙发上看电视,那么我们首先需要对三个类进行初始化,然后在开门之后依次调用这三个类对应的方法,代码如下:

public static void main(String[] args) {
    Light light = new Light();
    Window window = new Window();
    Television television = new Television();
    System.out.println("回家开门.");
    System.out.println("===========");
    light.turnOn();
    window.openCurtains();
    window.openWindow();
    television.turnOn();
    System.out.println("===========");
    System.out.println("看电视.");
}

可以看到我们在回家之后需要执行这么多步骤,非常的繁琐,于是便有了智能家居,我们在开门之后由智能家居上面这些开灯、开窗等操作就不再由我们亲自动手去操作,而是通过设置智能家居,在触发开门这个操作之后由智能家居自动完成。于是我们使用一个智能家居类(SmartHome)来控制灯、窗帘、窗户和电视的打开。代码如下:

// 智能家居类,Light、Window、Television 类
public class SmartHome {
    Light light;
    Window window;
    Television television;
​
    // 初始化操作
    public SmartHome() {
        light = new Light();
        window = new Window();
        television = new Television();
    }
    
    // 开门时调用设备打开方法
    public void openDoor() {
        light.turnOn();
        window.openCurtains();
        window.openWindow();
        television.turnOn();
    }
}

那么我们的调用端代码就需要改变了,从原来我们自己去一个一个打开设备,变成了只需设定好智能家居,然后触发智能家居的开门场景,这些设备的打开便交由智能家居去操作,我们直接回家然后就够躺在沙发上看电视了,大大减轻了调用方的代码实现。代码如下:

public static void main(String[] args) {
    SmartHome smartHome = new SmartHome();
    System.out.println("回家开门.");
    System.out.println("===========");
    smartHome.openDoor();
    System.out.println("===========");
    System.out.println("看电视.");
}

2. 外观模式在源码中的应用

外观模式其实就是讲究的一个封装,而封装是面向对象最基础的特性之一,因此外观模式在各个框架源码中是到处都有的。而外观模式在日常开发使用最多的地方就是 Java web 开发的三层结构即 Controller、Service、Dao。特别是 Service 层,在业务处理上可能会连续调用 Dao 层代码去触发数据库的读取操作,这个时候就是 Service 层对 Dao 层代码的封装。当业务处理复杂起来之后,我们甚至会对 Service 层再次进行封装,让一个新的 Service 类来整合其他业务逻辑代码。对于 Controller 来说无论你 Service 里面怎么封装,反正我只需要调用给出的 Service 即可,这样对 Service 封装之后能够保证 Controller 层代码的简洁,并不参与过多的业务逻辑。

image-20220819161756047.png

3. 总结

外观模式对调用方屏蔽了子系统的调用逻辑,简化了客户端的调用代码,从而实现了客户端与各个子系统之间的解耦。合理的使用外观模式不仅能够让我们对业务代码划分层次,而且做到专门的层维护对应的业务逻辑代码。外观模式的核心思想就是封装,而封装在面向对象中又是基础特性,所以外观模式在实际开发中其实是到处都是的。但是随着业务需求的增加,系统也会逐渐庞大起来,只对复杂逻辑进行封装不仅不能够简化代码,反而让后面的维护人员看得非常头痛,俗称堆屎山。因此在必要时还是需要对代码进行适当重构,而不是一味的封装。