PHP 外观模式(Facade Pattern)— 简化复杂系统调用
什么是外观模式?
外观模式提供了一个统一的简化接口,用来访问子系统中的一群接口,它为子系统中的一组接口提供一个一致的界面,使得子系统更容易使用。

用通俗的话说:外观模式就是给复杂的系统提供一个"门面",让客户端只需要和门面打交道,而不需要了解系统内部复杂的交互逻辑。
核心结构
┌─────────────────────────────────────────┐
│ Client(客户端) │
└──────────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Facade(外观类) │
│ - 知道哪些子系统负责处理请求 │
│ - 将客户端请求代理给适当的子系统对象 │
└──────────────────┬──────────────────────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 子系统A │ │ 子系统B │ │ 子系统C │
└─────────┘ └─────────┘ └─────────┘
代码示例
场景:家庭影院系统
<?php
// ==================== 子系统类 ====================
class Amplifier {
public function on() {
echo "功放:已开启<br>";
}
public function setVolume($level) {
echo "功放:音量设置为 {$level}<br>";
}
public function off() {
echo "功放:已关闭<br>";
}
}
class DVDPlayer {
public function on() {
echo "DVD播放器:已开启<br>";
}
public function play($movie) {
echo "DVD播放器:正在播放《{$movie}》<br>";
}
public function off() {
echo "DVD播放器:已关闭<br>";
}
}
class Projector {
public function on() {
echo "投影仪:已开启<br>";
}
public function setInput($source) {
echo "投影仪:输入源切换到 {$source}<br>";
}
public function off() {
echo "投影仪:已关闭<br>";
}
}
class Screen {
public function down() {
echo "屏幕:已放下<br>";
}
public function up() {
echo "屏幕:已收起<br>";
}
}
class Lights {
public function dim($level) {
echo "灯光:调暗至 {$level}%<br>";
}
public function on() {
echo "灯光:已全亮<br>";
}
}
// ==================== 外观类 ====================
class HomeTheaterFacade {
private $amplifier;
private $dvdPlayer;
private $projector;
private $screen;
private $lights;
public function __construct(
Amplifier $amplifier,
DVDPlayer $dvdPlayer,
Projector $projector,
Screen $screen,
Lights $lights
) {
$this->amplifier = $amplifier;
$this->dvdPlayer = $dvdPlayer;
$this->projector = $projector;
$this->screen = $screen;
$this->lights = $lights;
}
// 简化看电影的完整流程
public function watchMovie($movie) {
echo "===== 开始观影:{$movie} =====<br>";
// 内部处理各子系统的复杂交互
$this->lights->dim(10); // 调暗灯光
$this->screen->down(); // 放下屏幕
$this->projector->on(); // 开启投影仪
$this->projector->setInput('DVD'); // 切换输入源
$this->amplifier->on(); // 开启功放
$this->amplifier->setVolume(20); // 设置音量
$this->dvdPlayer->on(); // 开启播放器
$this->dvdPlayer->play($movie); // 播放电影
echo "===== 观影准备完成 =====<br><br>";
}
// 简化结束观影的流程
public function endMovie() {
echo "===== 结束观影 =====<br>";
$this->dvdPlayer->off(); // 关闭播放器
$this->amplifier->off(); // 关闭功放
$this->projector->off(); // 关闭投影仪
$this->screen->up(); // 收起屏幕
$this->lights->on(); // 打开灯光
echo "===== 观影结束,恢复原状 =====<br><br>";
}
}
// ==================== 客户端使用 ====================
// 不使用外观模式时,客户端需要知道所有细节
function withoutFacade($movie) {
echo "=== 不使用外观模式的观影流程 ===<br>";
$amplifier = new Amplifier();
$dvdPlayer = new DVDPlayer();
$projector = new Projector();
$screen = new Screen();
$lights = new Lights();
// 客户端必须了解所有子系统的交互细节
$lights->dim(10);
$screen->down();
$projector->on();
$projector->setInput('DVD');
$amplifier->on();
$amplifier->setVolume(20);
$dvdPlayer->on();
$dvdPlayer->play($movie);
// 结束也需要逐个关闭
$dvdPlayer->off();
$amplifier->off();
$projector->off();
$screen->up();
$lights->on();
echo "<br>";
}
// 使用外观模式
function withFacade($movie) {
echo "=== 使用外观模式 ===<br>";
$homeTheater = new HomeTheaterFacade(
new Amplifier(),
new DVDPlayer(),
new Projector(),
new Screen(),
new Lights()
);
// 只需调用一个方法即可
$homeTheater->watchMovie($movie);
$homeTheater->endMovie();
}
// 测试
withoutFacade("星际穿越");
withFacade("星际穿越");
输出结果:
=== 不使用外观模式的观影流程 ===
灯光:调暗至 10%
屏幕:已放下
投影仪:已开启
投影仪:输入源切换到 DVD
功放:已开启
功放:音量设置为 20
DVD播放器:已开启
DVD播放器:正在播放《星际穿越》
DVD播放器:已关闭
功放:已关闭
投影仪:已关闭
屏幕:已收起
灯光:已全亮
=== 使用外观模式 ===
===== 开始观影:星际穿越 =====
灯光:调暗至 10%
屏幕:已放下
投影仪:已开启
投影仪:输入源切换到 DVD
功放:已开启
功放:音量设置为 20
DVD播放器:已开启
DVD播放器:正在播放《星际穿越》
===== 观影准备完成 =====
===== 结束观影 =====
DVD播放器:已关闭
功放:已关闭
投影仪:已关闭
屏幕:已收起
灯光:已全亮
===== 观影结束,恢复原状 =====
进阶示例:API 客户端
<?php
// ==================== 复杂子系统 ====================
class HttpRequest {
public function send($url, $method, $data = []) {
// 模拟 HTTP 请求
echo "发送 {$method} 请求到 {$url}<br>";
return [
'status' => 200,
'data' => ['id' => 1, 'name' => '张三']
];
}
}
class DataValidator {
public function validate($data) {
if (empty($data['name'])) {
throw new Exception("名称不能为空");
}
return true;
}
}
class ResponseFormatter {
public function format($response) {
// 模拟格式化响应
return "响应内容:{<br>" .
" \"status\": {$response['status']},<br>" .
" \"data\": " . json_encode($response['data']) . "<br>" .
"}";
}
}
class Logger {
public function log($message) {
echo "[日志] {$message}<br>";
}
}
// ==================== 外观类 ====================
class ApiClientFacade {
private $httpRequest;
private $validator;
private $formatter;
private $logger;
public function __construct() {
$this->httpRequest = new HttpRequest();
$this->validator = new DataValidator();
$this->formatter = new ResponseFormatter();
$this->logger = new Logger();
}
// 统一的 GET 请求接口
public function get($url, $params = []) {
$this->logger->log("开始 GET 请求");
$response = $this->httpRequest->send($url, 'GET', $params);
return $this->formatter->format($response);
}
// 统一的 POST 请求接口
public function post($url, $data) {
try {
$this->logger->log("开始 POST 请求");
// 内部自动完成验证
$this->validator->validate($data);
$response = $this->httpRequest->send($url, 'POST', $data);
return $this->formatter->format($response);
} catch (Exception $e) {
$this->logger->log("POST 请求失败: " . $e->getMessage());
throw $e;
}
}
// 统一的 PUT 请求接口
public function put($url, $data) {
$this->logger->log("开始 PUT 请求");
$response = $this->httpRequest->send($url, 'PUT', $data);
return $this->formatter->format($response);
}
// 统一的 DELETE 请求接口
public function delete($url, $id) {
$this->logger->log("开始 DELETE 请求");
$response = $this->httpRequest->send($url, 'DELETE', ['id' => $id]);
return $this->formatter->format($response);
}
}
// ==================== 客户端使用 ====================
$apiClient = new ApiClientFacade();
// 客户端只需要调用简单的接口
try {
// GET 请求
echo "<strong>GET 请求:</strong><br>";
$result = $apiClient->get('/api/users/1');
echo $result . "<br><br>";
// POST 请求
echo "<strong>POST 请求:</strong><br>";
$result = $apiClient->post('/api/users', ['name' => '李四']);
echo $result . "<br><br>";
// POST 请求(验证失败)
echo "<strong>POST 请求(数据无效):</strong><br>";
$apiClient->post('/api/users', []);
} catch (Exception $e) {
echo "错误:" . $e->getMessage() . "<br>";
}
外观模式的优缺点
优点 ✅
| 优点 | 说明 |
|---|---|
| 简化接口 | 隐藏系统的复杂性,提供统一简洁的接口 |
| 解耦合 | 客户端与子系统解耦,降低耦合度 |
| 提高安全性和灵活性 | 只暴露必要的方法,内部可以自由修改 |
| 易于使用 | 降低学习成本,客户端不需要了解内部细节 |
| 便于分层 | 有助于建立层次结构,各层通过外观通信 |
缺点 ❌
| 缺点 | 说明 |
|---|---|
| 可能成为"上帝对象" | 如果不加控制,外观类可能变得过于庞大 |
| 不一定符合开闭原则 | 修改外观接口可能需要修改客户端 |
| 过度使用会隐藏问题 | 可能掩盖内部组件的真实问题 |
适用场景与最佳实践
适用场景
- 需要为一组复杂的子系统提供简化接口
- 客户端需要与多个子系统解耦
- 系统有层次结构,需要定义每一层的入口点
- 需要将系统划分为多个独立层次
最佳实践
// 1. 组合优于继承:外观类应该使用组合而不是继承
class Facade {
private $subSystemA;
private $subSystemB;
public function __construct(SubSystemA $a, SubSystemB $b) {
$this->subSystemA = $a;
$this->subSystemB = $b;
}
}
// 2. 外观接口应该保持简单,专注于客户端需求
class OrderFacade {
public function __construct(private InventorySystem $inventory,
private PaymentSystem $payment) {}
public function checkout($orderId) {
// 简单的业务逻辑组合
}
}
// 3. 不要在外观类中添加业务逻辑,只做调度
class BookingFacade {
public function bookFlight($flightId) {
// 只做调度,不做具体业务
return $this->flightSystem->book($flightId);
}
}
对比其他模式
| 比较 | 外观模式 | 适配器模式 | 代理模式 |
|---|---|---|---|
| 目的 | 简化接口 | 转换接口 | 控制访问 |
| 焦点 | 简化复杂系统 | 适配不同接口 | 延迟加载/权限控制 |
| 结构 | 多个子系统 | 两个不同接口 | 被代理对象 |
外观模式的核心价值:通过提供一个统一的入口,将复杂的系统内部交互对外隐藏,使得客户端代码更加简洁、清晰、易于维护,它特别适合用于库的封装、API 设计和系统分层等场景。
外观模式不是"万能药",当系统本身很简单时,使用外观模式反而会增加不必要的复杂度。适度使用才是关键。