Ioc也有称为DI(Dependecy Injection 依赖注射),由Martin Fowler的一篇《Inversion of Control Containers and the Dependency Injection pattern》提出。
分离关注( Separation of Concerns : SOC)是Ioc模式和AOP产生最原始动力,通过功能分解可得到关注点,这些关注可以是组件Components, 方面Aspects或服务Services。
从GoF设计模式中,我们已经习惯一种思维编程方式:Interface Driven Design 接口驱动,接口驱动有很多好处,可以提供不同灵活的子类实现,增加代码稳定和健壮性等等,但是接口一定是需要实现的,也就是如下语句迟早要执行:
AInterface a = new AInterfaceImp();
AInterfaceImp是接口AInterface的一个子类,Ioc模式可以延缓接口的实现,根据需要实现,有个比喻:接口如同空的模型套,在必要时,需要向模型套注射石膏,这样才能成为一个模型实体,因此,我们将人为控制接口的实现成为“注射”。
Ioc英文为 Inversion of Control,即反转模式,这里有著名的好莱坞理论:你呆着别动,到时我会找你。
其实Ioc模式也是解决调用者和被调用者之间的一种关系,上述AInterface实现语句表明当前是在调用被调用者AInterfaceImp,由于被调用者名称写入了调用者的代码中,这产生了一个接口实现的原罪:彼此联系,调用者和被调用者有紧密联系,在UML中是用依赖 Dependency 表示。
但是这种依赖在分离关注的思维下是不可忍耐的,必须切割,实现调用者和被调用者解耦,新的Ioc模式 Dependency Injection 模式由此产生了, Dependency Injection模式是依赖注射的意思,也就是将依赖先剥离,然后在适当时候再注射进入。
一、Ioc模式(Dependency Injection模式)有三种:
第一种类型 从JNDI或ServiceManager等获得被调用者,这里类似ServiceLocator模式。 1. EJB/J2EE,2. Avalon(Apache的一个复杂使用不多的项目)
第二种类型 使用JavaBeans的setter方法 1. Spring Framework,2. WebWork/XWork
第三种类型 在构造方法中实现依赖 1. PicoContainer,2. HiveMind
有过EJB开发经验的人都知道,每个EJB的调用都需要通过JNDI寻找到工厂性质的Home接口,在我的教程EJB是什么章节中,我也是从依赖和工厂模式角度来阐述EJB的使用。
在通常传统情况下,为了实现调用者和被调用者解耦,分离,一般是通过工厂模式实现的,下面将通过比较工厂模式和Ioc模式不同,加深理解Ioc模式。
二、工厂模式和Ioc
假设有两个类B 和 C:B作为调用者,C是被调用者,在B代码中存在对C的调用:
public class B{
private C comp;
......
}
实现comp实例有两种途径:单态工厂模式和Ioc。
工厂模式实现如下:
public class B{
private C comp;
private final static MyFactory myFactory = MyFactory.getInstance();
public B(){
this.comp = myFactory.createInstanceOfC();
}
public void someMethod(){
this.comp.sayHello();
}
......
}
特点:
每次运行时,MyFactory可根据配置文件XML中定义的C子类实现,通过createInstanceOfC()生成C的具体实例。
使用Ioc依赖性注射( Dependency Injection )实现Picocontainer如下,B类如同通常POJO类,如下:
public class B{
private C comp;
public B(C comp){
this.comp = comp;
}
public void someMethod(){
this.comp.sayHello();
}
......
}
假设C接口/类有有一个具体实现CImp类。当客户端调用B时,使用下列代码:
public class client{
public static void main( String[] args ) {
DefaultPicoContainer container = new DefaultPicoContainer();
container.registerComponentImplementation(CImp.class);
container.registerComponentImplementation(B.class);
B b = (B) container.getComponentInstance(B.class);
b.someMethod();
}
}
因此,当客户端调用B时,分别使用工厂模式和Ioc有不同的特点和区别:
主要区别体现在B类的代码,如果使用Ioc,在B类代码中将不需要嵌入任何工厂模式等的代码,因为这些工厂模式其实还是与C有些间接的联系,这样,使用Ioc彻底解耦了B和C之间的联系。
使用Ioc带来的代价是:需要在客户端或其它某处进行B和C之间联系的组装。
所以,Ioc并没有消除B和C之间这样的联系,只是转移了这种联系。
这种联系转移实际也是一种分离关注,它的影响巨大,它提供了AOP实现的可能。
Ioc和AOP
AOP我们已经知道是一种面向切面的编程方式,由于Ioc解放自由了B类,而且可以向B类实现注射C类具体实现,如果把B类想像成运行时的横向动作,无疑注入C类子类就是AOP中的一种Advice,如下图:
通过下列代码说明如何使用Picocontainer实现AOP,该例程主要实现是记录logger功能,通过Picocontainer可以使用简单一行,使所有的应用类的记录功能激活。
首先编制一个记录接口:
public interface Logging {
public void enableLogging(Log log);
}
有一个LogSwitcher类,主要用来激活具体应用中的记录功能:
import org.apache.commons.logging.Log;
public class LogSwitcher
{
protected Log m_log;
public void enableLogging(Log log) {
m_log = log;
m_log.info("Logging Enabled");
}
}
一般的普通应用JavaBeans都可以继承这个类,假设PicoUserManager是一个用户管理类,代码如下:
public class PicoUserManager extends LogSwitcher
{
..... //用户管理功能
}
public class PicoXXXX1Manager extends LogSwitcher
{
..... //业务功能
}
public class PicoXXXX2Manager extends LogSwitcher
{
..... //业务功能
}
注意LogSwitcher中Log实例是由外界赋予的,也就是说即将被外界注射进入,下面看看使用Picocontainer是如何注射Log的具体实例的。
DefaultPicoContainer container = new DefaultPicoContainer();
container.registerComponentImplementation(PicoUserManager.class);
container.registerComponentImplementation(PicoXXXX1Manager.class);
container.registerComponentImplementation(PicoXXXX2Manager.class);
.....
Logging logging = (Logging) container.getComponentMulticaster();
logging.enableLogging(new SimpleLog("pico"));//激活log
由上代码可见,通过使用简单一行logging.enableLogging()方法使所有的应用类的记录功能激活。这是不是类似AOP的advice实现?
总之,使用Ioc模式,可以不管将来具体实现,完全在一个抽象层次进行描述和技术架构,因此,Ioc模式可以为容器、框架之类的软件实现提供了具体的实现手段,属于架构技术中一种重要的模式应用。J道的JdonSD框架也使用了Ioc模式。
参考资料:
Inversion of Control Containers and the Dependency Injection pattern
A Brief Introduction to IoC
Ioc容器的革命性优点
Java企业系统架构选择考量
IOC模式的思考和疑问
三、IoC的几种实现类型
(1)Type1接口注入
通常做法是利用接口将调用者与实现者分离。
public class Sport {
private InterfaceBall ball; //InterfaceBall是定义的接口
public void init() {
//Basketball实现了InterfaceBall接口
ball = (InterfaceBall) Class.forName("Basketball").newInstance();
}
}
Sport类在编译期依赖于InterfaceBall的实现,为了将调用者与实现者分离,我们动态生成Basketball类并通了强制类型转换为InterfaceBall。Apache Avalon是一个典型的Type1型IoC容器。
(2)setter方法注入
在类中暴露setter方法来实现依赖关系。
public class Sport {
private InterfaceBall ball;
public void setBall(InterfaceBall arg) {
ball = arg;
}
}
这种方式对已经习惯了JavaBean的程序员而言,更显直观。Spring就是实现了该类型的轻量级容器。
(3)Type3构造子注入
即通过构造方法完成依赖关系。
public class Sport {
private InterfaceBall ball;
public Sport(InterfaceBall arg) {
ball = arg;
}
}
可以看到,通过类的构造方法建立依赖关系。由于Type3在构造期就形成了对象的依赖关系,即存对象的重用变的困难。有些框架需要组件提供一个默认的构造方法,此时就体现了Type3的局限性。通常所有的参数都是通过构造方法注入的,当对象间的依赖关系较多时,构造方法就显的比较复杂,不利于单元测试。PicoContainer就是实现了Type3依赖注入模式的轻量级容器。
AOP:面向方面编程
DI依赖项插入和面向方面编程是互补的技术,所以想把它们结合在一起使用是很自然的。
依赖项插入和面向方面编程(AOP)是两个关键的技术,有助于在企业应用程序中简化和纯化域模型和应用程序分层。依赖项插入封装了资源和协调器发现的细节,而方面可以(在其他事情中)封装中间件服务调用的细节 —— 例如,提供事务和安全性管理。因为依赖项插入和 AOP 都会形成更简单、更容易测试的基于对象的应用程序,所以想把它们结合在一起使用是很自然的。方面可以帮助把依赖项插入的能力带到更广的对象和服务中,而依赖项插入可以用来对方面本身进行配置。
在这篇文章中,我将介绍如何把 Spring 框架的依赖项插入与用 AspectJ 5 编写的方面有效地结合在一起。我假设您拥有基本的 AOP 知识(如果没有这方面知识 ,可以在 参考资料 中找到一些良好的起点),所以我的讨论将从对基于依赖项插入的解决方案中包含的关键角色和职责的分析开始。从这里,我将介绍如何通过依赖项插入配置单体(singleton)方面。因为配置非单体方面与配置域对象共享许多公共内容,所以后面我会研究一个应用于这两者的简单解决方案。总结这篇文章时,我会介绍如何为多个高级依赖项插入场景使用方面,其中包括基于接口的插入和重复插入。。。
祥细请参考
http://www-128.ibm.com/developerworks/cn/java/j-aopwork13.html
IoC全名Inversion of Control,如果中文硬要翻譯過來的話,就是「控制反轉」。初看IoC,從字面上不容易瞭解其意義,我覺得要瞭解IoC,要先從Dependency Inversion開始瞭解,也就是依賴關係的反轉。
Dependency Inversion在下面這篇文章中有了清楚的解釋:
http://www.objectmentor.com/publications/dip.pdf
簡單的說,在模組設計時,高層的抽象模組通常是與業務相關的模組,它應該具有重用性,而不依賴於低層的實作模組,例如如果低層模組原先是軟碟存取模式,而高層模組是個存檔備份的需求,如果高層模組直接叫用低層模組的函式,則就對其產生了依賴關係。
舉個例子,例如下面這個程式:
#include <floppy.h> .... void save() { .... saveToFloppy() } }
由於save()程式依賴於saveToFloppy(),如果今天要更換低層的存儲模組為Usb碟,則這個程式沒有辦法重用,必須加以修改才行,低層模組的更動造成了高層模組也必須跟著更動,這不是一個好的設計方式,我們希望模組都依賴於模組的抽象,這樣才可以重用高層的業務設計。
如果以物件導向的方式來設計,依賴反轉(Dependency Inversion)的解釋變為程式不應依賴實作,而是依賴於抽象,實作必須依賴於抽象。我們來看看下面這個Java程式:
public class BusinessObject { private FloppyWriter writer = new FloppyWriter(); .... public void save() { ... writer.saveToFloppy(); } }
在這個程式中,BusinessObject的存檔依賴於實際的FloppyWriter,如果今天我們想要將存檔改為存至Usb碟,我們必須修改或繼承BusinessObject進行擴展,而無法直接使用BusinessObject。
如果透過介面的宣告,可以改進此一情況,例如:
public interface IDeviceWriter { public void saveToDevice(); } public class BusinessObject { private IDeviceWriter writer; public void setDeviceWriter(IDeviceWriter writer) { this.writer = writer; } public void save() { .... writer.saveToDevice(); } }
這樣一來,BusinessObject就是可重用的,如果今天我有存儲至Floppy或Usb碟的需求,我只要實作IDeviceWriter即可,而不用修改BusinessObject:
public class FloppyWriter implement IDeviceWriter { public void saveToDevice() { .... // 實際儲存至Floppy的程式碼 } } public class UsbDiskWriter implement IDeviceWriter { public void saveToDevice() { .... // 實際儲存至UsbDisk的程式碼 } }
從這個角度來看,Dependency Inversion的意思即是程式不依賴於實作,而是程式與實作都要依賴於抽象。
IoC的Control是控制的意思,其實其背後的意義也是一種依賴關係的轉移,如果A依賴於B,其意義即是B擁有控制權,我們要轉移這種關係,所以依賴關係的反轉即是控制關係的反轉,藉由控制關係的轉移,我們可以獲得元件的可重用性,在上面的Java程式中,整個控制權從實際的 FloppyWriter轉移至抽象的IDeviceWriter介面上,使得BusinessObject、FloppyWriter、 UsbDiskWriter這幾個實現依賴於抽象的IDeviceWriter介面。
從容器(Container)的角度,程式的業務邏輯部份應是可以重用的,不應受到所使用框架或容器的影響,因為我們可能轉移整個業務邏輯至其它的框架或容器,如果業務邏輯過於依賴容器,則轉移至其它的框架或容器時,就會發生困難。
IoC在容器的角度,可以用這麼一句好萊塢名言來代表:"Don't call me, I'll call you." 以程式的術語來說的話,就是「不要向容器要求您所需要的(物件)資源,容器會自動將這些物件給您!」。IoC要求的是容器不侵入應用程式本身,應用程式本身提供好介面,容器可以透過這些介面將所需的資源注至程式中,應用程式不向容器主動要求資源,故而不會依賴於容器的元件,應用程式本身不會意識到正被容器使用,可以隨時從容器中脫離轉移而不用作任何的修改,而這個特性正是一些業務邏輯中間件最需要的。