外观模式(Facade Pattern)是一种结构型设计模式,它为子系统中的一组接口提供了一个统一的高层接口,使得子系统更加容易使用。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
外观模式的主要作用有以下几点:
简化系统的调用复杂性。通过外观模式,客户端可以只需调用外观类中的方法就可以完成复杂的操作,无需深入了解子系统的内部工作机制。
减小系统的编译依赖。通过外观模式,客户端只需要与外观类发生编译依赖,而无须与子系统的其他模块发生直接依赖。
有利于体系结构的拓展。在有新的子系统加入时,只需创建一个新的外观类,客户端无须修改源代码,减少了客户端与子系统的耦合关系。
外观模式主要包含以下几种角色:
外观(Facade)角色:外观角色需要知道所有子系统的功能和职责,它是一个独立的模块,它与系统中的其他模块一起一起,构成了一个更大的系统。
子系统(Sub System)角色:子系统角色实现系统的部分功能,并可以和其他子系统协作以完成更复杂的功能。子系统角色不需要知道外观的存在。
客户(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
类充当了外观角色,它封装了子系统SubSystemA
、SubSystemB
和SubSystemC
的功能,并提供了一个简单的operation()
方法供客户端调用。客户端只需要与外观Facade
对象交互,而不需要了解子系统的内部细节。
外观模式的优点包括:
缺点包括:
需求:一个家庭影院系统,它包含了音响系统、投影仪系统和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
类中,我们提供了watchMovie
和endMovie
两个方法,分别用于启动和关闭家庭影院系统。这些方法封装了对各个子系统的调用,简化了系统的使用复杂度。
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
对象交互,通过调用watchMovie
和endMovie
方法即可控制整个家庭影院系统。
使用外观模式,我们将复杂的子系统操作封装在HomeTheaterFacade
类中,客户端无需了解各个子系统的内部细节,从而降低了系统的使用复杂度。同时,如果需要增加或修改子系统,只需要修改外观类,而无需更改客户端代码,提高了系统的可维护性和可扩展性。
Java I/O库中广泛使用了外观模式。例如java.io.File
类就是一个外观,它提供了对文件系统进行操作的简化方法,而无需直接面对复杂的操作系统底层API。
File file = new File("example.txt");
file.createNewFile(); // 创建新文件
file.delete(); // 删除文件
通过File
对象,我们可以方便地执行文件的创建、删除等操作,而不用关心底层的具体实现细节。
Java JDBC中的DriverManager
类充当了数据库连接的外观角色。它封装了获取数据库连接的复杂过程,为我们提供了一个简单的接口。
Connection conn = DriverManager.getConnection(url, username, password);
通过DriverManager.getConnection()
方法,我们可以获取到一个数据库连接对象,而不需要关注加载驱动、创建连接等繁琐步骤。
Spring框架中的ApplicationContext
接口可以看作是一个外观。它为开发者提供了获取Spring Bean的统一入口,而隐藏了Bean的创建、配置、装配等复杂细节。
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
MyService service = (MyService) context.getBean("myService");
通过ApplicationContext
对象,我们可以方便地获取Spring管理的Bean实例,而无需了解Spring内部的工作机制。
在JavaEE的Servlet规范中,ServletRequest
和ServletResponse
接口可以看作是请求和响应对象的外观。它们为开发者提供了一系列方法来访问HTTP请求和响应的各种属性,而无需直接处理底层的HTTP协议细节。
protected void doGet(HttpServletRequest request, HttpServletResponse response) {
String param = request.getParameter("name");
response.setContentType("text/html");
// ...
}
通过ServletRequest
和ServletResponse
对象,我们可以方便地获取请求参数、设置响应头等,而无需关注HTTP协议的具体实现细节。
ApplicationContext
接口可以被视为一个外观(Facade)模式的典型应用。它为开发者提供了一个统一的入口来访问Spring容器中的Bean实例,而隐藏了Bean的创建、装配、初始化等复杂细节。分析一下ApplicationContext
接口的源码实现,以深入理解它是如何运用外观模式的。
ApplicationContext
接口继承自BeanFactory
接口,它定义了一些基本的方法,如getBean()
、containsBean()
等,用于获取和检查容器中的Bean。但是,ApplicationContext
接口还提供了一些额外的功能,如访问资源文件、发布事件等。这些功能由ApplicationContext
接口的不同实现类完成,如ClassPathXmlApplicationContext
、AnnotationConfigApplicationContext
等。
我们以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容器实现之间的耦合度,提高了代码的可维护性和可扩展性。