一句话
依赖注入(Dependency Injection)= 一个类不自己创建依赖,而是由外部传入。
IoC 容器 = 自动帮你完成”传入”这个动作的工厂 + 注册表。
一、问题:为什么不能自己 new
1 2 3 4 5 6
| class OrderService { private MysqlOrderRepo $repo; public function __construct() { $this->repo = new MysqlOrderRepo(new PDO(...)); } }
|
毛病:
- 不可换实现 —— 想换 Redis?改源码
- 不可测 —— 单元测试想 mock 数据库?改不动
- 强耦合 ——
OrderService 必须知道 PDO 的连接串 - 依赖隐藏 —— 看构造器看不出它依赖谁
二、三种注入方式
1. 构造器注入(推荐)
1 2 3 4
| class OrderService { public function __construct(private OrderRepo $repo) {} } $svc = new OrderService(new MysqlOrderRepo($pdo));
|
最佳实践:依赖必填、对象不可变、看签名一目了然。
2. Setter 注入
1 2 3 4
| class OrderService { private OrderRepo $repo; public function setRepo(OrderRepo $repo): void { $this->repo = $repo; } }
|
适用:可选依赖、运行时切换。缺点:对象创建后状态不完整。
3. 接口注入 / 属性注入
1 2 3
| class OrderService { #[Inject] public OrderRepo $repo; }
|
适用:框架内部魔法。缺点:依赖魔法、IDE 补全难。
三、面向接口编程
DI 真正的价值要配合接口:
1 2 3 4 5 6 7 8 9 10
| interface OrderRepo { public function find(int $id): ?Order; } class MysqlOrderRepo implements OrderRepo { } class RedisOrderRepo implements OrderRepo { } class FakeOrderRepo implements OrderRepo { }
class OrderService { public function __construct(private OrderRepo $repo) {} }
|
测试时:
1
| $svc = new OrderService(new FakeOrderRepo());
|
四、IoC 容器:自动注入
手动 new 几十层依赖太累,IoC 容器替你做:
1 2
| $container->bind(OrderRepo::class, MysqlOrderRepo::class); $svc = $container->make(OrderService::class);
|
30 行手写一个容器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| class Container { private array $bindings = [];
public function bind(string $abstract, string|callable $concrete): void { $this->bindings[$abstract] = $concrete; }
public function make(string $abstract): object { $concrete = $this->bindings[$abstract] ?? $abstract; if (is_callable($concrete)) return $concrete($this);
$ref = new ReflectionClass($concrete); $ctor = $ref->getConstructor(); if (!$ctor) return new $concrete();
$args = []; foreach ($ctor->getParameters() as $p) { $type = $p->getType(); if ($type && !$type->isBuiltin()) { $args[] = $this->make($type->getName()); } } return $ref->newInstanceArgs($args); } }
$c = new Container(); $c->bind(OrderRepo::class, MysqlOrderRepo::class); $svc = $c->make(OrderService::class);
|
Laravel / Symfony 的容器本质就是这套,加了单例、上下文绑定、循环依赖检测、属性注入等增强。
五、DI vs Service Locator
很多人混淆两者:
1 2 3 4 5 6 7 8 9 10 11
| class OrderService { public function __construct(private OrderRepo $repo) {} }
class OrderService { public function process() { $repo = ServiceLocator::get(OrderRepo::class); } }
|
Service Locator 把依赖隐藏到方法内部,等于没做 DI。
六、什么时候不用 DI
- 极小脚本 / 一次性工具
- 全是静态工具函数(无状态)
- 性能极致敏感的热路径(容器有反射开销,但生产环境一般已编译/缓存掉)
参考