重构与依赖管理:重构依赖注入,降低耦合
依赖注入,说白了就是「把对象需要的东西,从外面塞给它」。
我刚开始写代码那会儿,习惯在类里面直接 new 依赖对象。比如一个订单服务,里面直接 new 了一个数据库访问类。看起来挺顺手的,对吧?但后来项目大了,问题就来了——想换个数据库实现,得改好几处代码。单元测试更是噩梦,因为根本没法 mock 掉那个数据库。
嗯,这就是耦合太紧的典型症状。
依赖注入的核心价值
依赖注入不是啥高深技术,它就是个设计原则。核心思想就一句话:别让类自己去创建依赖,让外部来提供。
依赖注入的三大好处:
- 解耦:类不再依赖具体实现,只依赖接口
- 可测试:可以轻松替换为 mock 对象
- 灵活:切换实现时,改一行配置就行
我在项目中遇到过这样一个场景:一个支付模块,最初只支持微信支付。代码里到处都是 new WechatPay()。后来要接入支付宝,改得我头皮发麻。如果当初用了依赖注入,只需要新增一个 Alipay 实现类,然后在配置里切换一下就好了。
重构步骤:从紧耦合到依赖注入
咱们一步步来。假设你有一个 OrderService,它直接依赖 MySqlOrderRepository。
第一步:识别问题代码
public class OrderService {
private MySqlOrderRepository repository;
public OrderService() {
// 直接在构造函数里创建依赖
this.repository = new MySqlOrderRepository();
}
public void createOrder(Order order) {
repository.save(order);
}
}
你看,这个 OrderService 和 MySqlOrderRepository 绑死了。想换成 MongoOrderRepository?得改构造函数。
第二步:提取接口
先定义一个接口,把依赖抽象出来。
public interface OrderRepository {
void save(Order order);
Order findById(String id);
}
public class MySqlOrderRepository implements OrderRepository {
@Override
public void save(Order order) {
// MySQL 实现
}
@Override
public Order findById(String id) {
// MySQL 实现
}
}
第三步:构造函数注入
把依赖从外部传进来。
public class OrderService {
private final OrderRepository repository;
// 依赖通过构造函数注入
public OrderService(OrderRepository repository) {
this.repository = repository;
}
public void createOrder(Order order) {
repository.save(order);
}
}
现在,OrderService 不再关心 repository 的具体实现。你传给它什么,它就用什么。
第四步:使用依赖注入容器
手动注入虽然比直接 new 好,但项目大了还是麻烦。这时候可以用 DI 容器,比如 Spring 的 IoC 容器。
@Configuration
public class AppConfig {
@Bean
public OrderRepository orderRepository() {
return new MySqlOrderRepository();
}
@Bean
public OrderService orderService() {
return new OrderService(orderRepository());
}
}
或者用注解更简洁:
@Service
public class OrderService {
private final OrderRepository repository;
@Autowired
public OrderService(OrderRepository repository) {
this.repository = repository;
}
}
@Repository
public class MySqlOrderRepository implements OrderRepository {
// 实现
}
我的建议:优先用构造函数注入,别用字段注入。构造函数注入能保证依赖在对象创建时就准备好,而且更容易写单元测试。
依赖注入的常见模式
| 模式 | 说明 | 适用场景 |
|---|---|---|
| 构造函数注入 | 通过构造函数参数传入依赖 | 大多数情况,依赖是必需的 |
| Setter 注入 | 通过 setter 方法设置依赖 | 依赖是可选的,或者需要动态切换 |
| 接口注入 | 通过接口方法传入依赖 | 框架级别的设计,较少用 |
我个人习惯用构造函数注入。为什么?因为一旦对象构造完成,它的依赖就是完整的,不会出现空指针。你想想看,如果用了 Setter 注入,调用方忘了调 setter,那运行时就会炸。
避坑指南
我曾经踩过的坑:
- 过度注入:一个类注入十几个依赖,说明这个类职责太多了。该拆就拆。
- 循环依赖:A 依赖 B,B 又依赖 A。构造函数注入会直接报错。解决办法是拆出公共部分,或者改用 Setter 注入(但这是治标不治本)。
- 滥用容器:别把 DI 容器当成全局变量容器。只注入那些真正需要解耦的依赖。
依赖注入的 SVG 结构图
这张图展示了依赖注入的核心逻辑:客户端不直接创建具体实现,而是依赖一个接口。具体实现由 DI 容器在运行时注入进去。这样,客户端和具体实现之间就隔了一层接口,耦合度大大降低。
重构后的效果
重构完依赖注入后,你会发现:
- 单元测试好写了。传个 mock 进去就行。
- 切换实现成本极低。改一行配置,或者换一个 Bean 定义。
- 代码更清晰。每个类的依赖一目了然。
说白了,依赖注入就是给代码「松绑」。让每个类只关心自己的事,别管别人怎么实现。你想想看,如果每个类都自己 new 依赖,那整个系统就像一团乱麻。用 DI 梳理清楚后,每根线都清清楚楚。
一个小技巧:重构依赖注入时,别一次性改太多。我习惯从最底层开始,一层层往上改。每改完一层,就跑一遍测试,确保没破坏现有功能。
嗯,依赖注入这块就聊到这儿。记住一句话:别 new,要 inject。