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.
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。我发现数字名称相当难记,所以我这里使用了上述好记的名称。
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; }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.
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; }
类似的,在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()); }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; }
上面这个接口将由提供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; }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); }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); }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"); } }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.)