Inversion of Control Containers and the Dependency Injection pattern扎翻译
原文地址:控制反转和依赖注入
参考1:Object Builder Application Block
参考2:深入理解依赖注入
总结
依赖注入是一个模式,一个模糊的概念,而控制反转是实现依赖注入的一种实现方法。
实现控制反转有三种方式:
- 构造器注入
- setter注入
- 接口注入
除了控制反转可以实现依赖注入外,还有服务定位器模式也可以解除一定的依赖性。
一个简单的例子
从这里开始翻译:
In this example I'm writing a component that provides a list of movies directed by a particular director. This stunningly useful function is implemented by a single method.
在这个例子中,我正在写一个提供由特定导演指导的电影列表组件,这个非常有用的功能是通过一个方法来实现的。
MovieLister.class
public MovieLister{
// ...
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()]);
}
}
这个函数的实现非常的幼稚,它需要一个finder
对象(我们很快就知道)返回它的每一场电影,然后只是遍历这个 list (allMovies
),筛选出由特定导演指导的电影。我不会去修补这个天真的代码,因为这是本文真正意义上的基础。
这篇文章的真正要点就是这个finder
对象,特别的说,我们如何将list
对象和特定的finder
对象连接起来。有趣的原因是,我希望moviesDirectedBy
方法可以和所有电影如何存储的方法完全独立,所以在所有情况下,方法都可以引用finder
对象,并且finder
对象也会知道如何返回findAll
方法,我可以定义一个finder
的接口来解决这个问题
public interface MovieFinder {
List findAll();
}
目前这些都很好的解耦,但是在某些时候,我需要想出一个具体的类来存放这些电影,在这种情况下,我把这些放到了我的lister构造器中。
MovieLister.class
public MovieLister() {
finder = new ColonDelimitedMovieFinder("movies1.txt");
}
总代码如下:
MovieLister.class类
public class MovieLister{
private MovieFinder finder;
public MovieLister(){
// ColonDelimitedMovieFinder 冒号分割的文本 获取movie列表
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()]);
}
}
MovieFinder.class类
public interfave MovieFinder{
List findAll();
}
实现类的名称来源于我从冒号分割的文本中获取我的列表,我会告诉你多余细节,毕竟只是一些实现。
现在,如果只是我自己而使用这个类,一切都很好很正常。但是,当我的朋友非常渴望拷贝一份我的代码的时候会发生什么事情呢?如果他们还把电影列表存储在movies1.txt
中,那么一切都很美妙。如果他们的存放电影文件的命名不同,那么把文件名称放到一个配置文件中,也很容易。但是,如果他们有一个完全不同的形式存储电影列表:SQL数据库,XML文件,web服务,或者其他格式的文件呢?这种情况下,我们需要一个不同的类来获取这些数据。因为现在我已经定义了一个MovieFinder
接口,这不会改变我的moviesDirectedBy
方法,但是我仍然需要其他的方法,来正确的实例化finder
接口
图1: 在列表类中使用简单创建的依赖关系
Figure 1 shows the dependencies for this situation. The MovieLister class is dependent on both the MovieFinder interface and upon the implementation. We would prefer it if it were only dependent on the interface, but then how do we make an instance to work with?
In my book P of EAA, we described this situation as a Plugin. The implementation class for the finder isn't linked into the program at compile time, since I don't know what my friends are going to use. Instead we want my lister to work with any implementation, and for that implementation to be plugged in at some later point, out of my hands. The problem is how can I make that link so that my lister class is ignorant of the implementation class, but can still talk to an instance to do its work.
Expanding this into a real system, we might have dozens of such services and components. In each case we can abstract our use of these components by talking to them through an interface (and using an adapter if the component isn't designed with an interface in mind). But if we wish to deploy this system in different ways, we need to use plugins to handle the interaction with these services so we can use different implementations in different deployments.
So the core problem is how do we assemble these plugins into an application? This is one of the main problems that this new breed of lightweight containers face, and universally they all do it using Inversion of Control.
图一显示了这种状况的依赖关系,MovieLister
类同时依赖于MovieFinder
接口和实现。我们更喜欢如果MovieLister
类只依赖MovieFinder
的接口,但是我们该怎么做呢?
在我的一本书中 P of EAA,我们把这种情况称为一个Plugin,finder
的实现类在编译时没有链接到程序中,因为我不知道我的朋友们将使用什么。相反,我希望我的lister
能够和任何MovieFinder
实现类一起工作,并且MovieFinder
实现类将会自动的插入到程序中,而不是通过我来掌控。问题是我应该怎样完成这些链接,以便我的lister
类对实现类是不知情的,但是它依然可以和finder
实例完成工作。
把这些扩展成一个真正的系统,或许我们需要很多这样的服务和组件。在每一种情况下,我们可以通过一个接口(如果组件没有设计接口的话,可以使用组件),来让我们的组件相互关联。但是如果我们希望通过不同的方式部署这个系统,我们需要使用插件来处理这些服务的交互,以便我们可以在不同的部署中使用不同的实现。
所以核心问题是我们如何将这些插件组装到应用文件之中?这也是一些轻量级容器所面临的主要问题之一,通常他们都是通过控制反转来实现的。
控制反转
当这些容器谈到他们如何有用,他们说是因为实现了“控制反转”时候,我感到诧异。控制反转是架构的一个典型特征,所以当这些轻量级框架说因使用了控制反转而特殊,这就像说我的车是特殊的因为它有车轮。
问题是:“他们反转的是哪些方面的控制?”,当我第一次遇到控制反转的时候,是一个主要由用户控制的界面。早期的用户界面是被应用程序所控制,你会有一系列如“输入名称”,”输入地址”的命令;你的程序将驱动提示并承载每一个响应。使用图形(甚至基于屏幕的)UI,UI框架将会包含这个主循环,并且,你的程序将会提供事件处理程序来代替屏幕上的各个区域。这个程序的主要控制权被颠倒了,从你转移到了框架。
因此,我认为我们需要一个更具体的名称来表示这种模式,控制反转(Inversion of Control)是一个过于笼统的术语,因此人们感到困惑。因此与很多IOC提倡者进行了大量讨论之后,我们决定使用Dependency Injection(DI 依赖注入)这个词。
我将开始讨论各种形式的依赖注入,但是我想指出的是,使用插件实现来去除依赖并不是应用程序类的唯一方法。你可以使用的另一种模式是服务定位器模式,在完成解释依赖注入之后,我会讨论这个问题。
依赖注入的形式
依赖注入的基本思想是有一个分隔的对象,一个装配器,这个装配器有适当的finder
接口实现来填充lister
类中的字段,从而得到图二的依赖关系图
图二:依赖注入的依赖关系
有三种主要方式的依赖注入,我目前叫他们构造函数注入,setter注入和接口注入。如果你在目前关于依赖注入的讨论中阅读过,你会听到这些被称之为type 1 IOC(接口注入),type 2 IOC(setter注入),type 3 IOC(构造器注入)的内容。我发现数字名字很难记住,这就是为啥我在这儿使用了我曾经叫的名字。
带有PicoContainer的构造器注入
我从展示如何使用轻量级框架picocontainer开始.我从这里开始的根本原因是我的几位曾在ThoughtWorks重要的同事,在PicoContainer 的开发中非常的活跃(没错,这是一种企业裙带关系)
PicoContainer使用构造器来决定如何将finder
实现类注入到MoveiLister
类之中。为了这个工作,MovieLister
类需要声明一个构造器,它包含了它需要注入的所有东西。
MovieLister.class
public class MoveiLister{
private MovieFinder finder;
public MoveiLister(MovieFinder finder){
this.finder = finder;
}
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()]);
}
}
finder
本身也会被picocontainer
容器所管理,同理,文本文件的文件名也会被注入到这个容器中
ColonMovieFinder.class
public class ColonMovieFinder{
private String filename;
public ColonMovieFinder(String filename){
this.filename = filename;
}
}
接下来picocontainer
容器需要被告知哪些实现类需要与接口相关联,以及哪些字符串需要被注入到MovieFinder
中
private MutablePicoContainer configureContainer() {
MutablePicoContainer pico = new DefaultPicoContainer();
Parameter[] finderParams = {new ConstantParameter("movies1.txt")};
pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams);
pico.registerComponentImplementation(MovieLister.class);
return pico;
}
这些配置代码通常设置在一个不同的类中。在我们的例子中,每一个使用我的lister
类的朋友可能会在某些启动类中编写他们自己的配置代码。当然,在一个单独的配置文件中编写配置信息是很常见的。你可以写一个类去读取配置文件并恰当的设置容器。虽然PicoContainer
本身不包含这个功能,但是有一个名为NanoContainer
的项目与之紧密相连,它提供了相关的包装,允许你使用xml配置文件。这样的一个纳米级容器,将会解析xml,并配置一个潜在的PicoContainer
,该项目的理念是将配置文件和底层机制分离。
要使用这个容器你可以这样写:
public void testWithPico() {
MutablePicoContainer pico = configureContainer();
MovieLister lister = (MovieLister) pico.getComponentInstance(MovieLister.class);
Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}
尽管在这个例子中,我使用了构造器注入,但是PicoContainer
同样支持setter注入,尽管它的开发者更喜欢构造器注入。
setter注入和spring
spring框架是企业级Java开发的广泛使用的框架,它包含事务抽象层,持久层框架,web应用程序开发和JDBC,像PicoContainer
框架一样,它同样支持构造器注入和setter注入,但是它的开发人员跟倾向于setter注入,这使得它成为这个例子的合适选择。
为了让MovieLister
接受注入,我在该类中定义了set方法
MovieLister.class
public class MovieLister{
private MovieFinder finder;
public MoveiLister(MovieFinder finder){
this.finder = finder;
}
public void setFinder(MovieFinder finder) {
this.finder = finder;
}
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()]);
}
}
同样的,我也为filename
定义了一个set方法
public class ColonMovieFinder{
private String filename;
public ColonMovieFinder(String filename){
this.filename = filename;
}
public void setFilename(String filename){
this.filename = filename;
}
}
第三步是设置文件的配置,spring支持通过XML文件进行配置,也支持代码配置,但XML文件是常见方式
<beans>
<bean id="MovieLister" class="spring.MovieLister">
<property name="finder">
<ref local="MovieFinder"/>
</property>
</bean>
<bean id="MovieFinder" class="spring.ColonMovieFinder">
<property name="filename">
<value>movies1.txt</value>
</property>
</bean>
</beans>
测试文件应该看起来是这样子的:
public void testWithSpring() throws Exception {
ApplicationContext ctx = new FileSystemXmlApplicationContext("spring.xml");
MovieLister lister = (MovieLister) ctx.getBean("MovieLister");
Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}
接口注入
第三种注入方式是接口注入,Avalon是在某些情况下使用这种方式的框架的例子,稍后我会进一步讨论,但是现在,我将使用一些简单的示例代码。