📘 门面(Facade)------ 静态语法的"动态伪装术"
🌱 一、门面是"静态方法的快捷方式"
❓ 传统写法 vs 门面写法
传统写法(手动 new)
php
$logger = new Logger(); // 手动创建日志服务
$logger->log('用户登录成功'); // 调用日志方法
$cache = new Cache(); // 手动创建缓存服务
$cache->set('user:1001', $user); // 调用缓存方法
门面写法(一行搞定)
c
// 门面简化后的写法
Log::log('用户登录成功');
Cache::set('user:1001', $user);
区别:
- 传统写法:需要手动
new对象 - 门面写法:用静态方法直接调用,背后自动
new对象
🧠 二、门面的底层原理(从调用开始)
1. 调用 Log::log($message) 的全过程
步骤 1:触发 __callStatic
arduino
// 用户调用
Log::log($message);
Log是Facade的子类- PHP 会自动调用
static::__callStatic('log', [$message])
步骤 2:__callStatic 的核心逻辑
php
abstract class Facade {
public static function __callStatic($method, $args) {
// 1. 获取真实类名(Logger)
$instance = static::resolveFacadeInstance();
// 2. 调用真实实例的方法
return $instance->$method(...$args);
}
}
步骤 3:resolveFacadeInstance() 的实现
php
protected static function resolveFacadeInstance() {
$serviceId = static::getFacadeClass(); // 'Logger'
return Container::getInstance()->make($serviceId);
}
步骤 4:getFacadeClass() 返回真实类名
scala
class Log extends Facade {
protected static function getFacadeClass() {
return Logger::class; // 返回真实类名
}
}
步骤 5:容器 make() 创建实例
php
$logger = Container::make(Logger::class);
$logger->log($message);
📌 最终流程:
arduino
Log::log($message)
→ __callStatic('log', [...])
→ getFacadeClass() 返回 Logger::class
→ 容器 make(Logger::class)
→ 调用 $logger->log($message)
🔍 三、关键原理小备注
1. 容器如何被调用?
- 容器是一个类,但通过 单例模式 实现:
php
class Container {
protected static $instance;
public static function getInstance() {
if (is_null(static::$instance)) {
static::$instance = new self(); // 第一次 new
}
return static::$instance; // 后续直接返回
}
}
2. 门面 vs 控制器依赖注入(重点补充)
❌ 错误示例(控制器内部依赖注入)
kotlin
class OrderController {
public function __construct() {
// ❌ 错误:直接调用门面(耦合严重)
$this->logger = Log::class; // 这里赋值的是字符串 'Logger',不是对象
}
}
为什么错误?
-
赋值错误 :
Log::class返回的是字符串'Logger',不是Logger实例cssvar_dump(Log::class); // 输出: string(7) "Logger"- 这会导致
$this->logger是字符串,不是对象 - 后续调用
$this->logger->log(...)会报错:Trying to get property 'log' of non-object
- 这会导致
-
违反依赖注入原则:
- 控制器应该依赖于抽象接口 (如
LoggerInterface),而不是具体实现 - 门面是静态工具,不是依赖注入的载体
- 控制器应该依赖于抽象接口 (如
-
可测试性差:
- 无法在测试中替换日志服务(如使用 Mock 对象)
- 门面是静态的,无法被 Mock(除非框架提供特殊支持)
✅ 正确做法(构造函数注入)
php
// 1. 定义接口
interface LoggerInterface {
public function log(string $message);
}
// 2. 实现类
class FileLogger implements LoggerInterface {
public function log(string $message) {
file_put_contents('app.log', $message . PHP_EOL, FILE_APPEND);
}
}
// 3. 门面类
class Log extends Facade {
protected static function getFacadeClass() {
return FileLogger::class; // 返回真实类名
}
}
// 4. 控制器(正确写法)
class OrderController {
protected $logger;
// ✅ 正确:通过构造函数注入接口
public function __construct(LoggerInterface $logger) {
$this->logger = $logger;
}
public function createOrder() {
$this->logger->log('订单创建成功'); // 调用接口方法
}
}
为什么正确?
-
控制器依赖于
LoggerInterface(抽象),而非具体实现 -
测试时可轻松替换:
ini// 测试时使用 Mock $mockLogger = $this->createMock(LoggerInterface::class); $controller = new OrderController($mockLogger); -
与容器解耦:容器负责创建
FileLogger实例,控制器只需接收接口
✅ 四、门面的最佳实践
何时用?
-
全局服务:如日志、缓存、数据库连接
arduino// 日志门面示例 Log::log('用户登录成功'); // 背后调用 $container->get(Logger::class)->log() -
复杂组件:如支付网关、邮件服务
php// 支付门面示例 Payment::pay($order); // 背后调用 $container->get(PaymentService::class)->pay()
何时不用?
- 控制器内部依赖注入:用构造函数注入(如上例)
- 需要频繁实例化的新对象:如临时数据处理器
🎯 五、门面使用场景总结
| 场景 | 门面使用 | 正确做法 |
|---|---|---|
| 日志记录 | Log::info(...) |
✅ 在方法中调用(不作为依赖) ❌ 不要在构造函数中使用 |
| 控制器依赖 | Log::class |
✅ 构造函数注入 LoggerInterface |
| 业务逻辑 | Payment::pay(...) |
✅ 业务逻辑应通过服务类处理 ❌ 不应直接在控制器中调用 |
💡 重要结论
门面不是依赖注入的替代品,而是全局服务的访问入口。
控制器应该通过构造函数注入依赖,而不是使用门面。
- 门面适合:访问全局服务(日志、缓存)
- 门面不适合:作为控制器的依赖(应使用接口注入)
📌 为什么这个结论重要?
- 避免"门面滥用" :很多初学者会误以为门面可以替代依赖注入
- 保持代码可测试性:依赖注入是测试友好的设计
- 符合框架最佳实践:Laravel/ThinkPHP 官方文档都推荐构造函数注入
✅ 正确使用门面:
Log::info('消息')(在方法中临时使用)❌ 错误使用门面:
$this->logger = Log::class(在构造函数中赋值)
✅ 总结:门面使用指南
| 场景 | 推荐方式 | 错误方式 |
|---|---|---|
| 日志记录 | Log::info(...) |
$this->logger = Log::class |
| 缓存操作 | Cache::get(...) |
$this->cache = Cache::class |
| 控制器依赖 | public function __construct(LoggerInterface $logger) |
Log::class |
| 业务逻辑 | Payment::pay(...) |
Payment::class |