外观模式
发表于更新于
设计模式外观模式
Rif设计模式笔记:外观模式 (Facade Pattern)
一、一句话概括
为一整个复杂的子系统提供一个单一的、简化的接口。它就像一个“服务总台”,你只需要告诉它你要做什么,它会帮你协调内部所有部门来完成任务。
二、为什么需要它 (Why - The Pain Point)
现代软件系统通常由许多相互协作的类和模块组成,形成一个复杂的子系统。例如,一个家庭影院系统可能包含以下组件:
- 投影仪 (
Projector)
- DVD 播放器 (
DvdPlayer)
- 功放 (
Amplifier)
- 灯光 (
TheaterLights)
- 幕布 (
Screen)
如果你想看一场电影,你需要执行一系列复杂的操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| Projector projector = new Projector(); DvdPlayer dvdPlayer = new DvdPlayer(); Amplifier amp = new Amplifier(); TheaterLights lights = new TheaterLights(); Screen screen = new Screen();
lights.dim(10); screen.down(); projector.on(); projector.wideScreenMode(); amp.on(); amp.setDvd(dvdPlayer); amp.setSurroundSound(); amp.setVolume(5); dvdPlayer.on(); dvdPlayer.play("Inception");
|
这种方式存在严重问题:
- 客户端代码复杂:客户端需要知道子系统中所有组件的细节以及它们正确的交互顺序。这增加了客户端的负担,也让代码变得臃肿和难以维护。
- 紧密耦合:客户端与子系统中的每一个类都产生了依赖关系。如果将来子系统升级(比如
Amplifier 换成了 SonosAmp),所有使用到它的客户端代码都需要修改。
核心问题:如何为客户端提供一个简单易用的入口,同时将客户端与子系统的复杂内部结构解耦?
三、它是什么 (What - The Solution)
外观模式通过引入一个外观类(Facade Class) 来解决这个问题。这个外观类是子系统的唯一入口点。
核心思想和角色:
- **外观 (Facade)**:
- 这是模式的核心。它是一个单独的类。
- 它了解子系统中所有组件的功能和职责。
- 它提供了一些高层的、简化的方法(如
watchMovie(), endMovie())。
- 当客户端调用这些高层方法时,外观类会在内部委托子系统中的各个组件去执行一系列复杂的操作。
- **子系统类 (Subsystem Classes)**:
- 构成复杂系统的所有类和模块。
- 它们负责实现具体的功能,但不知道外观类的存在。
- 客户端可以选择绕过外观类直接与子系统交互,但通常不推荐这样做。
工作流程:
客户端不再需要直接操作 Projector, DvdPlayer 等,而是只与 HomeTheaterFacade 交互。
client.watchMovie("Inception") -> facade 内部执行上述 10 个步骤。
外观模式 vs. 适配器模式
- 意图不同:
- 外观:意图是简化接口。它为整个子系统提供一个更易用的新接口。
- 适配器:意图是转换接口。它只是将一个已有的接口包装成另一个不同的接口。
- 封装性:外观模式封装了整个子系统的复杂性,而适配器只是封装了一个对象。
四、怎么做 (How - The Blueprint)
基本步骤(以家庭影院为例):
- 识别复杂的子系统:找出所有相关的组件类(
Projector, DvdPlayer 等)。
- **创建外观类
HomeTheaterFacade**。
- 在外观类的构造函数中,创建或接收所有子系统组件的实例。
- 在外观类中,创建一些高层方法,如
watchMovie(String movie)。
- 在这些高层方法的实现中,按照正确的顺序调用子系统组件的相应方法。
- 让客户端代码只与
HomeTheaterFacade 交互。
Java 示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
| class Amplifier { public void on() { System.out.println("Amplifier on"); } public void off() { System.out.println("Amplifier off"); } public void setDvd(DvdPlayer dvd) { System.out.println("Amplifier setting DVD player"); } public void setSurroundSound() { System.out.println("Amplifier surround sound on (5 speakers, 1 subwoofer)"); } public void setVolume(int level) { System.out.println("Amplifier setting volume to " + level); } }
class DvdPlayer { public void on() { System.out.println("DVD Player on"); } public void off() { System.out.println("DVD Player off"); } public void play(String movie) { System.out.println("DVD Player playing \"" + movie + "\""); } public void stop() { System.out.println("DVD Player stopped"); } public void eject() { System.out.println("DVD Player eject"); } }
class Projector { public void on() { System.out.println("Projector on"); } public void off() { System.out.println("Projector off"); } public void wideScreenMode() { System.out.println("Projector in widescreen mode (16x9 aspect ratio)"); } }
class Screen { public void up() { System.out.println("Theater Screen going up"); } public void down() { System.out.println("Theater Screen going down"); } }
class TheaterLights { public void on() { System.out.println("Theater ceiling lights on"); } public void dim(int level) { System.out.println("Theater ceiling lights dimming to " + level + "%"); } }
class HomeTheaterFacade { private Amplifier amp; private DvdPlayer dvd; private Projector projector; private Screen screen; private TheaterLights lights;
public HomeTheaterFacade(Amplifier amp, DvdPlayer dvd, Projector projector, Screen screen, TheaterLights lights) { this.amp = amp; this.dvd = dvd; this.projector = projector; this.screen = screen; this.lights = lights; }
public void watchMovie(String movie) { System.out.println("Get ready to watch a movie..."); 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("\nShutting movie theater down..."); lights.on(); screen.up(); projector.off(); amp.off(); dvd.stop(); dvd.eject(); dvd.off(); } }
public class FacadePatternDemo { public static void main(String[] args) { Amplifier amp = new Amplifier(); DvdPlayer dvd = new DvdPlayer(); Projector projector = new Projector(); Screen screen = new Screen(); TheaterLights lights = new TheaterLights(); HomeTheaterFacade homeTheater = new HomeTheaterFacade(amp, dvd, projector, screen, lights); homeTheater.watchMovie("Inception"); homeTheater.endMovie(); } }
|
五、UML 类图
下面是上述例子的 PlantUML 代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| @startuml skinparam classAttributeIconSize 0 hide empty members
class HomeTheaterFacade { + void watchMovie(String movie) + void endMovie() }
package Subsystem { class Amplifier {} class DvdPlayer {} class Projector {} class Screen {} class TheaterLights {} }
class FacadePatternDemo {}
' Facade uses Subsystem classes HomeTheaterFacade --|> Amplifier HomeTheaterFacade --|> DvdPlayer HomeTheaterFacade --|> Projector HomeTheaterFacade --|> Screen HomeTheaterFacade --|> TheaterLights
' Client uses Facade FacadePatternDemo ..> HomeTheaterFacade : uses
note "Client does not directly\ninteract with the Subsystem" as N1 FacadePatternDemo . N1 N1 . Subsystem @enduml
|
图示解读:
HomeTheaterFacade 是一个中心节点。
- 它与
Subsystem 包中的所有类都有关联(这里用虚线箭头表示“使用”关系更准确,但为了图示清晰,表示依赖关系即可)。
- 客户端
FacadePatternDemo 只与 HomeTheaterFacade 交互,完全与复杂的子系统解耦。
六、优缺点分析
优点 (Pros):
- 极大简化客户端:客户端无需了解子系统的内部复杂性,只需与外观类交互。
- 解耦客户端与子系统:客户端与子系统之间不再有紧密的依赖关系。子系统的内部实现可以随意修改,只要外观类的接口不变,就不会影响到客户端。
- 提供了分层结构:外观模式可以帮助你为系统分层。你可以用外观来定义每一层的入口,层与层之间只通过外观进行通信。
- 不限制高级用户:外观模式并不阻止有特殊需求的客户端直接访问子系统。它只是提供了一个便捷的“快捷方式”。
缺点 (Cons):
- **可能成为“上帝对象” (God Object)**:如果一个外观类承担了过多的职责,它可能会变得非常臃肿,违反单一职责原则。
- 不符合开闭原则:如果需要为子系统增加新的功能,可能需要修改外观类的代码。
七、在 Java 中的应用
外观模式是一种编程思想,在各种框架和库中都有体现,即使没有明确命名为 “Facade”。
- SLF4J (Simple Logging Facade for Java): SLF4J 本身的名字就带了 “Facade”。它为底层各种复杂的日志系统(Log4j, Logback, JUL 等)提供了一个统一、简单的
Logger 接口。你的应用程序只需要面向 SLF4J 编程,就可以轻松地在不同的日志实现之间切换。
- JDBC (Java Database Connectivity): 在某种程度上,
java.sql.DriverManager 或 javax.sql.DataSource 也可以看作是数据库连接过程的外观。你只需要调用 getConnection(),它内部会处理驱动加载、协议通信等一系列复杂操作。
- Spring 框架: Spring 框架中的许多模板类,如
JdbcTemplate, RestTemplate,都扮演了外观的角色。例如,JdbcTemplate 封装了 JDBC 繁琐的资源管理(连接获取、Statement 创建、异常处理、资源关闭),让你只需要专注于编写 SQL 和处理结果集。