门面(Facade)—— 静态语法的“动态伪装术”

一、门面是“静态方法的快捷方式”

传统写法 vs 门面写法

传统写法(手动 new)
$logger = new Logger(); // 手动创建日志服务
$logger->log('用户登录成功'); // 调用日志方法

$cache = new Cache(); // 手动创建缓存服务
$cache->set('user:1001', $user); // 调用缓存方法
门面写法(一行搞定)
// 门面简化后的写法
Log::log('用户登录成功');
Cache::set('user:1001', $user);

区别

  • 传统写法:需要手动 new 对象
  • 门面写法:用静态方法直接调用,背后自动 new 对象

二、门面的底层原理(从调用开始)

1. 调用 Log::log($message) 的全过程

步骤 1:触发 __callStatic
// 用户调用
Log::log($message);
  • Log 是 Facade 的子类
  • PHP 会自动调用 static::__callStatic('log', [$message])
步骤 2:__callStatic 的核心逻辑
abstract class Facade {
    public static function __callStatic($method, $args) {
        // 1. 获取真实类名(Logger)
        $instance = static::resolveFacadeInstance();
        // 2. 调用真实实例的方法
        return $instance->$method(...$args);
    }
}
步骤 3:resolveFacadeInstance() 的实现
protected static function resolveFacadeInstance() {
    $serviceId = static::getFacadeClass(); // 'Logger'
    return Container::getInstance()->make($serviceId);
}
步骤 4:getFacadeClass() 返回真实类名
class Log extends Facade {
    protected static function getFacadeClass() {
        return Logger::class; // 返回真实类名
    }
}
步骤 5:容器 make() 创建实例
$logger = Container::make(Logger::class);
$logger->log($message);

最终流程

Log::log($message) 
→ __callStatic('log', [...]) 
→ getFacadeClass() 返回 Logger::class 
→ 容器 make(Logger::class) 
→ 调用 $logger->log($message)

三、关键原理小备注

1. 容器如何被调用?

  • 容器是一个类,但通过 单例模式 实现:
class Container {
    protected static $instance;
    public static function getInstance() {
        if (is_null(static::$instance)) {
            static::$instance = new self(); // 第一次 new
        }
        return static::$instance; // 后续直接返回
    }
}

2. 门面 vs 控制器依赖注入(重点补充)

错误示例(控制器内部依赖注入)
class OrderController {
    public function __construct() {
        //  错误:直接调用门面(耦合严重)
        $this->logger = Log::class; // 这里赋值的是字符串 'Logger',不是对象
    }
}

为什么错误?

  1. 赋值错误Log::class 返回的是字符串 'Logger',不是 Logger 实例

    var_dump(Log::class); // 输出: string(7) "Logger"
    
    • 这会导致 $this->logger 是字符串,不是对象
    • 后续调用 $this->logger->log(...) 会报错:Trying to get property 'log' of non-object
  2. 违反依赖注入原则

    • 控制器应该依赖于抽象接口(如 LoggerInterface),而不是具体实现
    • 门面是静态工具,不是依赖注入的载体
  3. 可测试性差

    • 无法在测试中替换日志服务(如使用 Mock 对象)
    • 门面是静态的,无法被 Mock(除非框架提供特殊支持)
正确做法(构造函数注入)
// 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(抽象),而非具体实现

  • 测试时可轻松替换:

    // 测试时使用 Mock
    $mockLogger = $this->createMock(LoggerInterface::class);
    $controller = new OrderController($mockLogger);
    
  • 与容器解耦:容器负责创建 FileLogger 实例,控制器只需接收接口


四、门面的最佳实践

何时用?

  • 全局服务:如日志、缓存、数据库连接

    // 日志门面示例
    Log::log('用户登录成功'); // 背后调用 $container->get(Logger::class)->log()
    
  • 复杂组件:如支付网关、邮件服务

    // 支付门面示例
    Payment::pay($order); // 背后调用 $container->get(PaymentService::class)->pay()
    

何时不用?

  • 控制器内部依赖注入:用构造函数注入(如上例)
  • 需要频繁实例化的新对象:如临时数据处理器

五、门面使用场景总结

场景门面使用正确做法
日志记录Log::info(...) 在方法中调用(不作为依赖) 不要在构造函数中使用
控制器依赖Log::class 构造函数注入 LoggerInterface
业务逻辑Payment::pay(...) 业务逻辑应通过服务类处理 不应直接在控制器中调用

重要结论

  • 门面适合:访问全局服务(日志、缓存)
  • 门面不适合:作为控制器的依赖(应使用接口注入)

为什么这个结论重要?

  1. 避免"门面滥用" :很多初学者会误以为门面可以替代依赖注入
  2. 保持代码可测试性:依赖注入是测试友好的设计
  3. 符合框架最佳实践:Laravel/ThinkPHP 官方文档都推荐构造函数注入

总结:门面使用指南

场景推荐方式错误方式
日志记录Log::info(...)$this->logger = Log::class
缓存操作Cache::get(...)$this->cache = Cache::class
控制器依赖public function __construct(LoggerInterface $logger)Log::class
业务逻辑Payment::pay(...)Payment::class
本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]