控制反转(Inversion of Containers,缩写为IoC), 是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。IoC意味着将被依赖对象交给IoC容器生成,而不是传统的在依赖对象内部直接new。
传统Java SE程序设计,我们直接在依赖对象里通过new来创建被依赖对象,是程序主动去创建被依赖对象;而IoC是有专门一个容器来创建这些对象,即由Ioc容器来控制对象的创建;下面我使用Martin fowler关于控制反转一文中传统(也就是我们平时通过new来创建对象的过程)创建对象的例子来展示为啥要使用IoC,这个例子用于提供一份电影清单,清单上列出的影片都是由一位特定的导演执导的。这份清单我使用的是txt文件来保存,首先我先展示一些这份清单的内容,如下图,每个影片以分号来区分,而冒号前面代表的是导演名字,冒号后面代表的是影片的名称。
文件放在如下图的位置,文件名为movies1.txt
如下代码:
首先是Movie类,用来保存影片信息
public class Movie {
private String director;
private String name;
public String getDirector() {
return director;
}
public void setDirector(String director) {
this.director = director;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
然后定义一个finder接口,用来返回所有的影片信息
package com.lgy.test.ioc;
import java.util.List;
public interface MovieFinder
{
List findAll();
}
接下来就是实现Finder接口,实现对影片信息搜索的具体操作。
package com.lgy.test.ioc.tradition;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import com.lgy.test.ioc.Movie;
import com.lgy.test.ioc.MovieFinder;
public class ColonDelimitedMovieFinder implements MovieFinder{
private String filename;
public ColonDelimitedMovieFinder(String filename) {
this.filename = filename;
}
@Override
public List findAll() {
List list = new ArrayList<Movie>();
String data = null;
File file = new java.io.File("C:\\EclipseWorkspace\\TestIOC\\src\\main\\resources\\"+filename);
FileInputStream fis = null;
BufferedReader reader = null;
try {
if (file != null&&file.exists()) {
fis = new FileInputStream(file);
if (fis != null) {
reader = new BufferedReader(new InputStreamReader(fis));
data = reader.readLine();
}
}
fis.close();
reader.close();
} catch (Exception e) {
// TODO: handle exception
}
if (data != null&&data.length()>0) {
String[] strings = data.split(";");
for (int i = 0; i < strings.length; i++) {
String[] strings2 = strings[i].split(":");
Movie movie = new Movie();
movie.setDirector(strings2[0]);
movie.setName(strings2[1]);
list.add(movie);
}
}
return list;
}
}
最后就是筛选出指定导演拍摄的所有影片
package com.lgy.test.ioc.tradition;
import java.util.Iterator;
import java.util.List;
import com.lgy.test.ioc.Movie;
import com.lgy.test.ioc.MovieFinder;
public class MovieLister {
private MovieFinder finder;
public MovieLister()
{
finder = new ColonDelimitedMovieFinder("movies1.txt");
}
public Movie[] moviesDirectedBy(String arg) {
List allMovies = finder.findAll();
for (Iterator it = allMovies.iterator(); it.hasNext();) {
Movie movie = (Movie) it.next();
if (!movie.getDirector().equals(arg)) it.remove();
}
return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]);
}
}
最后输入指定导演拍摄的电影
package com.lgy.test.ioc;
import com.lgy.test.ioc.tradition.MovieLister;
public class Test {
public static void main(String[] args) {
MovieLister lister = new MovieLister();
Movie[] movies = lister.moviesDirectedBy("lgy");
for (int i = 0; i < movies.length; i++) {
System.out.println("director:"+movies[i].getDirector()+" name:"+movies[i].getName());
}
}
}
这个功能的实现极其简单:moviesDirectedBy方法首先请求finder(影片搜寻者)对象返回后者所知道的所有影片,然后遍历finder对象返回的清单,并返回其中由特定的某个导演执导的影片。
从上面的例子可以看出,MovieLister和Finder是存在耦合的,即MovieLister依赖Finder,如下图,
除此之外,客户端(Test类)与MovieLister也存在耦合,即客户端依赖MovieLister
从中这个例子我们知道依赖对象MovieLister控制了被依赖对象Finder的生成,这个例子很好的展示了“依赖对象里通过new来创建被依赖对象,是程序主动去创建被依赖对象”这句话。
为什么要实现解耦?传统的方式(即直接在依赖对象的类里创建被依赖对象的方式)存在什么问题?
先回答第一个问题,为啥要解耦?是为了设计出一个容易维护,扩展性好的代码。下面通过例子说明高耦合会产生一些什么不好的后果。
如上图,假设图中使用的MovieLister类在很多地方都被调用了,并且都是通过new的方式来创建的,但是有一天,需求改变了,这个MovieLister已经不能满足需求了,假设无法通过修改MovieLister来满足需求或者项目里已经有一个类满足需求,只需要把MovieLister换成这个类即可。这种通过传统方式来创建对象的方法会造成以下问题:
(1)你需要将所有使用到MovieLister的地方都进行修改。
(2)或许你会说,每个地方都进行修改有什么问题?通过全局搜索关键之“MovieLister”,然后选中全部替换不上很简单的事吗?因为每个地方使用到MovieLister的逻辑都是不一样的,如果你贸然对如此多处地方进行修改,且不去捋清各自的逻辑,很有可能会存在隐患。
(3)假设项目里使用MovieLister的地方很少,只有10处,你捋清这10处地方的逻辑,但是,假设有100处呢?这是一个十分耗时的过程。维护起来十分困难。
因此为了实现低耦合,人们通过各种设计模式来实现低耦合,使得程序变得容易维护,拓展性好。在设计模式中,常说要基于接口开发,为啥要基于接口开发呢?使用上面的例子来解释一下:
从图中可以看出,MovieFinder是一个接口,而ColonDelimitedMovieFinder是这个接口的一个实现类。这样做有什么好处?通过这个方式,可以很好的减少要修改的代码,正如上面例子介绍的,如果对象的引用和和对象都是具体类,如果有多达10处地方使用到MovieFinder,那么你至少要修改至少20处地方(因为你要修改对象的引用和对象),如果通过接口的方式来声明,那么要修改的地方就会减少一半。虽然这样可以减轻一些工作量。但是这种操作还是远远不够的,它依然是有很高的耦合性。
那么IoC的优势在哪?先对比一下传统的new方式和IoC方式类图,
传统方式:
IoC(这里是通过设值注入方式来实现IoC的,类似与Spring的方法)
从这里可以看出,MovieLister和Finder的对象都是通过IoC容器来创建的,使用这种设计模式,假设你再碰到多个地方使用到Finder的,需求改变的时候,你只需要再这个xml文件里修改一下即可。
在此可以得出结论,IoC可以更好的实现低耦合,降低代码的维护量,增强代码的可拓展性。这种方式将依赖对象控制对象生成的操作,转交给IoC容器来控制生成,再由IoC容器转交给调用者来使用生成的对象。
依赖注入(Dependency Injection,简称DI)。其实依赖注入是属于实现IoC设计模式的一个手段。组件之间依赖关系由容器在运行期决定,依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。
下面介绍Dependency Injection模式的几种不同形式。
这里使用名为PicoContainer的轻量级容器完成依赖注入。首先在项目里依赖picocontainer包
然后是创建获取指定导演电影的MovieLister类, PicoContainer通过构造函数来判断如何将MovieFinder实例注入MovieLister 类。因此,MovieLister类必须声明一个构造函数,并在其中包含所有需要注入的元素:
接着是查找所有电影的Finder类, MovieFinder实例本身也将由PicoContainer来管理,因此文本文件的名字也可以由容器注入:
随后,需要告诉PicoContainer:各个接口分别与哪个实现类关联、将哪个字符串注入MovieFinder组件。
可以看到,关于Finder和MovieLister的配置都在这个方法里进行了,当然,你可以把这个配置方法单独放到一个类里去。
Spring 框架是一个用途广泛的企业级Java 开发框架,其中包括了针对事务、持久化框架、web应用开发和JDBC等常用功能的抽象。和PicoContainer一样,它也同时支持构造函数注入和设值方法注入,但该项目的开发者更推荐使用设值方法注入。
首先,先引入Spring包,这里我是通过Maven导入Spring-MVC,但是其实你只需要导入spring-context
为了让MovieLister类接受注入,我需要为它定义一个设值方法,该方法接受类型为MovieFinder的参数:
类似地,在MovieFinder的实现类中,我也定义了一个设值方法,接受类型为String 的参数:
然后是设定配置文件。Spring 支持多种配置方式,你可以通过XML 文件进行配置,也可以直接在代码中配置。不过,XML 文件是比较理想的配置方式。
输出结果和上一个例子一致。
通过接口完成注入。Avalon框架就使用了类似的技术。
首先,我需要定义一个接口,组件的注入将通过这个接口进行。在本例中,这个接口的用途是将一个MovieFinder实例注入继承了该接口的对象。
这个接口应该由提供MovieFinder接口的人一并提供。任何想要使用MovieFinder实例的类(例如MovieLister类)都必须实现这个接口。
然后,我使用类似的方法将文件名注入MovieFinder的实现类:
现在,还需要用一些配置代码将所有的组件实现装配起来。简单起见,我直接在代码中完成配置,并将配置好的MovieLister 对象保存在名为lister的字段中:
Martin Fowler的《Inversion of Control Containers and the Dependency Injection pattern》原文:
https://martinfowler.com/articles/injection.html
《Inversion of Control Containers and the Dependency Injection pattern》译文:
https://blog.csdn.net/remixone/article/details/78761027
上面贴的代码是完整的,花点时间整理一下就可以得到一个完整的代码了。对于不想花时间的同学,可以通过以下支付宝付款码支付1元并附上自己的邮箱
或者通过淘宝支付,支付的时候请附上自己的邮箱
https://item.taobao.com/item.htm?spm=a2oq0.12575281.0.0.264f1debTX35SJ&ft=t&id=602253362650
即可直接获取源码。