外观模式

  • 一、介绍
  • 二、家庭影院项目案例使用
  • 三、Java API或框架中应用分析
  • 三、Spring框架ApplicationContext源码


一、介绍

外观模式(Facade Pattern)是一种结构型设计模式,它为子系统中的一组接口提供了一个统一的高层接口,使得子系统更加容易使用。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

外观模式的主要作用有以下几点:

  1. 简化系统的调用复杂性。通过外观模式,客户端可以只需调用外观类中的方法就可以完成复杂的操作,无需深入了解子系统的内部工作机制。
  2. 减小系统的编译依赖。通过外观模式,客户端只需要与外观类发生编译依赖,而无须与子系统的其他模块发生直接依赖。
  3. 有利于体系结构的拓展。在有新的子系统加入时,只需创建一个新的外观类,客户端无须修改源代码,减少了客户端与子系统的耦合关系。

外观模式主要包含以下几种角色:

  1. 外观(Facade)角色:外观角色需要知道所有子系统的功能和职责,它是一个独立的模块,它与系统中的其他模块一起一起,构成了一个更大的系统。
  2. 子系统(Sub System)角色:子系统角色实现系统的部分功能,并可以和其他子系统协作以完成更复杂的功能。子系统角色不需要知道外观的存在。
  3. 客户(Client)角色:客户端通过外观模式访问子系统的功能。

下面是一个外观模式的简单示例:

// 子系统角色
class SubSystemA {
    public void operationA() {
        System.out.println("SubSystemA.operationA()");
    }
}

class SubSystemB {
    public void operationB() {
        System.out.println("SubSystemB.operationB()");
    }
}

class SubSystemC {
    public void operationC() {
        System.out.println("SubSystemC.operationC()");
    }
}

// 外观角色
class Facade {
    private SubSystemA a = new SubSystemA();
    private SubSystemB b = new SubSystemB();
    private SubSystemC c = new SubSystemC();

    public void operation() {
        a.operationA();
        b.operationB();
        c.operationC();
    }
}

// 客户端
public class Client {
    public static void main(String[] args) {
        Facade facade = new Facade();
        facade.operation();
    }
}

运行结果:

SubSystemA.operationA()
SubSystemB.operationB()
SubSystemC.operationC()

在这个示例中,Facade类充当了外观角色,它封装了子系统SubSystemASubSystemBSubSystemC的功能,并提供了一个简单的operation()方法供客户端调用。客户端只需要与外观Facade对象交互,而不需要了解子系统的内部细节。

外观模式的优点包括:

  1. 减少了系统与客户端的耦合度,使系统更加容易移植和维护。
  2. 通过为复杂子系统提供一个统一的入口,降低了客户端的使用难度。
  3. 客户端代码更加简洁,系统更加易于理解和维护。

缺点包括:

  1. 不符合开闭原则,如果增加新的子系统功能,可能需要修改外观类源码。
  2. 外观类的编写比较困难,需要全面了解子系统的功能和职责。

二、家庭影院项目案例使用

需求:一个家庭影院系统,它包含了音响系统、投影仪系统和DVD播放器系统等子系统。我们需要提供一个统一的接口,让用户可以方便地控制整个家庭影院系统。

1. 定义子系统

首先,我们定义音响系统、投影仪系统和DVD播放器系统的接口和实现类:

// 音响系统
interface AudioSystem {
    void turnOn();
    void turnOff();
    void setVolume(int volume);
}

class AudioSystemImpl implements AudioSystem {
    // 实现具体的音响系统操作
}

// 投影仪系统
interface ProjectorSystem {
    void turnOn();
    void turnOff();
    void setInput(String input);
}

class ProjectorSystemImpl implements ProjectorSystem {
    // 实现具体的投影仪系统操作
}

// DVD播放器系统
interface DVDPlayer {
    void turnOn();
    void turnOff();
    void play(String movie);
}

class DVDPlayerImpl implements DVDPlayer {
    // 实现具体的DVD播放器操作
}

2. 定义外观类

接下来,我们定义一个HomeTheaterFacade类作为外观,它封装了音响系统、投影仪系统和DVD播放器系统的操作:

class HomeTheaterFacade {
    private AudioSystem audioSystem;
    private ProjectorSystem projectorSystem;
    private DVDPlayer dvdPlayer;

    public HomeTheaterFacade(AudioSystem audioSystem, ProjectorSystem projectorSystem, DVDPlayer dvdPlayer) {
        this.audioSystem = audioSystem;
        this.projectorSystem = projectorSystem;
        this.dvdPlayer = dvdPlayer;
    }

    public void watchMovie(String movie) {
        audioSystem.turnOn();
        projectorSystem.turnOn();
        projectorSystem.setInput("DVD");
        dvdPlayer.turnOn();
        dvdPlayer.play(movie);
    }

    public void endMovie() {
        audioSystem.turnOff();
        projectorSystem.turnOff();
        dvdPlayer.turnOff();
    }
}

HomeTheaterFacade类中,我们提供了watchMovieendMovie两个方法,分别用于启动和关闭家庭影院系统。这些方法封装了对各个子系统的调用,简化了系统的使用复杂度。

3. 使用外观类

最后,在客户端代码中,我们可以直接使用HomeTheaterFacade类来控制整个家庭影院系统:

public class Client {
    public static void main(String[] args) {
        AudioSystem audioSystem = new AudioSystemImpl();
        ProjectorSystem projectorSystem = new ProjectorSystemImpl();
        DVDPlayer dvdPlayer = new DVDPlayerImpl();

        HomeTheaterFacade homeTheater = new HomeTheaterFacade(audioSystem, projectorSystem, dvdPlayer);

        homeTheater.watchMovie("机器人总动员");
        // 观看电影...
        homeTheater.endMovie();
    }
}

在上面的代码中,我们创建了音响系统、投影仪系统和DVD播放器系统的实例,然后将它们传递给HomeTheaterFacade构造函数。客户端只需要与HomeTheaterFacade对象交互,通过调用watchMovieendMovie方法即可控制整个家庭影院系统。

使用外观模式,我们将复杂的子系统操作封装在HomeTheaterFacade类中,客户端无需了解各个子系统的内部细节,从而降低了系统的使用复杂度。同时,如果需要增加或修改子系统,只需要修改外观类,而无需更改客户端代码,提高了系统的可维护性和可扩展性。

三、Java API或框架中应用分析

  1. Java I/O库

Java I/O库中广泛使用了外观模式。例如java.io.File类就是一个外观,它提供了对文件系统进行操作的简化方法,而无需直接面对复杂的操作系统底层API。

File file = new File("example.txt");
file.createNewFile(); // 创建新文件
file.delete(); // 删除文件

通过File对象,我们可以方便地执行文件的创建、删除等操作,而不用关心底层的具体实现细节。

  1. Java 数据库连接(JDBC)

Java JDBC中的DriverManager类充当了数据库连接的外观角色。它封装了获取数据库连接的复杂过程,为我们提供了一个简单的接口。

Connection conn = DriverManager.getConnection(url, username, password);

通过DriverManager.getConnection()方法,我们可以获取到一个数据库连接对象,而不需要关注加载驱动、创建连接等繁琐步骤。

  1. Spring框架

Spring框架中的ApplicationContext接口可以看作是一个外观。它为开发者提供了获取Spring Bean的统一入口,而隐藏了Bean的创建、配置、装配等复杂细节。

ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
MyService service = (MyService) context.getBean("myService");

通过ApplicationContext对象,我们可以方便地获取Spring管理的Bean实例,而无需了解Spring内部的工作机制。

  1. Java Servlet

在JavaEE的Servlet规范中,ServletRequestServletResponse接口可以看作是请求和响应对象的外观。它们为开发者提供了一系列方法来访问HTTP请求和响应的各种属性,而无需直接处理底层的HTTP协议细节。

protected void doGet(HttpServletRequest request, HttpServletResponse response) {
    String param = request.getParameter("name");
    response.setContentType("text/html");
    // ...
}

通过ServletRequestServletResponse对象,我们可以方便地获取请求参数、设置响应头等,而无需关注HTTP协议的具体实现细节。

三、Spring框架ApplicationContext源码

ApplicationContext接口可以被视为一个外观(Facade)模式的典型应用。它为开发者提供了一个统一的入口来访问Spring容器中的Bean实例,而隐藏了Bean的创建、装配、初始化等复杂细节。分析一下ApplicationContext接口的源码实现,以深入理解它是如何运用外观模式的。

ApplicationContext接口继承自BeanFactory接口,它定义了一些基本的方法,如getBean()containsBean()等,用于获取和检查容器中的Bean。但是,ApplicationContext接口还提供了一些额外的功能,如访问资源文件、发布事件等。这些功能由ApplicationContext接口的不同实现类完成,如ClassPathXmlApplicationContextAnnotationConfigApplicationContext等。

我们以ClassPathXmlApplicationContext为例,看一下它的实现细节:

public class ClassPathXmlApplicationContext extends AbstractXmlApplicationContext {
    // ...

    @Override
    protected Resource getResourceByPath(String path) {
        // 获取classpath资源
        return new ClassPathResource(path);
    }

    // ...
}

public abstract class AbstractXmlApplicationContext extends AbstractRefreshableConfigApplicationContext {
    // ...

    @Override
    protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
        // 加载Bean定义
        XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
        beanDefinitionReader.loadBeanDefinitions(getConfigResources());
    }

    // ...
}

从上面的源码可以看出,ClassPathXmlApplicationContext实现了getResourceByPath()方法,用于从classpath中加载资源文件。而loadBeanDefinitions()方法则负责从资源文件中加载Bean定义。这些复杂的实现细节都被封装在ApplicationContext接口的具体实现类中,对外部客户端来说是透明的。

客户端只需要直接使用ApplicationContext接口提供的方法即可,如:

ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
MyService service = context.getBean("myService", MyService.class);

在上面的代码中,客户端仅需创建一个ApplicationContext实例,并通过getBean()方法获取所需的Bean实例,而不必关心Bean的创建、装配、初始化等复杂过程。

从这个角度来看,ApplicationContext接口扮演了外观角色,它为客户端提供了一个统一的入口来访问Spring容器中的Bean,同时隐藏了Bean加载和管理的复杂细节。这种设计有效地降低了客户端代码与Spring容器实现之间的耦合度,提高了代码的可维护性和可扩展性。