[设计模式] Facade

意图

为子系统中的一组接口提供一个一致的界面, Facade模式定义了一个高层接口, 这个接口使得这一子系统更加容易使用.

适用性

在遇到以下情况使用Facade模式

  • 当你要为一个复杂子系统提供一个简单接口时. 子系统往往因为不断演化而变得越来越复杂. 大多数模式使用时都会产生更多更小的类. 这使得子系统更具可重用性, 也更容易对子系统进行定制, 但这也给那些不需要定制子系统的用户带来一些使用上的困难. Facade可以提供一个简单的缺省视图, 这一视图对大多数用户来说已经足够, 而那些需要更多的可定制性的用户可以越过facade层.
  • 客户程序与抽象类的实现部分之间存在着很大的依赖性. 引入facade将这个子系统与客户以及其他的子系统分离, 可以提高子系统的独立性和可移植性.
  • 当你需要构建一个层次结构的子系统时, 使用facade模式定义子系统中每层的入口点. 如果子系统之间是相互依赖的, 你可以让它们仅通过facade进行通讯, 从而简化了它们之间的依赖关系.

Head First

外观模式(facade)改变接口的目的是简化接口. 外观类没有对子系统进行封装, 只是提供了集合式简化的接口. 这其实是一个极朴素的概念, 不只是简化了接口, 也将客户从组件的子系统中解耦出来. 外观和适配器都可以包装多个类, 但是外观的意图在于简化接口, 而适配器的意图在于将接口转换成不同的接口.

家庭影院的外观模式例子, 将许许多多的组件动作结合在一起完成了一系列对用户来讲方便的方法:

public class HomeTheaterFacade {
    Amplifier amp;
    Tuner tuner;
    DvdPlayer dvd;
    CdPlayer cd;
    Projector projector;
    TheaterLights lights;
    Screen screen;
    PopcornPopper popper;
    public HomeTheaterFacade(Amplifier amp,
                 Tuner tuner,
                 DvdPlayer dvd,
                 CdPlayer cd,
                 Projector projector,
                 Screen screen,
                 TheaterLights lights,
                 PopcornPopper popper) {
        this.amp = amp;
        this.tuner = tuner;
        this.dvd = dvd;
        this.cd = cd;
        this.projector = projector;
        this.screen = screen;
        this.lights = lights;
        this.popper = popper;
    } //将每一个组件的引用传入该对象中
    public void watchMovie(String movie) {
        System.out.println("Get ready to watch a movie...");
        popper.on();
        popper.pop();
        lights.dim(10);
        screen.down();
        projector.on();
        projector.wideScreenMode();
        amp.on();
        amp.setDvd(dvd);
        amp.setSurroundSound();
        amp.setVolume(5);
        dvd.on();
        dvd.play(movie);
    }
    public void endMovie() {
        System.out.println("Shutting movie theater down...");
        popper.off();
        lights.on();
        screen.up();
        projector.off();
        amp.off();
        dvd.stop();
        dvd.eject();
        dvd.off();
    }
    public void listenToCd(String cdTitle) {
        System.out.println("Get ready for an audiopile experence...");
        lights.on();
        amp.on();
        amp.setVolume(5);
        amp.setCd(cd);
        amp.setStereoSound();
        cd.on();
        cd.play(cdTitle);
    }
    public void endCd() {
        System.out.println("Shutting down CD...");
        amp.off();
        amp.setCd(cd);
        cd.eject();
        cd.off();
    }
    public void listenToRadio(double frequency) {
        System.out.println("Tuning in the airwaves...");
        tuner.on();
        tuner.setFrequency(frequency);
        amp.on();
        amp.setVolume(5);
        amp.setTuner(tuner);
    }
    public void endRadio() {
        System.out.println("Shutting down the tuner...");
        tuner.off();
        amp.off();
    }
}

我们使用这个外观模式就可以简单舒适的看一场电影了:

public class HomeTheaterTestDrive {
    public static void main(String[] args) {
        Amplifier amp = new Amplifier("Top-O-Line Amplifier");
        Tuner tuner = new Tuner("Top-O-Line AM/FM Tuner", amp);
        DvdPlayer dvd = new DvdPlayer("Top-O-Line DVD Player", amp);
        CdPlayer cd = new CdPlayer("Top-O-Line CD Player", amp);
        Projector projector = new Projector("Top-O-Line Projector", dvd);
        TheaterLights lights = new TheaterLights("Theater Ceiling Lights");
        Screen screen = new Screen("Theater Screen");
        PopcornPopper popper = new PopcornPopper("Popcorn Popper");
        HomeTheaterFacade homeTheater =
                new HomeTheaterFacade(amp, tuner, dvd, cd,
                        projector, screen, lights, popper);
        homeTheater.watchMovie("Raiders of the Lost Ark");
        homeTheater.endMovie();
    }
}

我们由此引入一个新的OO原则——最少知识(Least Knowledge)原则(也就是传说中的Law of Demeter): 只和你的密友谈话. 也就是说, 当你正在设计一个系统时不管是任何对象, 你都要注意它所交互的类有哪些, 并注意它和这些类是如何交互的. 尽量减少类之间的依赖. 针对这个原则, 我们有一系列的准则可以遵循: 对于任何对象, 在其内部的方法中, 我们只应该调用属于以下范围的方法:

  • 该对象本身。
  • 被当做方法的参数而传递进来的对象。
  • 此方法所创建或实例化的任何对象。
  • 对象中的其他任何的对象成员和方法。

举一个汽车的例子:

public class car{
    Engine engine; // 类中的组件, 我们可以调用它的方法
    public Car(){
    }
    public void start(Key key){
        Doors doors = new Doors(); // 方法创建的对象, 我们可以调用它的方法
        boolean authorized = key.turns(); // 传入的参数, 我们可以调用它的方法
        if (authorized) {
            engine.start();
            updateDashboardDisplay(); // 对象中的方法, 我们可以调用
            doors.lock();
        }
    }
    public void updateDashboardDisplay(){
    }
}

不要对某个调用其他方法返回的对象进行方法的调用, 我们若是这么做就相当于向另一个对象的子部分发请求, 耦合性就会加大. 但是这个原则也会带来一些弊端, 导致更多的"包装"类被制造出来, 以处理和其他组件的沟通, 增加程序复杂度和运行时的性能.

我们再举书中一个练习题的例子仔细看看这个相当基本且重要的原则:

一个不符合这个原则的案例:

public class House{
    WeatherStation station;
    public float getTemp(){
        return station.getThermometer().getTemperature();
    }
}

这个getTemp方法中涉及了一个调用返回的对象.

而我们把这个方法拆开就可以得到一个符合这个原则的案例:

public class House{
    WeatherStation station;
    public float getTemp(){
        Thermometer thermometer = station.getThermometer();
        return getTempHelper(thermometer);
    }

    public float getTemperHelp(Thermometer thermometer){
        return thermometer.getTemperature();
    }
}

但是这有意义吗? 在这个简单的例子中恐怕是没有的. 其实我们经常用到违反该原则的例子, 比如System.out.println……, 这就告诉我们这并非是金科玉律. 有好有坏吧, 凡事有得有失, 在外观模式中, 我们看到HomeTheaterFacade类是遵循这个原则的, 这就主要带来好处: 一个组件的更换和升级不影响我们通过HomeTheaterTestDrive这个测试类中的主函数去轻松的看一场电影.

更多

维基: Facade pattern

举了一个电脑启动的例子. 我们只要按开机按钮, 但计算机内部帮我们做了一堆事情, 包括CPU、内存、硬盘等对象互相交互.