Inversion of Control
When these containers talk about how they are so useful because they implement "Inversion of Control" I end up very puzzled. Inversion of control is a common characteristic of frameworks, so saying that these lightweight containers are special because they use inversion of control is like saying my car is special because it has wheels.
The question, is what aspect of control are they inverting? When I first ran into inversion of control, it was in the main control of a user interface. Early user interfaces were controlled by the application program. You would have a sequence of commands like "Enter name", "enter address"; your program would drive the prompts and pick up a response to each one. With graphical (or even screen based) UIs the UI framework would contain this main loop and your program instead provided event handlers for the various fields on the screen. The main control of the program was inverted, moved away from you to the framework.
For this new breed of containers the inversion is about how they lookup a plugin implementation. In my naive example the lister looked up the finder implementation by directly instantiating it. This stops the finder from being a plugin. The approach that these containers use is to ensure that any user of a plugin follows some convention that allows a separate assembler module to inject the implementation into the lister.
As a result I think we need a more specific name for this pattern. Inversion of Control is too generic a term, and thus people find it confusing. As a result with a lot of discussion with various IoC advocates we settled on the name Dependency Injection.
I'm going to start by talking about the various forms of dependency injection, but I'll point out now that that's not the only way of removing the dependency from the application class to the plugin implementation. The other pattern you can use to do this is Service Locator, and I'll discuss that after I'm done with explaining Dependency Injection.
Forms of Dependency Injection
The basic idea of the Dependency Injection is to have a separate object, an assembler, that populates a field in the lister class with an appropriate implementation for the finder interface, resulting in a dependency diagram along the lines of Figure 2
Figure 2: The dependencies for a Dependency Injector
There are three main styles of dependency injection. The names I'm using for them are Constructor Injection, Setter Injection, and Interface Injection. If you read about this stuff in the current discussions about Inversion of Control you'll hear these referred to as type 1 IoC (interface injection), type 2 IoC (setter injection) and type 3 IoC (constructor injection). I find numeric names rather hard to remember, which is why I've used the names I have here.
依赖注射的形式
依赖注入最基本的思想就是用一个单独的对象,即装配器来获得Moviefinder接口的合适的实现,并将其实例赋给Movielister类的一个字段,图2展示的就是这样的一个依赖关系图。
一般有三种形式的依赖注入。我这里给他们分别命名为构造函数注入,设置方法注入和接口注入。假如你读过一些最近关于讨论控制反转的一些内容,你可能听说过这些材料所指的类型1,类型2和类型3。我发现数字名称相当难记,所以我这里使用了上述好记的名称。
Constructor Injection with PicoContainer
I'll start with showing how this injection is done using a lightweight container called PicoContainer. I'm starting here primarily because several of my colleagues at ThoughtWorks are very active in the development of PicoContainer (yes, it's a sort of corporate nepotism.)
PicoContainer uses a constructor to decide how to inject a finder implementation into the lister class. For this to work, the movie lister class needs to declare a constructor that includes everything it needs injected.
class MovieLister...
public MovieLister(MovieFinder finder) {
this.finder = finder;
}
The finder itself will also be managed by the pico container, and as such will have the filename of the text file injected into it by the container.
class ColonMovieFinder...
public ColonMovieFinder(String filename) {
this.filename = filename;
}
使用picoContainer进行构造函数注入
我下面将使用一种叫做picocontainer的轻量级容器来演示这种注入是如何完成的。这样做主要是因为我的几个在thoughtworks的同事在picocontainer开发方面非常
活跃,(没错,这也算是一种偏袒吧)。
picocontainer使用构造函数来决定如何向MovieLister类注入MovieFinder的实现,为了使picocontainer工作,Movie Lister类需要声明一个构造器,在其中需要包含它要注入
的所有东西。
class MovieLister...
public MovieLister(MovieFinder finder){
this.finder=finder;
}
Moviefinder本身也将由pico container来管理,同样,文本文件的文件名也将由容器来注入finder类。
The pico container then needs to be told which implementation class to associate with each interface, and which string to inject into the finder.
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; }
This configuration code is typically set up in a different class. For our example, each friend who uses my lister might write the appropriate configuration code in some setup class of their own. Of course it's common to hold this kind of configuration information in separate config files. You can write a class to read a config file and set up the container appropriately. Although PicoContainer doesn't contain this functionality itself, there is a closely related project called NanoContainer that provides the appropriate wrappers to allow you to have XML configuration files. Such a nano container will parse the XML and then configure an underlying pico container. The philosophy of the project is to separate the config file format from the underlying mechanism.
To use the container you write code something like this.
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());
}
Although in this example I've used constructor injection, PicoContainer also supports setter injection, although it's developers do prefer constructor injection.
MutablePicoContainer pico = new DefaultPicoContainer()
Parameter[] finderParams = {new ConstantParameter("movies1.txt")};
pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams);
pico.registerComponentImplementation(MovieLister.class);
return pico;
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());
}
Setter Injection with Spring
The Spring framework is a wide ranging framework for enterprise Java development. It includes abstraction layers for transactions, persistence frameworks, web application development and JDBC. Like PicoContainer it supports both constructor and setter injection, but its developers tend to prefer setter injection - which makes it an appropriate choice for this example.
To get my movie lister to accept the injection I define a setting method for that service
class MovieLister...
private MovieFinder finder;
public void setFinder(MovieFinder finder) {
this.finder = finder;
}
Similarly I define a setter for the filename.
class ColonMovieFinder...
public void setFilename(String filename) {
this.filename = filename; }
spring框架是一个被企业广泛使用的Java开发的框架。它包括了对事务处理、持久层框架、web应用和JDBC开发的抽象。就像picocontainer一样,他既支持构造器注入,也支持
也支持设置方法注入。但是他的开发者更喜欢设置方法注入-所以恰好适合这个例子。
为了让我的movie lister能够接受注入,我为它定义了一个设置方法,该设置方法接受MovieFinder为参数。
class MovieLister...private MovieFinder finder;public void setFinder(MovieFinder finder) {this.finder = finder;}
类似的,在MovieFinder中,我也定义了一个接受文件名为参数的设置方法。
class ColonMovieFinder...public void setFilename(String filename) {this.filename = filename;}
The third step is to set up the configuration for the files. Spring supports configuration through XML files and also through code, but XML is the expected way to do it.
<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>
The test then looks like this.
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()); }
第三步就是建立设置文件,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()); }
Interface Injection
The third injection technique is to define and use interfaces for the injection. Avalon is an example of a framework that uses this technique in places. I'll talk a bit more about that later, but in this case I'm going to use it with some simple sample code.
With this technique I begin by defining an interface that I'll use to perform the injection through. Here's the interface for injecting a movie finder into an object.
public interface InjectFinder {
void injectFinder(MovieFinder finder);
}
This interface would be defined by whoever provides the MovieFinder interface. It needs to be implemented by any class that wants to use a finder, such as the lister.
class MovieLister implements InjectFinder...
public void injectFinder(MovieFinder finder) {
this.finder = finder;
}
I use a similar approach to inject the filename into the finder implementation.
public interface InjectFinderFilename {
void injectFilename (String filename);
}
class ColonMovieFinder implements MovieFinder, InjectFinderFilename......
public void injectFilename(String filename) {
this.filename = filename;
}
接口注入
第三种注入技术就是定义和使用接口注入. Avalon就是某些地方使用了该种技术的一个框架例子,关于该种注入技术,稍后我要谈论得更多一点,但本例中我将通过一些
简单的代码来使用它。
使用这种技术时将首先定义一个接口,我将通过该接口执行注入。下面这个例子就是把Movie Finder实例注入了一个实现该接口的对象的例子。
public interface InjectFinder {void injectFinder(MovieFinder finder);}
上面这个接口将由提供MovieFinder接口的人来定义。任何使用movie finder的类都必须实现实现该接口,如以下movie lister.
class MovieLister implements InjectFinder...
public void injectFinder(MovieFinder finder) {
this.finder = finder;
}
我也使用了同样的方法把文件名注入movie finder的实现类。
public interface InjectFinderFilename {void injectFilename (String filename);}class ColonMovieFinder implements MovieFinder, InjectFinderFilename......public void injectFilename(String filename) {this.filename = filename;}
Then, as usual, I need some configuration code to wire up the implementations. For simplicity's sake I'll do it in code.
class Tester...
private Container container; private void configureContainer() {
container = new Container();
registerComponents();
registerInjectors();
container.start();
}
This configuration has two stages, registering components through lookup keys is pretty similar to the other examples.
class Tester...
private void registerComponents() {
container.registerComponent("MovieLister", MovieLister.class);
container.registerComponent("MovieFinder", ColonMovieFinder.class);
}
然后,就像通常所做的那样,我也需要一些设置代码来装配所有的实现。为简单起见,我直接在代码中完成配置。
class Tester...
private Container container; private void configureContainer() {
container = new Container();
registerComponents();
registerInjectors();
container.start(); }
这个设置分成注册部件和注册注入器两个步骤。通过查询关键字注册部件和其他例子的代码非常相似。
class Tester...
private void registerComponents() {
container.registerComponent("MovieLister", MovieLister.class);
container.registerComponent("MovieFinder", ColonMovieFinder.class);
}
A new step is to register the injectors that will inject the dependent components. Each injection interface needs some code to inject the dependent object. Here I do this by registering injector objects with the container. Each injector object implements the injector interface.
class Tester...
private void registerInjectors() {
container.registerInjector(InjectFinder.class, container.lookup("MovieFinder"));
//自己解释:通过这样的实现,再看下文ColorMovieFinder实现了Injecotor,Injector的实现其实是注册了依赖类(movie finder),因为每个依赖类都是调用原类
//(movie lister)来执行注入的,依赖类实现了Injector接口,传递了this到原类。
container.registerInjector(InjectFinderFilename.class, new FinderFilenameInjector());
}
public interface Injector {
public void inject(Object target);
}
下面一步就是注册注入器对象,该注入器对象将注入依赖的部件。每一个注入器接口需要一些代码来注入依赖的部件。这儿我通过向容器注册注入器对象来实现。每一个注入器
对象实现了injector接口。
class Tester... private void registerInjectors() {container.registerInjector(InjectFinder.class, container.lookup("MovieFinder"));container.registerInjector(InjectFinderFilename.class, new FinderFilenameInjector());}public interface Injector {public void inject(Object target); }
When the dependent is a class written for this container, it makes sense for the component to implement the injector interface itself, as I do here with the movie finder. For generic classes, such as the string, I use an inner class within the configuration code.
class ColonMovieFinder implements Injector......
public void inject(Object target) {
((InjectFinder) target).injectFinder(this);
}
class Tester...
public static class FinderFilenameInjector implements Injector {
public void inject(Object target) {
((InjectFinderFilename)target).injectFilename("movies1.txt");
}
}
当被注入的依赖部件本身是为容器而所写的一个类的时候,让该依赖部件本身实现injector接口更有意义,就像我这儿的movie finder,他本身实现了Injector接口.对于一般的类,
象string,在设置代码中我使用了内部类。
class ColonMovieFinder implements Injector......public void inject(Object target) {((InjectFinder) target).injectFinder(this);}class Tester... public static class FinderFilenameInjector implements Injector {public void inject(Object target) {((InjectFinderFilename)target).injectFilename("movies1.txt");}}
The tests then use the container.
class IfaceTester...
public void testIface() {
configureContainer();
MovieLister lister = (MovieLister)container.lookup("MovieLister");
Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}
The container uses the declared injection interfaces to figure out the dependencies and the injectors to inject the correct dependents. (The specific container implementation I did here isn't important to the technique, and I won't show it because you'd only laugh.)
public void testIface() {
configureContainer();
MovieLister lister = (MovieLister)container.lookup("MovieLister");
Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}