跳至主要內容

结构型外观模式

gavin-james设计模式设计模式大约 8 分钟

外观模式基本介绍

现实生活中,常常存在办事较复杂的例子,如办房产证或注册一家公司,有时要同多个部门联系,这时要是有一个综合部门能解决一切手续问题就好了。

软件设计也是这样,当一个系统的功能越来越强,子系统会越来越多,客户对系统的访问也变得越来越复杂。这时如果系统内部发生改变,客户端也要跟着改变,这违背了「开闭原则」,也违背了「迪米特法则」,所以有必要为多个子系统提供一个统一的接口,从而降低系统的耦合度,这就是外观模式的目标。

外观模式(Facade Pattern),也叫 过程模式门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。

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

外观(Facade)模式是 迪米特法则 的典型应用。

主要优点

  • 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类
  • 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易
  • 降低了大型软件系统中的编译依赖性,简化了系统在不同平台之间的移植过程,因为编译一个子系统不会影响其他的子系统,也不会影响外观对象

主要缺点

  • 不能很好地限制客户使用子系统类,很容易带来未知风险
  • 增加新的子系统可能需要修改外观类或客户端的源代码,违背了 开闭原则

外观模式的结构与实现

外观(Facade)模式的结构比较简单,主要是定义了一个高层接口。它包含了对各个子系统的引用,客户端可以通过它访问各个子系统的功能。现在来分析其基本结构和实现方法。

模式的结构

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

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

模式的实现

// 子系统角色
class SubSystem01 {
    public void method1() {
        System.out.println("子系统01的method1()被调用!");
    }
}
// 子系统角色
class SubSystem02 {
    public void method2() {
        System.out.println("子系统02的method2()被调用!");
    }
}
// 子系统角色
class SubSystem03 {
    public void method3() {
        System.out.println("子系统03的method3()被调用!");
    }
}
// 外观角色
class Facade {
    private SubSystem01 obj1 = new SubSystem01();
    private SubSystem02 obj2 = new SubSystem02();
    private SubSystem03 obj3 = new SubSystem03();
    public void method() {
        obj1.method1();
        obj2.method2();
        obj3.method3();
    }
}
// 客户角色
public class FacadePattern {
    public static void main(String[] args) {
        Facade f = new Facade();
        f.method();
    }
}

影院管理项目

组建一个家庭影院:DVD 播放器、投影仪、自动屏幕、环绕立体声、爆米花机,灯光。

过程:开爆米花机、开爆米花机、开投影仪、开音响、开 DVD、选 DVD、去拿爆米花、调暗灯光、播放、观影结束后,关闭各种设备。

传统方式解决影院管理

传统方式解决问题图

image-20220314175108493
image-20220314175108493

传统方式解决问题分析

在 ClientTest 的 main 方法中,创建各个子系统的对象,并直接去调用子系统(对象)相关方法,会造成调用过程混乱,没有清晰的过程,不利于在 ClientTest 中,去维护对子系统的操作

解决思路:定义一个高层接口,给子系统中的一组接口提供一个一致的界面(比如在高层接口提供四个方法 ready, play, pause, end),用来访问子系统中的一群接口。

也就是说就是通过定义一个一致的接口(界面类),用以屏蔽内部子系统的细节,使得调用端只需跟这个接口发生调用,而无需关心这个子系统的内部细节,即使用 外观模式

外观模式解决影院管理

外观模式可以理解为转换一群接口,客户只要调用一个接口,而不用调用多个接口才能达到目的。比如:在 PC 上安装软件的时候经常有一键安装选项(省去选择安装目录、安装的组件等等),还有就是手机的重启功能(把关机和启动合为一个操作)。

外观模式就是解决多个复杂接口带来的使用困难,起到简化用户操作的作用。

示意图:

image-20220314175728964
image-20220314175728964

影院管理项目类图:

image-20220314175754422
image-20220314175754422

代码示例:

影院的 DVD 播放器、投影仪、自动屏幕、环绕立体声、爆米花机、灯光类

// DVD 播放器
public class DVDPlayer {

    // 使用单例模式, 使用饿汉式
    private static DVDPlayer instance = new DVDPlayer();

    public static DVDPlayer getInstanc() {
        return instance;
    }

    public void on() {
        System.out.println(" dvd on ");
    }

    public void off() {
        System.out.println(" dvd off ");
    }

    public void play() {
        System.out.println(" dvd is playing ");
    }

    // ....
    public void pause() {
        System.out.println(" dvd pause ..");
    }
}
// 投影仪
public class Projector {

    private static Projector instance = new Projector();

    public static Projector getInstance() {
        return instance;
    }

    public void on() {
        System.out.println(" Projector on ");
    }

    public void off() {
        System.out.println(" Projector ff ");
    }

    public void focus() {
        System.out.println(" Projector is Projector  ");
    }
    //...
}
// 自动屏幕
public class Screen {

    private static Screen instance = new Screen();

    public static Screen getInstance() {
        return instance;
    }

    public void up() {
        System.out.println(" Screen up ");
    }

    public void down() {
        System.out.println(" Screen down ");
    }
}
// 爆米花机
public class Popcorn {

    private static Popcorn instance = new Popcorn();

    public static Popcorn getInstance() {
        return instance;
    }

    public void on() {
        System.out.println(" popcorn on ");
    }

    public void off() {
        System.out.println(" popcorn ff ");
    }

    public void pop() {
        System.out.println(" popcorn is poping  ");
    }
}
// 环绕立体声
public class Stereo {

    private static Stereo instance = new Stereo();

    public static Stereo getInstance() {
        return instance;
    }

    public void on() {
        System.out.println(" Stereo on ");
    }

    public void off() {
        System.out.println(" Screen off ");
    }

    public void up() {
        System.out.println(" Screen up.. ");
    }
    //...
}
// 灯光
public class TheaterLight {

    private static TheaterLight instance = new TheaterLight();

    public static TheaterLight getInstance() {
        return instance;
    }

    public void on() {
        System.out.println(" TheaterLight on ");
    }

    public void off() {
        System.out.println(" TheaterLight off ");
    }

    public void dim() {
        System.out.println(" TheaterLight dim.. ");
    }

    public void bright() {
        System.out.println(" TheaterLight bright.. ");
    }
}

每一步的过程类

public class HomeTheaterFacade {

    // 定义各个子系统对象
    private TheaterLight theaterLight;
    private Popcorn popcorn;
    private Stereo stereo;
    private Projector projector;
    private Screen screen;
    private DVDPlayer dVDPlayer;


    // 构造器
    public HomeTheaterFacade() {
        super();
        this.theaterLight = TheaterLight.getInstance();
        this.popcorn = Popcorn.getInstance();
        this.stereo = Stereo.getInstance();
        this.projector = Projector.getInstance();
        this.screen = Screen.getInstance();
        this.dVDPlayer = DVDPlayer.getInstanc();
    }

    // 操作分成 4 步
    public void ready() {
        popcorn.on();
        popcorn.pop();
        screen.down();
        projector.on();
        stereo.on();
        dVDPlayer.on();
        theaterLight.dim();
    }

    public void play() {
        dVDPlayer.play();
    }

    public void pause() {
        dVDPlayer.pause();
    }

    public void end() {
        popcorn.off();
        theaterLight.bright();
        screen.up();
        projector.off();
        stereo.off();
        dVDPlayer.off();
    }
}

测试类:

public class Client {

    public static void main(String[] args) {
        HomeTheaterFacade homeTheaterFacade = new HomeTheaterFacade();
        homeTheaterFacade.ready();
        homeTheaterFacade.play();

        homeTheaterFacade.end();
    }
}

MyBatis 框架的外观模式剖析

MyBatis 中的 Configuration 去创建 MetaObject 对象使用到外观模式。

image-20220314180358602
image-20220314180358602

对源码中使用到的外观模式的角色类图

image-20220314180442486
image-20220314180442486

外观模式的注意事项和细节

  • 外观模式 对外屏蔽了子系统的细节,因此外观模式降低了客户端对子系统使用的复杂性

  • 外观模式对客户端与子系统的耦合关系:解耦,让子系统内部的模块更易维护和扩展

  • 通过合理的使用外观模式,可以帮我们更好的 划分访问的层次

  • 当系统需要进行分层设计时,可以考虑使用 Facade 模式

  • 在维护一个遗留的大型系统时,可能这个系统已经变得非常难以维护和扩展,此时可以考虑为新系统开发一个 Facade 类,来提供遗留系统的比较清晰简单的接口,让新系统与 Facade 类交互,提高复用性

  • 不能过多的或者不合理的使用外观模式,使用外观模式好,还是直接调用模块好。要以让系统有层次,利于维护为目的

外观模式的应用场景

通常在以下情况下可以考虑使用外观模式:

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

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

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