实践依赖倒置
使用依赖倒置将适配器连接到核心。
实践依赖倒置 是 CoddyKit 上的免费 PHP Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 PHP Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 PHP Academy 课程共包含 4 节课。
倒置与注入
人们常常将两个不同的概念混为一谈。依赖注入是一种技术:传入协作者,而不是创建协作者。依赖倒置(SOLID 中的 D)是一项原则:高层策略和低层细节都依赖于抽象,并且抽象由高层模块拥有。本课要让第二点真正落地——将具体适配器连接到由核心定义接口的结构上。
准确理解这项原则
DIP 表述如下:
- 高层模块不应依赖低层模块;二者都应依赖抽象。
- 抽象不应依赖细节;细节应依赖抽象。
微妙之处在于:接口属于使用方,而不是实现方。您的领域声明 PaymentGateway;Stripe 适配器遵循该接口,而不是反过来。
在核心中定义抽象
将接口放在需要它的代码旁边,并使用领域术语表达。不要让 Stripe 类型泄漏进来。
<?php
// src/Billing/Application/Port/PaymentGateway.php
interface PaymentGateway
{
public function charge(Money $amount, CardToken $token): ChargeId;
}
final class Money {
public function __construct(public readonly int $cents, public readonly string $currency) {}
}
final class CardToken { public function __construct(public readonly string $value) {} }
final class ChargeId { public function __construct(public readonly string $value) {} }让适配器遵循它
基础设施适配器实现核心的接口,并将其转换为供应商 SDK。依赖箭头从适配器(细节)指向抽象(策略),从而实现了倒置。
<?php
final class StripePaymentGateway implements PaymentGateway
{
public function __construct(private \Stripe\StripeClient $stripe) {}
public function charge(Money $amount, CardToken $token): ChargeId {
$intent = $this->stripe->paymentIntents->create([
'amount' => $amount->cents,
'currency' => strtolower($amount->currency),
'payment_method' => $token->value,
'confirm' => true,
]);
return new ChargeId($intent->id);
}
}默认使用构造函数注入
通过构造函数注入,并为抽象指定类型提示。这样,依赖关系变得明确、不可变,也不会被遗忘。对于必需的协作者,请避免使用设置器注入和属性注入,因为它们会允许创建出未完整构造的对象。
<?php
final class CheckoutService
{
public function __construct(
private PaymentGateway $payments, // abstraction, not StripeClient
private OrderRepository $orders,
) {}
public function pay(OrderId $id, CardToken $token): ChargeId {
$order = $this->orders->get($id);
$charge = $this->payments->charge($order->total(), $token);
$order->markPaid($charge);
$this->orders->save($order);
return $charge;
}
}组合根
所有具体的组装都应发生在唯一一个地方——组合根——并且应尽可能靠近 main()/入口点。其他任何地方都不应自行创建基础设施。这是唯一知道 Stripe 存在的位置。
<?php
// public/index.php — composition root
$stripe = new \Stripe\StripeClient(getenv('STRIPE_SECRET'));
$gateway = new StripePaymentGateway($stripe);
$orders = new PdoOrderRepository(new PDO(getenv('DB_DSN')));
$checkout = new CheckoutService($gateway, $orders);
// Everything below depends only on abstractions
$controller = new CheckoutController($checkout);使用 DI 容器进行组装
对于较复杂的应用,容器(PHP-DI、Symfony)可以自动完成组装。关键步骤是将接口绑定到实现。自动装配会根据类型解析构造函数;您只需声明接口→类映射。
<?php
use function DI\autowire;
use function DI\get;
return [
PaymentGateway::class => autowire(StripePaymentGateway::class),
OrderRepository::class => autowire(PdoOrderRepository::class),
\Stripe\StripeClient::class => fn() => new \Stripe\StripeClient(getenv('STRIPE_SECRET')),
PDO::class => fn() => new PDO(getenv('DB_DSN')),
];替换适配器即可证明这一点
由于核心只依赖 PaymentGateway,切换供应商或进行离线测试只需修改一行组装代码。下面是单元测试中使用的伪实现——CheckoutService 无需改动,也从未导入 Stripe。
<?php
final class FakeGateway implements PaymentGateway {
public array $charges = [];
public function charge(Money $a, CardToken $t): ChargeId {
$this->charges[] = $a;
return new ChargeId('ch_test_' . count($this->charges));
}
}
$fake = new FakeGateway();
$id = $fake->charge(new Money(2500, 'EUR'), new CardToken('tok_visa'));
echo $id->value, ' charges=', count($fake->charges), PHP_EOL; // ch_test_1 charges=1避免服务定位器陷阱
注入容器本身,然后在方法内部获取依赖,是服务定位器反模式。它隐藏依赖关系,破坏类型检查,并使代码重新与容器耦合。请明确注入所需的依赖。
<?php
// ANTI-PATTERN: hidden dependencies, container leaks everywhere
final class BadCheckout {
public function __construct(private ContainerInterface $c) {}
public function pay($id, $token) {
$gateway = $this->c->get(PaymentGateway::class); // hidden!
// ...
}
}容器可以出现在哪里
容器只在一个层次中合理:组合根及其周围的框架胶水代码(例如控制器工厂)。您的领域类和应用类必须与容器无关——它们通过构造函数接收普通对象,也可以手动实例化。一个很好的测试是:没有容器时,您能否在单个 PHP 文件中组装整个应用?如果可以,说明您的依赖关系是明确真实的。
不使用服务定位器的延迟组装
有时,某个依赖构建成本很高,或者只在特定条件下才需要。请不要注入容器,而是注入一个工厂闭包。这样,依赖关系仍然明确且具有类型信息,同时会将构建延迟到真正使用时。
<?php
final class ReportService
{
/** @param Closure():PaymentGateway $gatewayFactory */
public function __construct(private Closure $gatewayFactory) {}
public function refundIfNeeded(bool $needed): void {
if (!$needed) return;
$gateway = ($this->gatewayFactory)(); // built only when required
// $gateway->charge(...) etc.
}
}
// Composition root supplies the factory, not the container
$svc = new ReportService(fn() => new StripePaymentGateway($stripe ?? null));
echo 'lazy dependency wired', PHP_EOL;快速检查
PaymentGateway 接口应当由谁拥有?
回顾
您已经以正确的方式将适配器连接到核心:
- 倒置 ≠ 注入:注入是实现机制,倒置则是让使用方拥有抽象。
- 核心定义接口;基础设施适配器遵循该接口。
- 使用抽象的构造函数注入;所有具体组装都应在单一的组合根中完成(或在将接口绑定到类的容器配置中完成)。
- 替换适配器以及在测试中使用伪实现,都只需修改一行代码。
- 避免服务定位器反模式,只将容器保留在边缘。
常见问题解答
「实践依赖倒置」课时是免费的吗?
是的 — 「实践依赖倒置」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 PHP Academy 课程的其余内容,请升级到 CoddyKit PRO。 PHP Academy 课程共包含 4 节课。
「实践依赖倒置」这节课中我会学到什么?
使用依赖倒置将适配器连接到核心。 你通过在浏览器中直接运行的动手代码来练习 PHP Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 PHP Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 PHP Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「实践依赖倒置」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 PHP Academy 课中编写并运行代码吗?
能。每节 PHP Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。