简介
很早以前接触过spring,听说过关于这个框架最多的就是关于依赖注入和它带来的各种好处。那么spring框架的这些概念到底是什么意思呢?它对于我们实际中的开发有什么帮助呢?在这里,我们以一个初学者的视角一步步来理解和体会其中的作用。
从OO设计原则说起
当我们使用一些面向对象的语言来设计以及实现应用的时候,经常会强调一些基本的设计原则。其中最基本的一条就是面向抽象编程而不是具体的实现。为什么要面向抽象而不是一个具体的实现呢?我们来看一个如下的示例:
假设如上图我们有一个BusinessHandler类,它需要通过一个cvs文件读取信息来获取一组用户账号信息。这个时候,我们一个最直接的设计就是上图这样,我们直接将BusinessHandler和CVSAccountDAO对象关联起来。
基于前面讨论的面向对象设计原则来说,这样做有一些不合理。因为我们的类是和具体的类耦合在一起的,这里是解析一个CVS文件,如果我们程序要做单元测试的时候,BusinessHandler就必须要有一个对应的CVSAccountDAO实现以及关联的文件。因为这牵涉到对外部环境的依赖,可以说这个测试已经不是一个单纯的单元测试。所以,我们应该让BusinessHandler仅仅依赖于一个抽象的接口而不是具体的实现,这样后面要做单元测试的时候,我们也方便提供一些mock的实现。因此,按照这个思路,我们修改后的设计如下:
现在,到这一步,从BusinessHandler到AccountDAO这一步来说,它们的关系已经定义的比较好了。一个参考的实现如下:
BusinessHandler:
public class BusinessHandler {
private AccountDAO dao;
public BusinessHandler(AccountDAO dao) {
this.dao = dao;
}
public List<Account> getAccountList() {
return dao.FindAll();
}
}
AccountDAO:
public interface AccountDAO {
List<Account> findAll();
}
组装起来
前面定义好的BusinessHandler和AccountDAO接口这样的关系是挺好的。可是我们在一些应用中的时候还是会碰到一些问题,比如说我们在应用程序中需要将BusinessHandler和一个具体的实现组装起来的时候,比如下图这样:
这个时候,我们的应用程序App类里需要引用到AccountDAO的一个具体实现。而且从实现的要求来说,必然是CVSAccountDAO。那么这部分参考实现的代码如下:
public class App {
public static void main() {
AccountDAO dao = new CVSAccountDAO();
BusinessHandler handler = new BusinessHandler(dao);
List<Account> list = handler.getAccountList();
//...
}
}
这部分代码如果是最后的实现的话,其实也没什么问题。因为我们这里定义了一个类和它对一个接口的依赖,所以需要创建这个被依赖的对象然后在作为参数提供给依赖对象。这里就是多了这么一步而已没什么问题。可是在实际情况中,随着问题更加复杂化,很多情况也会出现。比如说,如果我们的CVSAccountDAO它也有一个依赖呢?比如说我们要读取的文件并不在本地,它是在网络上某个地址的。这个时候,我们的系统设计就可能会是这样了:
这个时候,如果我们再来拼装它们的话,可能会是如下的代码:
public static void main(String[] args) {
FildReader reader = new NetworkFileReader();
AccountDAO dao = new CVSAccountDAO(reader);
BusinessHandler handler = new BusinessHandler(dao);
List<Account> list = handler.getAccountList();
// ...
}
这个时候,我们会发现因为引入越多的抽象我们的代码显得越发的繁杂。因为我们引入越来越多的接口 ,在具体的拼装的时候就需要将这些关联实现都拼接起来。
IOC和依赖注入
我们来看前面代码里的问题。我们在实际应用程序里,实际上只是使用到BusinessHandler,虽然BusinessHandler要真正能够跑起来需要有一堆的依赖,但是这个不是我们所关心的。我们只需要一个符合我们期望的对象。比如我想要组装一个mock的实现,或者一个针对数据库访问的实现。在前面的代码里,每次如果要组装哪个对象需要自己在代码里显式的标注好,而且后面要是业务逻辑变了我们就需要自己去改这部分代码。
因此,原来的思路是我们自己来构造一个对象,然后自己拿到应用里使用。而IOC的实现就换了一种思路,我们可不可以把要什么样的对象告诉一个第三方或者服务方。然后这边就可以得到想要的对象了。不是什么都自己来构造,而是由别人来构造。按照这个思路,我们的设计图将变成如下:
这个时候,应用程序App只是直接和一个Assembler来打交道,然后它给了一个我们需要的BusinessHandler对象来使用。这种方式有几个好处,一个是对象的创建不需要我直接在代码里处理,尤其当依赖关系比较复杂的时候,代码精简了很多。一个是对于这些对象的生命周期管理也统一由Assembler管理了。
在这里,我们针对IOC和DI的概念再思考和讨论一下。IOC的意思是inversion of control。既然是控制反转,那么是哪里的控制,对什么的控制呢?怎么个反转法?这可能需要从一些应用程序的开发和运行流程说起。在有的程序里,我们需要开发人员自己定义程序的入口,比如一个main函数,然后在这个函数里执行各种流程。这个时候,我们在程序里指定各种步骤,第一步做什么,第二步做什么等等。这时候,各种依赖和步骤都是在我们程序主体中,也可以说是程序员定义的过程中来确定的。相当于我们在控制着程序的一切。
但是,在某些情况下。比如一些事件触发的处理,或者某些框架里定义的一些行为的处理。一些典型的示例有想js里,一些页面按钮的点击操作,它会触发一个onclick方法。在一些桌面应用的开发里,也有类似的实现。我们实现一个对应的事件处理方法。但是这里不是我们写的程序来决定自己什么时候调用这个事件处理方法,而是在这个事件触发的时候我们定义的这个方法被调用。这个时候,我们好像失去了对程序执行流程的控制,变成了我们提供一些实现,由框架或者系统来调用它们。这个过程可以说是一种控制反转。反转的是程序执行的流程。
再结合前面的示例来看,我们需要有这么一个assembler,由它来提供我们需要对象的创建。这里,我们相当于将对象创建和关联的控制移交给这个assembler了。这也是一种控制的反转。而这里,我们原本需要管理的就是对需要对象构造一系列的依赖,以达到构造一个期望对象的目的。这种通过某个assembler来达到实现依赖管理和对象构造的方式也叫依赖注入,也就是DI(dependency injection)。所以说,依赖注入用来描述我们这个问题的场景更加确切一些。DI相当于IOC的一种特殊情况。IOC反转的可不仅仅是对于依赖的管理。
spring中依赖的定义和注入
前面提到了我们把对象的创建和提供都由第三方来提供。在spring里,这就是创建和存储对象的地方,称其为容器。既然这里对象的创建还是由我们来指派只是不需要我们来一个个的new,那么该怎么来具体的指派它们呢?最常用的几种方式是通过xml文件的定义。以上面的对象关系为例,我们最开始定义的BusinessHandler对AccountDAO的依赖是在定义构造函数的时候引入的。于是在xml里,如果我们希望将CVSAccountDAO作为真正的参数传入给BusinessHandler,则需要在文件里定义一组内容如下:
<bean id="handler" class="BusinessHandler">
<constructor-arg index="0" ref="CVSAccountDAO" />
</bean>
<bean id="CVSAccountDAO" class="CVSAccountDAO"/>
而我们应用程序里创建对象和使用它们的代码如下:
public static void main( String[] args )
{
String springConfig = "applicationContext.xml";
ApplicationContext context = new ClassPathXmlApplicationContext(springConfig);
BusinessHandler handler = (BusinessHander) context.getBean("BusinessHandler");
handler.getAccountList();
// ...
}
这样,对象创建的繁琐步骤就都交给spring容器了,我们只要拿来用就可以。
当然,除了上述的依赖关系定义,如果我们前面BusinessHandler对AccountDAO的依赖定义成如下的方式:
public class BusinessHandler {
private AccountDAO dao;
public void setAccountDAO(AccountDAO dao) {
this.dao = dao;
}
public List<Account> getAccountList() {
return dao.FindAll();
}
}
这种方式的依赖和前面的构造函数式的依赖不同,我们称之为属性注入,如果系统设定特定的对象构造,则对应的配置文件如下:
<bean id="handler" class="BusinessHandler"> <property name="dao" ref="CVSAccountDAO"/> </bean> <bean id="CVSAccountDAO" class="CVSAccountDAO"/>
前面应用程序的代码不需要做任何的改变。同样,如果我们需要在程序里修改一下依赖对象的关系,只需要在里面创建对应对象的bean,并修改一下他们的引用关系就可以了。这样可以保证原来程序的代码不用变更。
总结
依赖注入,其本质上就是保证实现代码里没有对具体实现的依赖,达到一个松耦合的情况。这种方式方便我们在程序开发的时候独立的开发个部分组件,而且更好的进行测试。同时,spring容器的特性方便对象的组装。我们也不用在程序里依赖关系复杂的时候去写这么一大堆的对象构造和拼装的辅助代码,也使得后续的实现更加简洁。
参考材料
http://www.amazon.com/Spring-Practice-Willie-Wheeler/dp/1935182056/ref=sr_1_1?s=books&ie=UTF8&qid=1419323762&sr=1-1&keywords=spring+in+practice
http://www.amazon.com/Spring-Action-Craig-Walls/dp/161729120X/ref=sr_1_2?s=books&ie=UTF8&qid=1419323762&sr=1-2&keywords=spring+in+practice
http://martinfowler.com/articles/injection.html
http://www.amazon.com/gp/product/1935182056/ref=s9_simh_gw_p14_d2_i2?pf_rd_m=ATVPDKIKX0DER&pf_rd_s=desktop-2&pf_rd_r=1C8K4JW160M7X63CVR4G&pf_rd_t=36701&pf_rd_p=1970566782&pf_rd_i=desktop