IoC(Inverse of Control)控制反转:是Spring容器的内核,AOP、声明式事务等功能在此基础上开花结果。但是IoC这个重要的概念却比较晦涩难懂。IoC确实包含很多内涵,它涉及代码解耦、设计模式、代码优化等问题的考量,我们打算通过一个小例子来说明这个概念。
1.通过实例理解IoC的概念
我们以一个城门问话的情景来进行描述。张之亮的《墨攻》中有这样一个镜头:当刘德华所饰演的墨者革离到达梁国城下,守军问道:来者何人?刘德华回答:“墨者革离!”。
public class MoAttack{
public void cityGateAsk(){
//演员直接侵入剧本
LiuDeHua ldh=new LiuDeHua();
ldh.responseAsk("墨者革离!");
}
}
我们会发现在这个地方,作为具体角色饰演者的刘德华直接侵入到剧本中,使剧本和演员直接耦合在一起了。
一个明智的编剧在剧情创作的时候应该围绕着故事的角色进行,而不应考虑角色的具体饰演者,这样才可能在剧本投拍时自由地筛选任何合适的演员,而非绑定在刘德华一个人身上。通过这样的分析,我们似乎需要为该剧的主人公革离定义一个接口:
public class MoAttack{
public void cityGateAsk(){
GeLi geli=new LiuDeHua();
geli.responseAsk("墨者革离!");
}
}
可是,我们可以看到MoAttack同时依赖于GeLi接口和LiuDeHua类,并没有达到我们期待的剧本仅仅依赖于角色的目的。但是角色最终必须通过具体的演员才能完成拍摄,如何让LiuDeHua和剧本无关又能完成GeLi的具体动作呢?当然是在影片投拍的时候,导演将LiuDeHua安排在GeLi的角色上,导演将剧本、角色、饰演者装配起来。
通过引入导演,使得剧本和具体的饰演者解除耦合了。对应到软件之中,导演像是一个装配器,安排演员表演具体的角色。
现在,我们可以反过来讲解IoC的概念了。它包括两方面的内容,一是控制,二是反转。
那到底什么东西的“控制”被“反转”了呢?
对应前面的例子,“控制”是指选择GeLi角色扮演者的控制权;反转是指这种控制权从《墨攻》剧本中移除,转交到导演的手中。
对于软件来说,即是某一接口具体实现类的选择控制权从调用类中移除,转交给第三方决定。
因为IoC确实不够开门见山,因此业界曾进行了广泛的讨论,最终引入了DI(依赖注入:Dependency Injection)的概念用以代替IoC,即让调用类对某一接口实现类的依赖关系由第三方(容器或是协作类)注入,以移除调用类对某一接口实现类的依赖。“依赖注入”显然比“控制反转”直接明了,易于理解。
2.IoC的类型
从注入方法上看,主要可以划分为三种类型:构造函数注入、属性注入以及接口注入。Spring支持构造函数注入以及属性注入,那么我们来看一下它们的区别。
2.1 构造函数注入
在构造函数注入中,我们通过调用类的构造函数,将接口实现类通过构造函数变量进行传入:
public class MoAttack{
private GeLi geli;
public MoAttack(GeLi geli){
this.geli=geli;
}
public void cityGateAsk(){
geli.responseAsk("墨者革离!");
}
}
MoAttack的构造函数不关心具体是谁扮演革离这个角色,只要传入的扮演者按照剧本的要求完成相应的表演即可。角色的具体扮演者由导演来安排:
public class Director{
public void direct(){
GeLi geli=new LiuDeHua();//导演安排刘德华饰演革离这一角色。
MoAttack moAttack=new MoAttack(geli);//将刘德华注入到《墨攻》的剧本之中。
moAttack.cityGateAsk();//开始“城门叩问剧情的演出工作”。
}
}
2.2属性注入
有时,导演会发现,虽然革离是影片《墨攻》的第一男主角,但并非每个场景都需要革离的出现。这种情况下,使用构造函数注入可能并不妥当,这时可以考虑属性注入。属性注入可以有选择地通过Setter方法完成调用类所需依赖的注入,更加灵活方便:
public class MoAttack{//MoAttack:通过Setter方法注入革离扮演者
private GeLi geli;
public void setGeli(GeLi geli){//Setter方法,以便让导演在需要时注入geli的具体扮演者
this.geli=geli;
}
public void cityGateAsk(){
geli.response("墨者革离!");
}
}
public class Director{//通过Setter方法注入革离扮演者
public void direct(){
GeLi geli=new LiuDeHua();
MoAttack moAttack=new MoAttack();
moAttack.setGeli(geli);
moAttack.cityGateAsk();
}
}
和通过构造函数注入革离扮演者不同,在实例化MoAttack剧本时,并未指定任何扮演者,而是实例化MoAttack后,在需要革离出场时,才调用其setGeli()方法注入扮演者。按照类似的方式,我们还可以分别为剧本中其他的角色提供注入的Setter方法,这样,导演就可以根据所拍剧段的不同,注入相应的角色了。
3.通过容器完成依赖关系的注入
虽然MoAttack和LiuDeHua实现了解耦,MoAttack无需关注角色实现类的实例化工作,但这些工作在代码中依然存在,只是转移到了Director类中而已。假设某一制片人想改变这一局面,在选择某个剧本后,希望通过一个第三方中介结构选择导演、演员,让他们各司其职,那剧本、导演、演员就实现解耦了。
所谓第三方中介机构是一个第三方的容器,它帮助完成类的初始化与装配工作,让开发者从这些底层实现类的实例化、依赖关系装配等工作中脱离出来。Spring就是这样一个容器,它通过配置文件或注解描述类与类之间的依赖关系,自动完成类的初始化与注入工作。