直接new对象,是你在主动寻找和创建你需要的“零件”;而 IoC 则是你告诉一个“管家”你需要什么“零件”,由“管家”帮你找到或创建好,然后递给你。
这个“管家”就是 IoC 容器(比如 Spring 容器),而“递给你”的这个动作,最常见的实现方式就是依赖注入(Dependency Injection, DI)。
下面我们通过一个例子来解释清楚。
场景:UserService
需要使用 UserDao
来操作数据库
假设我们有一个 UserService
类,它负责用户相关的业务逻辑,比如用户注册。它需要依赖一个 UserDao
类来与数据库进行交互。UserDao
是一个接口,它有一个具体的实现类 UserDaoImpl
。
Java
// 数据访问层接口
public interface UserDao {void addUser(User user);
}// 接口的实现类
public class UserDaoImpl implements UserDao {@Overridepublic void addUser(User user) {System.out.println("数据库:添加用户 " + user.getName());}
}
1. 直接 new
一个对象(正向控制)
在这种方式下,UserService
类需要自己负责创建它所依赖的 UserDaoImpl
对象。
代码示例:
Java
public class UserService {// UserService 内部自己创建了 UserDaoImpl 实例private UserDao userDao = new UserDaoImpl();public void register(User user) {// ... 其他业务逻辑,如检查用户名密码等userDao.addUser(user);}
}
分析这种方式的问题(为什么会耦合):
- 高度耦合:
UserService
和具体的实现类UserDaoImpl
紧紧地“绑死”在了一起。UserService
的代码里写死了new UserDaoImpl()
。 - 难以更换实现:如果有一天,我们不想用
UserDaoImpl
了,想换成一个新的、性能更好的UserDaoMongoImpl
(用 MongoDB 数据库),我们必须修改UserService
的源代码,把new UserDaoImpl()
改成new UserDaoMongoImpl()
。如果有一百个类都依赖UserDaoImpl
,你就得改一百次。 - 难以测试:当我们要对
UserService
进行单元测试时,我们希望用一个“假的”UserDao
(Mock 对象)来模拟数据库操作,而不是真的去连接数据库。但由于UserDaoImpl
是在UserService
内部new
出来的,我们根本没有办法在外部替换它,导致单元测试非常困难。
总结:控制权在 UserService
手中,它主动去创建依赖,导致了它和依赖的实现类之间强烈的耦合关系。
2. 使用 IoC / DI (控制反转)
在 IoC 模式下,UserService
不再自己创建依赖,它只负责“声明”自己需要一个 UserDao
类型的依赖。对象的创建和管理权被交给了 IoC 容器(例如 Spring)。
代码示例:
Java
// 使用 Spring 框架的注解来示意
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service // 告诉 Spring:这个类交给你管理
public class UserService {// 依赖一个接口,而不是具体的实现类private final UserDao userDao;// 通过构造函数注入依赖@Autowiredpublic UserService(UserDao userDao) {this.userDao = userDao;}public void register(User user) {// ... 其他业务逻辑userDao.addUser(user);}
}// UserDaoImpl 也需要被 Spring 管理
@Repository // 告诉 Spring:这个数据访问层类也交给你管理
public class UserDaoImpl implements UserDao {@Overridepublic void addUser(User user) {System.out.println("数据库:添加用户 " + user.getName());}
}
分析这种方式的好处(为什么会解耦):
-
实现了解耦:
UserService
只依赖于抽象的UserDao
接口,它根本不知道、也不关心最终传进来的是UserDaoImpl
还是UserDaoMongoImpl
。它只知道“我需要一个能操作用户的 DAO,是谁不重要,能用就行”。 -
轻松更换实现:如果我们想把
UserDaoImpl
换成UserDaoMongoImpl
,我们完全不需要修改UserService
的代码。只需要在 Spring 的配置中告诉它:“以后凡是需要UserDao
的地方,都给他UserDaoMongoImpl
的实例”。 -
非常容易测试:在进行单元测试时,我们可以非常轻松地手动创建一个
MockUserDao
对象,然后通过UserService
的构造函数传进去,从而实现对UserService
的隔离测试。Java
// 测试代码 @Test void testRegister() {// 创建一个假的 DaoUserDao mockDao = new MockUserDao(); // 手动注入这个假的 Dao 来创建 UserServiceUserService userService = new UserService(mockDao);userService.register(new User("test"));// ... 断言 mockDao 的方法是否被正确调用 }
总结:UserService
失去了创建依赖的控制权,这个控制权被“反转”给了 IoC 容器。容器负责创建好 UserDaoImpl
的实例,然后通过依赖注入(这里是构造函数注入)的方式“塞”给 UserService
。UserService
从一个主动的创建者,变成了一个被动的接收者。