依赖注入
本课属于 Coddy PHP 学习旅程的“面向对象编程”单元。第 57 课,共 91 课。
在上一课中,你了解了组合如何让 Car 包含一个 Engine 对象。但在类内部创建依赖会带来一个问题:这会让代码变得僵化,难以测试。依赖注入通过从外部传入依赖,而不是在内部创建依赖,解决了这个问题。
比较这两种方法:
<?php
// 未使用 DI,依赖在内部创建
class Car {
private Engine $engine;
public function __construct() {
$this->engine = new Engine(); // 紧密耦合
}
}
// 使用 DI,依赖从外部传入
class Car {
public function __construct(private Engine $engine) {}
}
$engine = new Engine();
$car = new Car($engine); // 从外部注入
第二种方法是依赖注入。Car 不会创建自己的引擎,而是接收一个引擎。这个简单的改变有很多好处:你可以传入不同类型的引擎,为测试替换实现,让这个类变得更加灵活。
为了获得更大的灵活性,可以注入接口,而不是具体类:
<?php
interface EngineInterface {
public function start(): string;
}
class Car {
public function __construct(private EngineInterface $engine) {}
public function start(): string {
return $this->engine->start();
}
}
现在,Car 可以与任何实现了 EngineInterface 的类配合使用,例如汽油发动机、电动机,或用于测试的模拟对象。这种解耦正是依赖注入成为编写易于维护、易于测试的 PHP 应用程序的基础的原因。
挑战
简单让我们构建一个通知系统,展示依赖注入的强大之处。你不会把消息发送方式写死,而是创建一个灵活的系统,可以轻松更换消息的发送方式。
你会将代码分成 4 个文件:
MessageSenderInterface.php:定义一个名为MessageSenderInterface的接口,其中只有一个方法send(string $recipient, string $message): string。这个接口规定了所有消息发送器都必须遵守的约定。EmailSender.php:创建一个实现MessageSenderInterface的EmailSender类。引入接口文件。send()方法应返回"Email to [recipient]: [message]"。NotificationService.php:创建一个通过构造函数接收依赖项的NotificationService类。引入接口文件。这个类应该:- 在构造函数中使用构造函数属性提升来接收一个
MessageSenderInterface - 包含一个方法
notify(string $recipient, string $message),该方法会调用注入的发送器,并返回其结果
NotificationService依赖的是接口,而不是具体的类。这意味着你可以注入任何实现该接口的发送器,而无需更改这个服务。- 在构造函数中使用构造函数属性提升来接收一个
main.php:引入 EmailSender 和 NotificationService 文件。你会收到两个输入:收件人和消息。创建一个EmailSender,将它注入NotificationService,然后调用notify()并输出结果。
这种模式让你的 NotificationService 保持灵活。无论你注入的是电子邮件、短信发送器,还是尚未构建的其他发送器,它都能正常工作。这个服务不会自行创建依赖项,而是从外部接收它们。
亲手试试
<?php
require_once 'EmailSender.php';
require_once 'NotificationService.php';
// 读取输入
$recipient = trim(fgets(STDIN));
$message = trim(fgets(STDIN));
// TODO:创建一个 EmailSender 实例
// TODO:将它注入 NotificationService
// TODO:调用 notify() 并打印结果
?>这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: PHP 在线编译器