[Java][依赖注入和控制反转]

1.原文地址:http://www.lgygg.wang/lgyblog/2019/09/04/java%e4%be%9d%e8%b5%96%e6%b3%a8%e5%85%a5%e5%92%8c%e6%8e%a7%e5%88%b6%e5%8f%8d%e8%bd%ac/

2.什么是控制反转

控制反转(Inversion of Containers,缩写为IoC), 是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。IoC意味着将被依赖对象交给IoC容器生成,而不是传统的在依赖对象内部直接new。

1)控制什么

传统Java SE程序设计,我们直接在依赖对象里通过new来创建被依赖对象,是程序主动去创建被依赖对象;而IoC是有专门一个容器来创建这些对象,即由Ioc容器来控制对象的创建;下面我使用Martin fowler关于控制反转一文中传统(也就是我们平时通过new来创建对象的过程)创建对象的例子来展示为啥要使用IoC,这个例子用于提供一份电影清单,清单上列出的影片都是由一位特定的导演执导的。这份清单我使用的是txt文件来保存,首先我先展示一些这份清单的内容,如下图,每个影片以分号来区分,而冒号前面代表的是导演名字,冒号后面代表的是影片的名称。
[Java][依赖注入和控制反转]_第1张图片
文件放在如下图的位置,文件名为movies1.txt
[Java][依赖注入和控制反转]_第2张图片
如下代码:
首先是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,如下图,
[Java][依赖注入和控制反转]_第3张图片

除此之外,客户端(Test类)与MovieLister也存在耦合,即客户端依赖MovieLister
[Java][依赖注入和控制反转]_第4张图片

从中这个例子我们知道依赖对象MovieLister控制了被依赖对象Finder的生成,这个例子很好的展示了“依赖对象里通过new来创建被依赖对象,是程序主动去创建被依赖对象”这句话。

2)反转什么

为什么要实现解耦?传统的方式(即直接在依赖对象的类里创建被依赖对象的方式)存在什么问题?
先回答第一个问题,为啥要解耦?是为了设计出一个容易维护,扩展性好的代码。下面通过例子说明高耦合会产生一些什么不好的后果。
[Java][依赖注入和控制反转]_第5张图片

如上图,假设图中使用的MovieLister类在很多地方都被调用了,并且都是通过new的方式来创建的,但是有一天,需求改变了,这个MovieLister已经不能满足需求了,假设无法通过修改MovieLister来满足需求或者项目里已经有一个类满足需求,只需要把MovieLister换成这个类即可。这种通过传统方式来创建对象的方法会造成以下问题:
(1)你需要将所有使用到MovieLister的地方都进行修改。
(2)或许你会说,每个地方都进行修改有什么问题?通过全局搜索关键之“MovieLister”,然后选中全部替换不上很简单的事吗?因为每个地方使用到MovieLister的逻辑都是不一样的,如果你贸然对如此多处地方进行修改,且不去捋清各自的逻辑,很有可能会存在隐患。
(3)假设项目里使用MovieLister的地方很少,只有10处,你捋清这10处地方的逻辑,但是,假设有100处呢?这是一个十分耗时的过程。维护起来十分困难。
因此为了实现低耦合,人们通过各种设计模式来实现低耦合,使得程序变得容易维护,拓展性好。在设计模式中,常说要基于接口开发,为啥要基于接口开发呢?使用上面的例子来解释一下:

[Java][依赖注入和控制反转]_第6张图片
从图中可以看出,MovieFinder是一个接口,而ColonDelimitedMovieFinder是这个接口的一个实现类。这样做有什么好处?通过这个方式,可以很好的减少要修改的代码,正如上面例子介绍的,如果对象的引用和和对象都是具体类,如果有多达10处地方使用到MovieFinder,那么你至少要修改至少20处地方(因为你要修改对象的引用和对象),如果通过接口的方式来声明,那么要修改的地方就会减少一半。虽然这样可以减轻一些工作量。但是这种操作还是远远不够的,它依然是有很高的耦合性。
那么IoC的优势在哪?先对比一下传统的new方式和IoC方式类图,
传统方式:
[Java][依赖注入和控制反转]_第7张图片

IoC(这里是通过设值注入方式来实现IoC的,类似与Spring的方法)
[Java][依赖注入和控制反转]_第8张图片

[Java][依赖注入和控制反转]_第9张图片

从这里可以看出,MovieLister和Finder的对象都是通过IoC容器来创建的,使用这种设计模式,假设你再碰到多个地方使用到Finder的,需求改变的时候,你只需要再这个xml文件里修改一下即可。
在此可以得出结论,IoC可以更好的实现低耦合,降低代码的维护量,增强代码的可拓展性。这种方式将依赖对象控制对象生成的操作,转交给IoC容器来控制生成,再由IoC容器转交给调用者来使用生成的对象。

3.什么是依赖注入

依赖注入(Dependency Injection,简称DI)。其实依赖注入是属于实现IoC设计模式的一个手段。组件之间依赖关系由容器在运行期决定,依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。
下面介绍Dependency Injection模式的几种不同形式。

1)构造函数注入(Constructor Injection)

这里使用名为PicoContainer的轻量级容器完成依赖注入。首先在项目里依赖picocontainer包
[Java][依赖注入和控制反转]_第10张图片

然后是创建获取指定导演电影的MovieLister类, PicoContainer通过构造函数来判断如何将MovieFinder实例注入MovieLister 类。因此,MovieLister类必须声明一个构造函数,并在其中包含所有需要注入的元素:

[Java][依赖注入和控制反转]_第11张图片

接着是查找所有电影的Finder类, MovieFinder实例本身也将由PicoContainer来管理,因此文本文件的名字也可以由容器注入:

[Java][依赖注入和控制反转]_第12张图片
[Java][依赖注入和控制反转]_第13张图片

随后,需要告诉PicoContainer:各个接口分别与哪个实现类关联、将哪个字符串注入MovieFinder组件。
[Java][依赖注入和控制反转]_第14张图片

结果如下:

可以看到,关于Finder和MovieLister的配置都在这个方法里进行了,当然,你可以把这个配置方法单独放到一个类里去。
[Java][依赖注入和控制反转]_第15张图片

2)设值方法注入(Setter Injection)

Spring 框架是一个用途广泛的企业级Java 开发框架,其中包括了针对事务、持久化框架、web应用开发和JDBC等常用功能的抽象。和PicoContainer一样,它也同时支持构造函数注入和设值方法注入,但该项目的开发者更推荐使用设值方法注入。
首先,先引入Spring包,这里我是通过Maven导入Spring-MVC,但是其实你只需要导入spring-context
[Java][依赖注入和控制反转]_第16张图片

为了让MovieLister类接受注入,我需要为它定义一个设值方法,该方法接受类型为MovieFinder的参数:
[Java][依赖注入和控制反转]_第17张图片

类似地,在MovieFinder的实现类中,我也定义了一个设值方法,接受类型为String 的参数:
[Java][依赖注入和控制反转]_第18张图片

然后是设定配置文件。Spring 支持多种配置方式,你可以通过XML 文件进行配置,也可以直接在代码中配置。不过,XML 文件是比较理想的配置方式。
[Java][依赖注入和控制反转]_第19张图片

[Java][依赖注入和控制反转]_第20张图片

最后测试代码如下:
[Java][依赖注入和控制反转]_第21张图片

输出结果和上一个例子一致。

3)接口注入(Interface Injection)

通过接口完成注入。Avalon框架就使用了类似的技术。
首先,我需要定义一个接口,组件的注入将通过这个接口进行。在本例中,这个接口的用途是将一个MovieFinder实例注入继承了该接口的对象。
[Java][依赖注入和控制反转]_第22张图片

这个接口应该由提供MovieFinder接口的人一并提供。任何想要使用MovieFinder实例的类(例如MovieLister类)都必须实现这个接口。
[Java][依赖注入和控制反转]_第23张图片

然后,我使用类似的方法将文件名注入MovieFinder的实现类:
[Java][依赖注入和控制反转]_第24张图片

[Java][依赖注入和控制反转]_第25张图片

现在,还需要用一些配置代码将所有的组件实现装配起来。简单起见,我直接在代码中完成配置,并将配置好的MovieLister 对象保存在名为lister的字段中:
[Java][依赖注入和控制反转]_第26张图片

4.参考文章

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

5.结语

上面贴的代码是完整的,花点时间整理一下就可以得到一个完整的代码了。对于不想花时间的同学,可以通过以下支付宝付款码支付1元并附上自己的邮箱
[Java][依赖注入和控制反转]_第27张图片
或者通过淘宝支付,支付的时候请附上自己的邮箱
https://item.taobao.com/item.htm?spm=a2oq0.12575281.0.0.264f1debTX35SJ&ft=t&id=602253362650
即可直接获取源码。

你可能感兴趣的:(java)