IOC 和 DI 先认为是一回事儿,以理解其中的思想为主。
让调用类对某一接口实现类的依赖关系由第三方(容器或者协作类)注入,以移除调用类对某一接口实现类的依赖。
IOC 的目的是为了解耦,具体是怎么解耦的呢?
IOC 具体实现可以总结为两点:
- 类的成员变量用接口类型(父类)声明
- 类实例交给第三方管理
1. 类的成员变量用接口类型(父类)声明
《Effective Java》 第 5 条:优先考虑依赖注入来引用资源
public class SpellChecker {
// 首先用 Lexicon 这是个接口或者父类
private final Lexicon dict;
// 其次有构造器来 注入 一个实例
public SpellChecker(Lexicon dict) {
this.dict = dict;
}
public boolean isValid(String word) {...}
public List<String> suggestions(String type) {...}
}
直接看上面的代码,可能会想,平时写的代码不就是这个样子吗?这有啥,跟依赖注入有啥关系?
这就是依赖注入。很多人已经使用多年,只是不知道它的名字而已。
没有对比就没有伤害,看下下面的代码,就能理解了
public class SpellChecker {
private final EnglishLexicon dict = new EnglishLexicon();
public SpellChecker() {}
public boolean isValid(String word) {...}
public List<String> suggestions(String type) {...}
}
现在没有人会写这种代码了,大家已经习惯了上面的。
对比下面的代码,依赖注入的例子里,SpellChecker
只需要认识 Lexicon
这个接口,而不需要认识像 EnglishLexicon
或者 ChineseLexicon
这些具体实现类,就实现了 SpellChecker
与具体实现类的解耦,可复用性也更强,当换实现的时候,只需要在构造方法里,填充不同的实现类的实例即可。
public class Main {
public static void main(String[] args) {
// SpellChecker englishSpellChecker = new SpellChecker(new EnglishLexicon());
// 这里理解为更换国家,需要新的实现类,暂不理解为多个实现类共存
SpellChecker chineseSpellChecker = new SpellChecker(new ChineseLexicon());
}
}
2. 类实例交给第三方管理
在上面说注入实例的时候,用了这么一段代码。
public class Main {
public static void main(String[] args) {
// SpellChecker englishSpellChecker = new SpellChecker(new EnglishLexicon());
// 这里理解为更换国家,需要新的实现类,暂不理解为多个实现类共存
SpellChecker chineseSpellChecker = new SpellChecker(new ChineseLexicon());
}
}
这里其实就隐含了一个第三方(协作类): main 函数
,它管理了 new EnglishLexicon()
和 new ChineseLexicon()
这两个实例,这种方式被称为 手动依赖注入
。
关于网上说的纠结 这还不是自己 new 的么?
这个问题,现在应该有一个答案了吧,这是 手动依赖注入
,其中的 手动
就是在自己 new
。
别急,我知道你想说 Spring
这种框架,也可能还想说 工厂模式
创建 bean
之类的东西。
有 手动
那就得有 自动
对吧,Spring
, Guice
这类 IOC 容器就是 自动依赖注入
要不 Autowired
咋个会有 Auto
呢?
new
这个动作,是可以自己一个一个去 new
,远古时期的猿可能也就是这么干的,但慢慢聪明的猿发现,new
这个动作,实在只是个机械重复性动作,枯燥无聊,还业务无关,而计算机最擅长做的事情就是 重复
,所以就把这些工作交给了框架来完成了。
那框架里又是怎么完成呢?无非就是 反射 + 配置文件
,来实现 new
这个动作,当然说起来简单,做起来还是有各种困难的,什么循环依赖,引用未初始化完 bean 之类的问题也就接踵而至,具体实现细节,暂且不表,这里只是说明,自动
是怎么来的。
依赖注入框架
就是 自动依赖注入
的第三方了,总管实例的生成,存储,注入等等。容器本体是一个或者多个 Map {beanName: bean 实例}
。
@Component
public class SpellChecker {
private final Lexicon dict;
@Autowired
public SpellChecker(Lexicon dict) {
this.dict = dict;
}
public boolean isValid(String word) {...}
public List<String> suggestions(String type) {...}
}
@Configuration
public class LexiconConfig {
@Bean
public Lexicon lexicon() {
// 曾经是修改 xml 里 id="lexicon" 的实现类
// 现在是修改 new 后面的这个类
return new EnglishLexicon();
}
}
上面的代码就是现代使用 Spring
的时候的代码了,熟悉吧,当需要更换字典的时候就 return new ChineseLexicon()
;
使用配置文件的方式更能体现自动依赖注入的强大,也更有利于理解。
<bean id="lexicon" class="com.demo.lexicon.EnglishLexicon"></bean>
当需要修改实现的时候,只需要新建一个类,把 id 指过去就可以了,使用者甚至无需重新编译代码,只需要编译新增的这个类就可以了。
<bean id="lexicon" class="com.demo.lexicon.ChineseLexicon"></bean>
如果都是本来就有的类,根据业务调整,更换实现类,只需要改了配置,重启应用就可以了。
对于不知道 Spring 通过配置文件是怎么实例化 bean 的,一个简单示例
// 从 xml 中读到 "lexicon" 和 "com.demo.lexicon.ChineseLexicon" 两个字符串
// ioc 本体的一部分
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 简单示例,不是真实实现
singletonObjects.put("lexicon", Class.forName("com.demo.lexicon.ChineseLexicon").newInstance());
// @Autowired 就是在实例化 SpellChecker 的时候从 singletonObjects 中 getBean("lexicon") 来设置 dict 字段的。
// 这里边儿什么万一 lexicon 还没实例化完怎么办之类的问题,就需要自己去研读 Spring 的代码了。