15、MVC中的依赖注入:解耦的艺术
依赖注入(Dependency Injection,简称DI),说白了就是「把组件需要的依赖,从外部塞给它,而不是让它自己 new 出来」。这个概念在 MVC 里特别重要,因为它直接决定了你的代码是「一坨屎山」还是「可维护的乐高积木」。
我刚开始写 MVC 项目时,也踩过这个坑。Controller 里直接 new Service,Service 里直接 new Repository,结果项目一大了,改一个地方要牵连七八个文件。嗯,后来我才明白——依赖注入就是用来解决这个问题的。
为什么 MVC 需要依赖注入?
先看一个反面教材。假设我们有一个用户注册功能:
// 没有依赖注入的写法
class UserController {
private $userService;
public function __construct() {
// 直接在构造函数里 new 依赖
$this->userService = new UserService();
}
public function register($data) {
return $this->userService->createUser($data);
}
}
class UserService {
private $userRepo;
private $emailService;
public function __construct() {
$this->userRepo = new UserRepository();
$this->emailService = new EmailService();
}
public function createUser($data) {
$user = $this->userRepo->save($data);
$this->emailService->sendWelcome($user);
return $user;
}
}
这段代码有什么问题?问题大了去了。
- 耦合太紧:UserController 和 UserService 绑死了,没法替换实现。
- 测试困难:你想单元测试 UserController,结果它连数据库、发邮件都一起跑了。
- 扩展性差:想换个 UserService 实现?得改 Controller 的构造函数。
我在项目中遇到过类似的情况。有一次要加个「用户注册后发送短信」的功能,结果发现 EmailService 被硬编码在十几个地方。改起来简直想骂人。从那以后,我强制团队所有新代码必须用依赖注入。
依赖注入的核心思想
依赖注入其实就三个角色:
| 角色 | 说明 | 类比 |
|---|---|---|
| 服务(Service) | 被依赖的组件,比如数据库、邮件服务 | 螺丝刀 |
| 客户端(Client) | 使用依赖的组件,比如 Controller、Service | 修理工 |
| 注入器(Injector) | 负责创建并传递依赖的容器 | 工具箱 |
你想想看,修理工(客户端)不需要自己造螺丝刀(服务),工具箱(注入器)会给他配好。这就是依赖注入的朴素思想。
三种注入方式
依赖注入有三种常见方式,我按推荐程度排个序:
1. 构造函数注入(最推荐)
class UserController {
private $userService;
// 依赖通过构造函数传入
public function __construct(UserService $userService) {
$this->userService = $userService;
}
public function register($data) {
return $this->userService->createUser($data);
}
}
这种方式的好处是:依赖关系一目了然,对象创建时就必须提供所有依赖,不会出现「用到一半发现没初始化」的尴尬。
2. Setter 注入(可选依赖用)
class UserController {
private $logger;
// 日志不是必须的,所以用 setter
public function setLogger(Logger $logger) {
$this->logger = $logger;
}
public function register($data) {
// 有日志就记录,没有就算了
if ($this->logger) {
$this->logger->info('用户注册');
}
// ... 业务逻辑
}
}
适合那些「有最好,没有也能跑」的依赖。比如日志、缓存、监控等。
3. 接口注入(框架层面用)
interface LoggerAwareInterface {
public function setLogger(Logger $logger);
}
class UserController implements LoggerAwareInterface {
private $logger;
public function setLogger(Logger $logger) {
$this->logger = $logger;
}
}
这种方式在框架层面用得比较多,比如 Symfony 的组件就大量使用接口注入。普通业务代码不太需要。
依赖注入容器(DIC)
手动注入依赖太累了。一个项目几十个 Service,每个都要 new 一遍?所以我们需要依赖注入容器。
容器就是一个「中央工厂」,它知道如何创建所有对象,并且自动把依赖传递进去。
// 一个简单的容器示例
class Container {
private $bindings = [];
private $instances = [];
// 注册一个服务
public function set($id, $factory) {
$this->bindings[$id] = $factory;
}
// 获取一个服务
public function get($id) {
if (!isset($this->instances[$id])) {
$this->instances[$id] = call_user_func($this->bindings[$id], $this);
}
return $this->instances[$id];
}
}
// 使用容器
$container = new Container();
// 注册依赖
$container->set('UserRepository', function($c) {
return new UserRepository($c->get('Database'));
});
$container->set('UserService', function($c) {
return new UserService($c->get('UserRepository'));
});
$container->set('UserController', function($c) {
return new UserController($c->get('UserService'));
});
// 获取 Controller,所有依赖自动注入
$controller = $container->get('UserController');
依赖注入在 MVC 中的典型应用
下面这张图展示了依赖注入在 MVC 中的完整流程:
你看,每一层都只关心自己的事。Controller 不需要知道 UserService 是怎么创建的,它只需要「拿到一个 UserService 实例」就行了。这就是解耦。
避坑指南
另外还有几个常见坑:
- 循环依赖:A 依赖 B,B 依赖 A。容器会死循环。解决办法是拆出公共接口,或者用 setter 注入打破循环。
- 过度抽象:每个类都搞个接口,结果接口比实现还多。我建议只对「可能变化」的部分抽象,比如数据库驱动、缓存驱动、第三方服务。
- 容器滥用:把容器当作 Service Locator 到处传。正确的做法是:只在入口处(比如路由分发)使用容器,业务代码里只通过构造函数接收依赖。
实际项目中的最佳实践
以我最近做的一个电商项目为例,我们是这样组织依赖注入的:
// 1. 定义接口(契约)
interface OrderRepositoryInterface {
public function findById($id);
public function save(Order $order);
}
// 2. 实现接口
class MysqlOrderRepository implements OrderRepositoryInterface {
private $db;
public function __construct(PDO $db) {
$this->db = $db;
}
// ... 实现方法
}
class RedisOrderRepository implements OrderRepositoryInterface {
private $redis;
public function __construct(Redis $redis) {
$this->redis = $redis;
}
// ... 实现方法
}
// 3. 在容器中绑定接口到具体实现
$container->set('OrderRepositoryInterface', function($c) {
// 根据配置决定用 MySQL 还是 Redis
if (config('use_redis')) {
return new RedisOrderRepository($c->get('Redis'));
}
return new MysqlOrderRepository($c->get('PDO'));
});
// 4. Service 只依赖接口
class OrderService {
private $orderRepo;
public function __construct(OrderRepositoryInterface $orderRepo) {
$this->orderRepo = $orderRepo;
}
// ... 业务逻辑
}
这样做的好处是:
- 切换数据源只需要改容器配置,业务代码一行不动
- 单元测试时,可以注入 Mock 对象,不用连真实数据库
- 团队协作时,每个人负责自己的模块,接口约定好就行
依赖注入不是银弹,但它确实是 MVC 架构中解耦的利器。说白了,它让我们的代码从「硬编码的钢筋混凝土」变成了「可插拔的乐高积木」。你想想看,当你的项目从几千行膨胀到几十万行时,这种灵活性有多重要。
嗯,关于依赖注入在 MVC 中的应用,今天就聊到这里。记住一个核心原则:依赖从外部来,不要自己造。你的代码会感谢你的。
公众号:蓝海资料掘金营,微信deep3321