本文是软件构造系列的第四篇文章。其余文章在:
https://ruanx.net/tag/software-construction/

  想象这样一个场景:某用户到医院就诊,按照正常流程,他应该挂号、去门诊、化验、取药、缴费。这个过程中,他会与挂号窗口、门诊医生、化验员、药房、缴费窗口交互。如果这是他第一次去医院,显然连找到这些地点都非常繁琐。

  软件亦然。客户端如果需要与太多个类进行交互,事情会变得非常繁琐。这需要每个类都有极为详尽的文档;另一方面,如果系统接口改变,客户端也必须改变。

  对于医院而言,比较适当的方法是找几个学生志愿者做导诊员,带领客户去各种地方,客户只需要与导诊员沟通。在软件构造上,这就是 facade (外观)模式。

  笔者不太喜欢“外观模式”这个翻译,窃以为叫做“门面模式”更恰当。facade 模式的核心思想就是:假设系统是一个非常复杂的厂房,里面干着各种各样的事。叫客户自己去各个地方办事是不恰当的,客户并不太清楚厂房里面各个区域的功能。工厂的门口应当有一个接待员,客户只需要把自己的需求告诉接待员,由接待员进去帮他跑腿,办完事之后回到门口,把办事结果返回给客户。

  这里的接待员,是复杂系统的代理(proxy)。所有客户端与复杂系统的交互,都经过代理员来完成,客户端只需要与代理者交流、从代理者那里得到返回信息。


场景:todo list

  假设我开发了一款“工大 deadline”APP,用户都是工大的学生。既然是学生,那就有各种各样的 deadline 压身——可能是课程报告的,可能是作业的,可能是科研的。按常理,一个学生获取自己的 todo list,代码会写成这样:

package facade;

public class Main {
    public static void main(String[] args) {
        new CourseApp().getCourseDDL();            // 课程
        new HomeworkApp().recvHomeworkDeadLine();  // 作业
        new ResearchApp().paperToDo();             // 科研
    }
}

class CourseApp {
    public void getCourseDDL() {
        System.out.println("课程 : 明天上信息安全概论课");
        System.out.println("课程 : 后天上数值方法课");
    }
}

class HomeworkApp {
    public void recvHomeworkDeadLine() {
        System.out.println("作业 : 明天软构实验");
    }
}

class ResearchApp {
    public void paperToDo() {
        System.out.println("科研 : 明天看完某paper");
    }
}

  课程、作业、科研获取 deadline 的接口不一样。客户端程序想获得三类 deadline,必须知道后端的三个 App 的接口,这提高了学习成本。

  facade 模式的改进是:用一个类(外观角色),专门代理学生查询 deadline. 学生只需要指定是哪种类型的 deadline,外观角色就会帮学生调用对应的接口。代码如下:

package facade;

public class Main {
    public static void main(String[] args) {
        Facade proxy = new Facade();
        // 接口统一
        proxy.getDDL("course");
        proxy.getDDL("homework");
        proxy.getDDL("research");
    }
}

class Facade {
    void getDDL(String task) {
        if ("course".equals(task))
            new CourseApp().getCourseDDL();
        else if ("homework".equals(task))
            new HomeworkApp().recvHomeworkDeadLine();
        else
            new ResearchApp().paperToDo();
    }
}

class CourseApp {
    public void getCourseDDL() {
        System.out.println("课程 : 明天上信息安全概论课");
        System.out.println("课程 : 后天上数值方法课");
    }
}

class HomeworkApp {
    public void recvHomeworkDeadLine() {
        System.out.println("作业 : 明天软构实验");
    }
}

class ResearchApp {
    public void paperToDo() {
        System.out.println("科研 : 明天看完某paper");
    }
}

  可见,采用 facade 模式之后,客户端代码大大简化,只需要调用外观角色的 getDDL 这一个接口,就可以获得各种 deadline.

  facade 模式还带来了另一个优点:假设 HomeworkApp 的接口改变,那么客户端直接调用 HomeworkApp 的代码就会失效,必须重构。但是如果采用 facade 模式,只需要系统的发布者把 Facade 类的逻辑改掉,客户端代码不会受到影响。这对于一些被广泛使用的库,好处是显而易见的。