从零开始学习Java设计模式 | 结构型模式篇:外观模式

Posted 李阿昀

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从零开始学习Java设计模式 | 结构型模式篇:外观模式相关的知识,希望对你有一定的参考价值。

在本讲,我们来学习一下结构型模式里面的第五个设计模式,即外观模式。

概述

在向大家讲解外观模式之前,我们先来看一个例子。

有些人可能炒过股票,但其实大部分人都是不太懂里面的一些操作的,这种没有足够了解证券知识的情况下炒股票是很容易亏钱的,刚开始炒股时我们肯定都会想,如果有个懂行的帮帮手就好了,其实基金就是个好帮手,支付宝里就有许多的基金,它将投资者分散的资金集中起来,交由专业的经理人进行管理,投资于股票、债券、外汇等领域,而基金投资的收益归持有者所有,管理机构则是收取一定比例的托管管理费用。其实,

其实,以上所讲述的例子就用到了外观模式,这话又该怎么去理解呢?大家不妨思考一下,如果我们直接去买基金或者找专业的经理人,那么我们就不需要去了解股票具体的一些操作了,而且也不需要额外去了解债券、外汇等这些知识了,我们只需要把咱的资金交由经理人或者基金就行,至于经理人或者基金到底是投资股票,还是债券,还是外汇,我们并不需要去关注。这种思想就是今天我要讲的外观模式。

那到底什么是外观模式呢?接下来,我们就来看看外观模式的概念。

外观模式又名门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一的接口(你可以将其理解为上例中的基金,具体基金里面到底是投资股票,还是债券,还是外汇,咱们并不关注,因为这是由子系统来实现的),外部应用程序不用关心内部子系统的具体的细节,这样就大大降低了应用程序的复杂度,并提高了程序的可维护性。

还有一点需要大家知道,外观(Facade)模式是"迪米特法则"的典型应用。我们可以来看一下下面这张图。

先看上图左边部分,大的矩形表示的就是一个子系统,子系统里面有很多很多的类,对于访问者来说,只有了解了子系统里面的实之后,他才能更好的去使用子系统,这是在没有使用外观模式的情况下对子系统的一个访问,很明显,这增加了访问者访问的难度。

再来看上图右边部分,这是在使用外观模式的情况下对子系统的一个访问,此时,对于访问者来说,他并不需要去关注这个子系统里面是如何实现的,而是只需要去调用对外提供的统一的接口就可以正常的去访问它了,比如上面我所讲述的基金,炒股者只需要去了解基金就可以了,至于基金最终到底是去投资股票,还是债券,还是外汇,炒股者并不需要去关注,这样是不是就可以大大地降低了访问者使用子系统类的一个成本啊?其实,这也是外观模式的一个好处。

结构

理解了外观模式的概念之后,接下来,我们来看一下外观模式所包含的角色。

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

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

这样,对于访问者来说,他并不需要去关注子系统,而只需要关注外观角色就行,因为我们到时候是要通过外观角色去使用子系统里面的那些对象的。

外观模式案例

接下来,我们来实现一个外观模式的案例,通过该案例大家再去理解一下外观模式,这个案例就是智能家电控制。

分析

小明的爷爷已经60岁了,一个人在家生活:每次都需要打开灯、打开电视、打开空调;睡觉时关闭灯、关闭电视、关闭空调;操作起来都比较麻烦。所以小明给爷爷买了一个智能音箱,可以通过语音直接控制这些智能家电的开启和关闭。

阅读完上面的描述,我们知道智能音箱代表的就是外观角色,客户只需要和这个智能音箱进行交互即可。

下面我们再来看一下这张类图。

从以上类图中可以看到,有一个电灯类(即Light),它里面有两个方法,一个是开启电灯(即on方法),一个是关闭电灯(即off方法);也有一个电视类(即TV),它里面也有两个方法,一个是开启电视(即on方法),一个是关闭电视(即off方法);还有一个空调类(即AirCondition),它里面也是有两个方法,一个是开启空调(即on方法),一个是关闭空调(即off方法)。

以上类都不太重要,重要的是SmartAppliancesFacade类,它是一个外观类,它里面聚合了Light、TV、AirCondition等等这些类;除此之外,它里面还提供了一个无参构造及say方法,通过该say方法,我们就可以通过语音直接控制这些智能家电的开启和关闭了;最后,它里面还提供了两个方法,一个是用来一键开启所有智能家电的(即on方法),一个是用来一键关闭所有智能家电的(即off方法),注意了,这俩方法都是私有的,因为这俩方法都只在say方法里面被调用。

以上类图分析完了以后,接下来,我们就要开始编写代码实现以上案例了。

实现

首先,打开咱们的maven工程,并在com.meimeixia.pattern包下新建一个子包,即facade,也即外观模式的具体代码我们是放在了该包下。

然后,创建电灯类,即Light。

package com.meimeixia.pattern.facade;

/**
 * 电灯类
 * @author liayun
 * @create 2021-07-31 19:07
 */
public class Light {

    // 开灯
    public void on() {
        System.out.println("打开电灯......");
    }

    // 关灯
    public void off() {
        System.out.println("关闭电灯......");
    }

}

接着,创建电视机类,即TV。

package com.meimeixia.pattern.facade;

/**
 * 电视机类
 * @author liayun
 * @create 2021-07-31 19:10
 */
public class TV {

    public void on() {
        System.out.println("打开电视机......");
    }

    public void off() {
        System.out.println("关闭电视机......");
    }

}

紧接着,创建空调类,即AirCondition。

package com.meimeixia.pattern.facade;

/**
 * 空调类
 * @author liayun
 * @create 2021-07-31 19:07
 */
public class AirCondition {

    public void on() {
        System.out.println("打开空调......");
    }

    public void off() {
        System.out.println("关闭空调......");
    }

}

再接着,创建智能音箱类,这里我们不妨起名为SmartAppliancesFacade。注意了,该类就是外观类,用户主要就是和该类对象进行交互。

经过对以上类图的分析,也相信大家能写出该类的代码来,如下所示。

package com.meimeixia.pattern.facade;

/**
 * 外观类,用户主要和该类对象进行交互
 * @author liayun
 * @create 2021-07-31 19:24
 */
public class SmartAppliancesFacade {

    // 聚合电灯对象、电视机对象、空调对象
    private Light light;
    private TV tv;
    private AirCondition airCondition;

    /*
     * 以上只是声明了三个成员变量,但是它们都还未赋值具体的对象,所以我们可以在SmartAppliancesFacade类
     * 中定义一个如下的无参的构造方法,在该无参构造里面为它们赋值
     */
    public SmartAppliancesFacade() {
        light = new Light();
        tv = new TV();
        airCondition = new AirCondition();
    }

    // 通过语音控制
    public void say(String message) {
        if (message.contains("打开")) { // 若用户说的语言里面包含了打开这样的一个字眼,则调用on方法一键打开所有智能家电
            on();
        } else if (message.contains("关闭")) { // 若用户说的语言里面包含了关闭这样的一个字眼,则调用off方法一键关闭所有智能家电
            off();
        } else {
            System.out.println("我还听不懂你说的!!!");
        }
    }

    // 一键打开功能
    private void on() {
        light.on();
        tv.on();
        airCondition.on();
    }

    // 一键关闭功能
    private void off() {
        light.off();
        tv.off();
        airCondition.off();
    }

}

最后,创建一个客户端类来进行测试。

package com.meimeixia.pattern.facade;

/**
 * @author liayun
 * @create 2021-07-31 19:39
 */
public class Client {
    public static void main(String[] args) {
        // 创建智能音箱对象
        SmartAppliancesFacade facade = new SmartAppliancesFacade();
        // 控制家电
        facade.say("打开家电"); // 早上起来时,喊一声"打开家电",那么所有的家电都会打开
        System.out.println("==========================");
        facade.say("关闭家电"); // 晚上睡觉时,喊一声"关闭家电",那么所有的家电都会关闭
    }
}

此时,运行以上客户端类,打印结果如下图所示,确实如我们所想的一样,那么现在就可方便了,我们只需要和智能音箱进行交互就行了。当然,在这里,我只是简单地去模拟了一下通过语音来控制家电,但是大家要知道的一点是,现实中具体实现的细节还有很多很多,例如自然语言的识别,显然这已超出咱们的认知范围了,所以我也就不过多赘述了。

外观模式的优缺点以及使用场景

接下来,我们来看一看外观模式的优缺点以及使用场景。

优缺点

优点

外观模式的优点,我总结出来了下面两点。

  1. 降低了子系统与客户端之间的耦合度(很显然,这是由外观类来降低的),使得子系统的变化不会影响调用它的客户端
  2. 对客户屏蔽了子系统组件,减少了客户处理的对象数目(如果客户直接去访问这些子系统的话,那么就可能要去访问不同的子系统里面的多个对象了,这还是比较麻烦的),并使得子系统使用起来更加容易

缺点

外观模式是不符合开闭原则的,所以修改起来很麻烦。

比如说子系统里面发生了一个改变的话,那么我们就得去修改该子系统了,虽然客户端我们是不需要修改的,而且可能我们还需要去修改外观类里面的代码。

使用场景

外观模式的使用场景,我总结出来了下面三点。

  1. 对分层结构系统构建时,使用外观模式定义子系统中每层的入口点可以简化子系统之间的依赖关系。

    这句话如何来理解呢?我想有必要给大家讲一下。它说的是如果系统有分层结构(即高层调用底层)的话,那么我们在去调用的时候,直接去依赖具体的一个公共接口即可,而不是再依赖具体的实现了,这样,可以使得后期代码的扩展性更好一些,当然了,也可以简化系统之间的依赖关系

  2. 当一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问。

    这也说明以后我们在去设计咱们的软件时,一定要满足这一点。其实,你在去使用别人写的框架时,你会发现框架底层做了很多很多的封装,这样,你使用起该框架来就会特别特别简单了,这个就是人家写的框架的一个好处

  3. 当客户端与多个子系统之间存在很大的联系时,引入外观模式可将它们分离,从而提高子系统的独立性和可移植性。

    我们使用外观模式,可以降低(或者消除)客户端和多个子系统之间的一个耦合,这样,当我们去修改客户端代码时,就不用再去修改子系统里面的代码了,因为此时客户端和子系统是没有任何关系的。同理,当我们去修改子系统里面的代码时,客户端代码也就不需要再进行修改了

外观模式在源码中的应用

接下来,我们来看一下之前学过的技术里面,哪一块用到了外观模式。

Tomcat相信大家一定都用过,我们主要是使用Tomcat作为一个web容器。使用Tomcat作为web容器时,接收浏览器发送过来的请求,Tomcat会将请求信息封装成ServletRequest对象,如下图①处对象。但是,大家想一想,ServletRequest是一个接口,它还有一个子接口HttpServletRequest,而我们知道doPost方法里面的request对象肯定是一个HttpServletRequest接口的子实现类对象,那大家又知不知道该request对象到底是哪个类的对象呢?相信大家之前在用的时候,都没有去关注过这个问题,这里我就为大家来揭晓这个问题的答案。

大家想要知道答案的话,其实也很简单,可以直接将该request对象打印出来,这样,你就可以看到它到底是哪个类的对象了。这里,我也就不卖关子了,直接告诉大家答案好了,该request对象其实是一个名为RequestFacade的类的对象,而该类就使用到了外观模式,为何这样说呢,下面我就为大家说道说道。

大家先看一下下面这张类图。

可以看到,最顶层是一个叫ServletRequest的接口,它下面有一个子接口,即HttpServletRequest,而该HttpServletRequest接口又有一个叫RequestFacade的子实现类。并且,在RequestFacade类中,还聚合了Request类的对象,注意,Request类也是HttpServletRequest接口的一个子实现类,这可从以上类图中看出。

那么问题来了,RequestFacade类到底有没有使用到外观模式呢?下面我们就来分析分析。

其实,RequestFacade类就是外观类,也即它代表的是外观角色,而Request类属于子系统角色,这样的话,我们只需要去和RequestFacade类的对象进行交互就行了,并不需要再直接去和Request类的对象进行交互了,因此,RequestFacade类确实是使用到了外观模式。

好,问题又来了,为什么在此处使用外观模式呢? 下面我们接着来分析。

定义RequestFacade类,让其分别实现ServletRequest和HttpServletRequest这俩接口,同时又在其里面定义了一个私有成员变量(Request类型的变量),很显然,RequestFacade类里面方法的实现会调用Request成员变量里面的方法,也就是说RequestFacade类里面的方法其本质上还是使用Request类中的方法。然后,我们在去使用的时候,Tomcat会将RequestFacade对象上转为ServletRequest(或者HttpServletRequest)并传给Servlet里面的service方法,这样即使在Servlet中被下转为RequestFacade,咱们也不能访问私有成员变量对象(即Request对象)中的方法,因为该私有成员变量对象(即Request对象)是受保护的,是不能直接去使用的。

可见,现在是既用了Request对象里面的方法,又能防止其中方法被外界不合理的访问(这是因为Request对象里面的一些方法是想对外进行一个屏蔽的),感觉还行啊!嘻嘻😋

以上是关于从零开始学习Java设计模式 | 结构型模式篇:外观模式的主要内容,如果未能解决你的问题,请参考以下文章

从零开始学习Java设计模式 | 结构型模式篇:组合模式

从零开始学习Java设计模式 | 结构型模式篇:组合模式

从零开始学习Java设计模式 | 结构型模式篇:装饰者模式

从零开始学习Java设计模式 | 结构型模式篇:装饰者模式

从零开始学习Java设计模式 | 结构型模式篇:外观模式

从零开始学习Java设计模式 | 结构型模式篇:外观模式