现在的位置: 首页 > 综合 > 正文

Martin Fowler的Inversion of Control Containers and the Dependency Injection pattern(中英文对照版)

2013年05月04日 ⁄ 综合 ⁄ 共 40180字 ⁄ 字号 评论关闭
文章目录

Inversion of Control Containers and the Dependency Injection pattern

In the Java community there's been a rush of lightweight containers that help to assemble components from different projects into a cohesive application. Underlying these containers is a common pattern to how they perform the wiring, a concept they refer under the very generic name of "Inversion of Control". In this article I dig into how this pattern works, under the more specific name of "Dependency Injection", and contrast it with the Service Locator alternative. The choice between them is less important than the principle of separating configuration from use.

Last significant update: 23 Jan 04

| Chinese | Portuguese | French |


One of the entertaining things about the enterprise Java world is the huge amount of activity in building alternatives to the mainstream J2EE technologies, much of it happening in open source. A lot of this is a reaction to the heavyweight complexity in the mainstream J2EE world, but much of it is also exploring alternatives and coming up with creative ideas. A common issue to deal with is how to wire together different elements: how do you fit together this web controller architecture with that database interface backing when they were built by different teams with little knowledge of each other.A number of frameworks have taken a stab at this problem, and several are branching out to provide a general capability to assemble components from different layers. These are often referred to as lightweight containers, examples include PicoContainer, and Spring.

Underlying these containers are a number of interesting design principles, things that go beyond both these specific containers and indeed the Java platform. Here I want to start exploring some of these principles. The examples I use are in Java, but like most of my writing the principles are equally applicable to other OO environments, particularly .NET.


Components and Services

The topic of wiring elements together drags me almost immediately into the knotty terminology problems that surround the terms service and component. You find long and contradictory articles on the definition of these things with ease. For my purposes here are my current uses of these overloaded terms.

I use component to mean a glob of software that's intended to be used, without change, by application that is out of the control of the writers of the component. By 'without change' I mean that the using application doesn't change the source code of the components, although they may alter the component's behavior by extending it in ways allowed by the component writers.

A service is similar to a component in that it's used by foreign applications. The main difference is that I expect a component to be used locally (think jar file, assembly, dll, or a source import). A service will be used remotely through some remote interface, either synchronous or asynchronous (eg web service, messaging system, RPC, or socket.)

I mostly use service in this article, but much of the same logic can be applied to local components too. Indeed often you need some kind of local component framework to easily access a remote service. But writing "component or service" is tiring to read and write, and services are much more fashionable at the moment.


A Naive Example

To help make all of this more concrete I'll use a running example to talk about all of this. Like all of my examples it's one of those super-simple examples; small enough to be unreal, but hopefully enough for you to visualize what's going on without falling into the bog of a real example.

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.

class 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()]);
    }

The implementation of this function is naive in the extreme, it asks a finder object (which we'll get to in a moment) to return every film it knows about. Then it just hunts through this list to return those directed by a particular director. This particular piece of naivety I'm not going to fix, since it's just the scaffolding for the real point of this article.

The real point of this article is this finder object, or particularly how we connect the lister object with a particular finder object. The reason why this is interesting is that I want my wonderful moviesDirectedBy method to be completely independent of how all the movies are being stored. So all the method does is refer to a finder, and all that finder does is know how to respond to the findAll method. I can bring this out by defining an interface for the finder.

public interface MovieFinder {
    List findAll();
}

Now all of this is very well decoupled, but at some point I have to come up with a concrete class to actually come up with the movies. In this case I put the code for this in the constructor of my lister class.

class MovieLister...
  private MovieFinder finder;
  public MovieLister() {
    finder = new ColonDelimitedMovieFinder("movies1.txt");
  }

The name of the implementation class comes from the fact that I'm getting my list from a colon delimited text file. I'll spare you the details, after all the point is just that there's some implementation.

Now if I'm using this class for just myself, this is all fine and dandy. But what happens when my friends are overwhelmed by a desire for this wonderful functionality and would like a copy of my program? If they also store their movie listings in a colon delimited text file called "movies1.txt" then everything is wonderful. If they have a different name for their movies file, then it's easy to put the name of the file in a properties file. But what if they have a completely different form of storing their movie listing: a SQL database, an XML file, a web service, or just another format of text file? In this case we need a different class to grab that data. Now because I've defined a MovieFinder interface, this won't alter my moviesDirectedBy method. But I still need to have some way to get an instance of the right finder implementation into place.

Figure 1

Figure 1: The dependencies using a simple creation in the lister class

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.


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

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.

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;
    }

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.

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 string the finder.

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());
    }

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;
    }

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);
  }

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"));
    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.)


Using a Service Locator

The key benefit of a Dependency Injector is that it removes the dependency that the MovieLister class has on the concrete MovieFinder implementation. This allows me to give listers to friends and for them to plug in a suitable implementation for their own environment. Injection isn't the only way to break this dependency, another is to use a service locator.

The basic idea behind a service locator is to have an object that knows how to get hold of all of the services that an application might need. So a service locator for this application would have a method that returns a movie finder when one is needed. Of course this just shifts the burden a tad, we still have to get the locator into the lister, resulting in the dependencies of Figure 3

Figure 3

Figure 3: The dependencies for a Service Locator

In this case I'll use the ServiceLocator as a singleton Registry. The lister can then use that to get the finder when it's instantiated.

class MovieLister...
    MovieFinder finder = ServiceLocator.movieFinder();
class ServiceLocator...
    public static MovieFinder movieFinder() {
        return soleInstance.movieFinder;
    }
    private static ServiceLocator soleInstance;
    private MovieFinder movieFinder;

As with the injection approach, we have to configure the service locator. Here I'm doing it in code, but it's not hard to use a mechanism that would read the appropriate data from a configuration file.

class Tester...
    private void configure() {
        ServiceLocator.load(new ServiceLocator(new ColonMovieFinder("movies1.txt")));
    }
class ServiceLocator...
    public static void load(ServiceLocator arg) {
        soleInstance = arg;
    }

    public ServiceLocator(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

Here's the test code.

class Tester...
    public void testSimple() {
        configure();
        MovieLister lister = new MovieLister();
        Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
        assertEquals("Once Upon a Time in the West", movies[0].getTitle());
    }

I've often heard the complaint that these kinds of service locators are a bad thing because they aren't testable because you can't substitute implementations for them. Certainly you can design them badly to get into this kind of trouble, but you don't have to. In this case the service locator instance is just a simple data holder. I can easily create the locator with test implementations of my services.

For a more sophisticated locator I can subclass service locator and pass that subclass into the registry's class variable. I can change the static methods to call a method on the instance rather accessing instance variables directly. I can provide thread specific locators by using thread specific storage. All of this can be done without changing clients of service locator.

A way to think of this is that service locator is a registry not a singleton. A singleton provides a simple way of implementing a registry, but that implementation decision is easily changed.

Using a Segregated Interface for the Locator

One of the issues with the simple approach above, is that the MovieLister is dependent on the full service locator class, even though it only uses one service. We can reduce this by using a segregated interface. That way, instead of using the full service locator interface, the lister can declare just the bit of interface it needs.

In this situation the provider of the lister would also provide a locator interface which it needs to get hold of the finder.

public interface MovieFinderLocator {
    public MovieFinder movieFinder();

The locator then needs to implement this interface to provide access to a finder.

    MovieFinderLocator locator = ServiceLocator.locator();
    MovieFinder finder = locator.movieFinder();
   public static ServiceLocator locator() {
        return soleInstance;
    }
    public MovieFinder movieFinder() {
        return movieFinder;
    }
    private static ServiceLocator soleInstance;
    private MovieFinder movieFinder;

You'll notice that since we want to use an interface, we can't just access the services through static methods any more. We have to use the class to get a locator instance and then use that to get what we need.

A Dynamic Service Locator

The above example was static, in that the service locator class has methods for each of the services that you need. This isn't the only way of doing it, you can also make a dynamic service locator that allows you to stash any service you need into it and make your choices at runtime.

In this case, the service locator uses a map instead of fields for each of the services, and provides generic methods to get and load services.

class ServiceLocator...
    private static ServiceLocator soleInstance;
    public static void load(ServiceLocator arg) {
        soleInstance = arg;
    }
    private Map services = new HashMap();
    public static Object getService(String key){
        return soleInstance.services.get(key);
    }
    public void loadService (String key, Object service) {
        services.put(key, service);
    }

Configuring involves loading a service with an appropriate key.

class Tester...
    private void configure() {
        ServiceLocator locator = new ServiceLocator();
        locator.loadService("MovieFinder", new ColonMovieFinder("movies1.txt"));
        ServiceLocator.load(locator);
    }

I use the service by using the same key string.

class MovieLister...
    MovieFinder finder = (MovieFinder) ServiceLocator.getService("MovieFinder");

On the whole I dislike this approach. Although it's certainly flexible, it's not very explicit. The only way I can find out how to reach a service is through textual keys. I prefer explicit methods because it's easier to find where they are by looking at the interface definitions.

Using both a locator and injection with Avalon

Dependency injection and a service locator aren't necessarily mutually exclusive concepts. A good example of using both together is the Avalon framework. Avalon uses a service locator, but uses injection to tell components where to find the locator.

Berin Loritsch sent me this simple version of my running example using Avalon.

public class MyMovieLister implements MovieLister, Serviceable {
    private MovieFinder finder;

    public void service( ServiceManager manager ) throws ServiceException {
        finder = (MovieFinder)manager.lookup("finder");
    } 
      

The service method is an example of interface injection, allowing the container to inject a service manager into MyMovieLister. The service manager is an example of a service locator. In this example the lister doesn't store the manager in a field, instead it immediately uses it to lookup the finder, which it does store.


Deciding which option to use

So far I've concentrated on explaining how I see these patterns and their variations. Now I can start talking about their pros and cons to help figure out which ones to use and when.

Service Locator vs Dependency Injection

The fundamental choice is between Service Locator and Dependency Injection. The first point is that both implementations provide the fundamental decoupling that's missing in the naive example - in both cases application code is independent of the concrete implementation of the service interface. The important difference between the two patterns is about how that implementation is provided to the application class. With service locator the application class asks for it explicitly by a message to the locator. With injection there is no explicit request, the service appears in the application class - hence the inversion of control.

Inversion of control is a common feature of frameworks, but it's something that comes at a price. It tends to be hard to understand and leads to problems when you are trying to debug. So on the whole I prefer to avoid it unless I need it. This isn't to say it's a bad thing, just that I think it needs to justify itself over the more straightforward alternative.

The key difference is that with a Service Locator every user of a service has a dependency to the locator. The locator can hide dependencies to other implementations, but you do need to see the locator. So the decision between locator and injector depends on whether that dependency is a problem.

Using dependency injection can help make it easier to see what the component dependencies are. With dependency injector you can just look at the injection mechanism, such as the constructor, and see the dependencies. With the service locator you have to search the source code for calls to the locator. Modern IDEs with a find references feature make this easier, but it's still not as easy as looking at the constructor or setting methods.

A lot of this depends on the nature of the user of the service. If you are building an application with various classes that use a service, then a dependency from the application classes to the locator isn't a big deal. In my example of giving a Movie Lister to my friends, then using a service locator works quite well. All they need to do is to configure the locator to hook in the right service implementations, either through some configuration code or through a configuration file. In this kind of scenario I don't see the injector's inversion as providing anything compelling.

The difference comes if the lister is a component that I'm providing to an application that other people are writing. In this case I don't know much about the APIs of the service locators that my customers are going to use. Each customer might have their own incompatible service locators. I can get around around some of this by using the segregated interface. Each customer can write an adapter that matches my interface to their locator, but in any case I still need to see the first locator to lookup my specific interface. And once the adapter appears then the simplicity of the direct connection to a locator is beginning to slip.

Since with an injector you don't have a dependency from a component to the injector, the component cannot obtain further services from the injector once it's been configured.

A common reason people give for preferring dependency injection is that it makes testing easier. The point here is that to do testing, you need to easily replace real service implementations with stubs or mocks. However there is really no difference here between dependency injection and service locator: both are very amenable to stubbing. I suspect this observation comes from projects where people don't make the effort to ensure that their service locator can be easily substituted. This is where continual testing helps, if you can't easily stub services for testing, then this implies a serious problem with your design.

Of course the testing problem is exacerbated by component environments that are very intrusive, such as Java's EJB framework. My view is that these kinds of frameworks should minimize their impact upon application code, and particularly should not do things that slow down the edit-execute cycle. Using plugins to substitute heavyweight components does a lot help this process, which is vital for practices such as Test Driven Development.

So the primary issue is for people who are writing code that expects to be used in applications outside of the control of the writer. In these cases even a minimal assumption about a Service Locator is a problem.

Constructor versus Setter Injection

For service combination, you always have to have some convention in order to wire things together. The advantage of injection is primarily that it requires very simple conventions - at least for the constructor and setter injections. You don't have to do anything odd in your component and it's fairly straightforward for an injector to get everything configured.

Interface injection is more invasive since you have to write a lot of interfaces to get things all sorted out. For a small set of interfaces required by the container, such as in Avalon's approach, this isn't too bad. But it's a lot of work for assembling components and dependencies, which is why the current crop of lightweight containers go with setter and constructor injection.

The choice between setter and constructor injection is interesting as it mirrors a more general issue with object-oriented programming - should you fill fields in a constructor or with setters.

My long running default with objects is as much as possible, to create valid objects at construction time. This advice goes back to Kent Beck's Smalltalk Best Practice Patterns: Constructor Method and Constructor Parameter Method. Constructors with parameters give you a clear statement of what it means to create a valid object in an obvious place. If there's more than one way to do it, create multiple constructors that show the different combinations.

Another advantage with constructor initialization is that it allows you to clearly hide any fields that are immutable by simply not providing a setter. I think this is important - if something shouldn't change then the lack of a setter communicates this very well. If you use setters for initialization, then this can become a pain. (Indeed in these situations I prefer to avoid the usual setting convention, I'd prefer a method like initFoo, to stress that it's something you should only do at birth.)

But with any situation there are exceptions. If you have a lot of constructor parameters things can look messy, particularly in languages without keyword parameters. It's true that a long constructor is often a sign of an over-busy object that should be split, but there are cases when that's what you need.

If you have multiple ways to construct a valid object, it can be hard to show this through constructors, since constructors can only vary on the number and type of parameters. This is when Factory Methods come into play, these can use a combination of private constructors and setters to implement their work. The problem with classic Factory Methods for components assembly is that they are usually seen as static methods, and you can't have those on interfaces. You can make a factory class, but then that just becomes another service instance. A factory service is often a good tactic, but you still have to instantiate the factory using one of the techniques here.

Constructors also suffer if you have simple parameters such as strings. With setter injection you can give each setter a name to indicate what the string is supposed to do. With constructors you are just relying on the position, which is harder to follow.

If you have multiple constructors and inheritance, then things can get particularly awkward. In order to initialize everything you have to provide constructors to forward to each superclass constructor, while also adding you own arguments. This can lead to an even bigger explosion of constructors.

Despite the disadvantages my preference is to start with constructor injection, but be ready to switch to setter injection as soon as the problems I've outlined above start to become a problem.

This issue has led to a lot of debate between the various teams who provide dependency injectors as part of their frameworks. However it seems that most people who build these frameworks have realized that it's important to support both mechanisms, even if there's a preference for one of them.

Code or configuration files

A separate but often conflated issue is whether to use configuration files or code on an API to wire up services. For most applications that are likely to be deployed in many places, a separate configuration file usually makes most sense. Almost all the time this will be an XML file, and this makes sense. However there are cases where it's easier to use program code to do the assembly. One case is where you have a simple application that's not got a lot of deployment variation. In this case a bit of code can be clearer than separate XML file.

A contrasting case is where the assembly is quite complex, involving conditional steps. Once you start getting close to programming language then XML starts breaking down and it's better to use a real language that has all the syntax to write a clear program. You then write a builder class that does the assembly. If you have distinct builder scenarios you can provide several builder classes and use a simple configuration file to select between them.

I often think that people are over-eager to define configuration files. Often a programming language makes a straightforward and powerful configuration mechanism. Modern languages can easily compile small assemblers that can be used to assemble plugins for larger systems. If compilation is a pain, then there are scripting languages that can work well also.

It's often said that configuration files shouldn't use a programing language because they need to be edited by non-programmers. But how often is this the case? Do people really expect non-programmers to alter the transaction isolation levels of complex server-side application? Non-language configuration files work well only to the extent they are simple. If they become complex then it's time to think about using a proper programming language.

One thing we're seeing in the Java world at the moment is a cacophony of configuration files, where every component has its own configuration files which are different to everyone else's. If you use a dozen of these components, you can easily end up with a dozen configuration files to keep in sync.

My advice here is to always provide a way to do all configuration easily with a programmatic interface, and then treat a separate configuration file as an optional feature. You can easily build configuration file handling to use the programmatic interface. If you are writing a component you then leave it up to your user whether to use the programmatic interface, your configuration file format, or to write their own custom configuration file format and tie it into the programmatic interface

Separating Configuration from Use

The important issue in all of this is to ensure that the configuration of services is separated from their use. Indeed this is a fundamental design principle that sits with the separation of interfaces from implementation. It's something we see within an object-oriented program when conditional logic decides which class to instantiate, and then future evaluations of that conditional are done through polymorphism rather than through duplicated conditional code.

If this separation is useful within a single code base, it's especially vital when you're using foreign elements such as components and services. The first question is whether you wish to defer the choice of implementation class to particular deployments. If so you need to use some implementation of plugin. Once you are using plugins then it's essential that the assembly of the plugins is done separately from the rest of the application so that you can substitute different configurations easily for different deployments. How you achieve this is secondary. This configuration mechanism can either configure a service locator, or use injection to configure objects directly.


Some further issues

In this article, I've concentrated on the basic issues of service configuration using Dependency Injection and Service Locator. There are some more topics that play into this which also deserve attention, but I haven't had time yet to dig into. In particular there is the issue of life-cycle behavior. Some components have distinct life-cycle events: stop and starts for instance. Another issue is the growing interest in using aspect oriented ideas with these containers. Although I haven't considered this material in the article at the moment, I do hope to write more about this either by extending this article or by writing another.

You can find out a lot more about these ideas by looking at the web sites devoted to the lightweight containers. Surfing from the picocontainer and spring web sites will lead to you into much more discussion of these issues and a start on some of the further issues.


Concluding Thoughts

The current rush of lightweight containers all have a common underlying pattern to how they do service assembly - the dependency injector pattern. Dependency Injection is a useful alternative to Service Locator. When building application classes the two are roughly equivalent, but I think Service Locator has a slight edge due to its more straightforward behavior. However if you are building classes to used in multiple applications then Dependency Injection is a better choice.

If you use Dependency Injection there are a number of styles to choose between. I would suggest you follow constructor injection unless you run into into one of the specific problems with that approach, in which case switch to setter injection. If you are choosing to build or obtain a container, look for one that supports both constructor and setter injection.

The choice between Service Locator and Dependency Injection is less important than the principle of separating service configuration from the use of services within an application.



Acknowledgments

My sincere thanks to the many people who've helped me with this article. Rod Johnson, Paul Hammant, Joe Walnes, Aslak Helles?on Tirs鮠and Bill Caputo helped me get to grips with these concepts and commented on the early drafts of this article. Berin Loritsch and Hamilton Verissimo de Oliveira provided some very helpful advice on how Avalon fits in. Dave W Smith persisted in asking questions about my initial interface injection configuration code and thus made me confront the fact that it was stupid.

Significant Revisions

23 Jan 04: Redid the configuration code of the interface injection example.

16 Jan 04: Added a short example of both locator and injection with Avalon.

14 Jan 04: First Publication


martinfowler.com logo thoughtworks logo

© Copyright Martin Fowler, all rights reserved

 

 

中文版 

 

IoC 容器和Dependency Injection 模式
撰文/Martin Fowler 编译/透明
Java 社群近来掀起了一阵轻量级容器的热潮,这些容器能够帮助开发者将来自不同项目的组件
组装成为一个内聚的应用程序。在它们的背后有着同一个模式,这个模式决定了这些容器进行组
件装配的方式。人们用一个大而化之的名字来称呼这个模式:“控制反转”( Inversion of
Control,IoC)。在本文中,我将深入探索这个模式的工作原理,给它一个更能描述其特点的名
字——“依赖注入”(Dependency Injection),并将其与“服务定位器”(Service Locator)
模式作一个比较。不过,这两者之间的差异并不太重要,更重要的是:应该将组件的配置与使用
分离开——两个模式的目标都是这个。
在企业级Java 的世界里存在一个有趣的现象:有很多人投入很多精力来研究主流J2EE 技术的
替代品——自然,这大多发生在open source 社群。在很大程度上,这可以看作是开发者对主
流J2EE 技术的笨重和复杂作出的回应,但其中的确有很多极富创意的想法,的确提供了一些可
供选择的方案。J2EE 开发者常遇到的一个问题就是如何组装不同的程序元素:如果web 控制
器体系结构和数据库接口是由不同的团队所开发的,彼此几乎一无所知,你应该如何让它们配合
工作?很多框架尝试过解决这个问题,有几个框架索性朝这个方向发展,提供了更通用的“组装
各层组件”的方案。这样的框架通常被称为“轻量级容器”,PicoContainer 和Spring 都在此
列中。
在这些容器背后,一些有趣的设计原则发挥着作用。这些原则已经超越了特定容器的范畴,甚至
已经超越了Java 平台的范畴。在本文中,我就要初步揭示这些原则。我使用的范例是Java 代
码,但正如我的大多数文章一样,这些原则也同样适用于别的OO 环境,特别是.NET。
组件和服务
“装配程序元素”,这样的话题立即将我拖进了一个棘手的术语问题:如何区分“服务”(service)
和“组件”(component)?你可以毫不费力地找出关于这两个词定义的长篇大论,各种彼此
矛盾的定义会让你感受到我所处的窘境。有鉴于此,对于这两个遭到了严重滥用的词汇,我将首
先说明它们在本文中的用法。
所谓“组件”是指这样一个软件单元:它将被作者无法控制的其他应用程序使用,但后者不能对
组件进行修改。也就是说,使用一个组件的应用程序不能修改组件的源代码,但可以通过作者预
留的某种途径对其进行扩展,以改变组件的行为。
服务和组件有某种相似之处:它们都将被外部的应用程序使用。在我看来,两者之间最大的差异
在于:组件是在本地使用的(例如JAR 文件、程序集、DLL、或者源码导入);而服务是要通过
——同步或异步的——远程接口来远程使用的(例如web service、消息系统、RPC,或者
socket)。
在本文中,我将主要使用“服务”这个词,但文中的大多数逻辑也同样适用于本地组件。实际上,
为了方便地访问远程服务,你往往需要某种本地组件框架。不过,“组件或者服务”这样一个词
组实在太麻烦了,而且“服务”这个词当下也很流行,所以本文将用“服务”指代这两者。
一个简单的例子
为了更好地说明问题,我要引入一个例子。和我以前用的所有例子一样,这是一个超级简单的例
子:它非常小,小得有点不够真实,但足以帮助你看清其中的道理,而不至于陷入真实例子的泥
潭中无法自拔。
在这个例子中,我编写了一个组件,用于提供一份电影清单,清单上列出的影片都是由一位特定
的导演执导的。实现这个伟大的功能只需要一个方法:
class 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()]);
}
你可以看到,这个功能的实现极其简单:moviesDirectedBy 方法首先请求finder(影片搜寻
者)对象(我们稍后会谈到这个对象)返回后者所知道的所有影片,然后遍历finder 对象返回
的清单,并返回其中由特定的某个导演执导的影片。非常简单,不过不必担心,这只是整个例子
的脚手架罢了。
我们真正想要考察的是finder对象,或者说,如何将MovieLister对象与特定的finder对象连
接起来。为什么我们对这个问题特别感兴趣?因为我希望上面这个漂亮的moviesDirectedBy
方法完全不依赖于影片的实际存储方式。所以,这个方法只能引用一个finder对象,而finder
对象则必须知道如何对findAll 方法作出回应。为了帮助读者更清楚地理解,我给finder定义了
一个接口:
public interface MovieFinder {
List findAll();
}
现在,两个对象之间没有什么耦合关系。但是,当我要实际寻找影片时,就必须涉及到
MovieFinder 的某个具体子类。在这里,我把“涉及具体子类”的代码放在MovieLister 类的
构造子中。
class MovieLister...
private MovieFinder finder;
public MovieLister() {
finder = new ColonDelimitedMovieFinder("movies1.txt");
}
这个实现类的名字就说明:我将要从一个逗号分隔的文本文件中获得影片列表。你不必操心具体
的实现细节,只要设想这样一个实现类就可以了。
如果这个类只由我自己使用,一切都没问题。但是,如果我的朋友叹服于这个精彩的功能,也想
使用我的程序,那又会怎么样呢?如果他们也把影片清单保存在一个逗号分隔的文本文件中,并
且也把这个文件命名为“ movie1.txt ”,那么一切还是没问题。如果他们只是给这个文件改改名,
我也可以从一个配置文件获得文件名,这也很容易。但是,如果他们用完全不同的方式——例如
SQL 数据库、XML 文件、web service,或者另一种格式的文本文件——来存储影片清单呢?
在这种情况下,我们需要用另一个类来获取数据。由于已经定义了MovieFinder接口,我可以
不用修改moviesDirectedBy 方法。但是,我仍然需要通过某种途径获得合适的MovieFinder
实现类的实例。
图1:“在MovieLister 类中直接创建MovieFinder 实例”时的依赖关系
图1 展现了这种情况下的依赖关系:MovieLister 类既依赖于MovieFinder接口,也依赖于具
体的实现类。我们当然希望MovieLister 类只依赖于接口,但我们要如何获得一个MovieFinder
子类的实例呢?
在Patterns of Enterprise Application Architecture 一书中,我们把这种情况称为“插件”
(plugin):MovieFinder的实现类不是在编译期连入程序之中的,因为我并不知道我的朋友会
使用哪个实现类。我们希望MovieLister 类能够与MovieFinder的任何实现类配合工作,并且
允许在运行期插入具体的实现类,插入动作完全脱离我(原作者)的控制。这里的问题就是:如
何设计这个连接过程,使MovieLister 类在不知道实现类细节的前提下与其实例协同工作。
将这个例子推而广之,在一个真实的系统中,我们可能有数十个服务和组件。在任何时候,我们
总可以对使用组件的情形加以抽象,通过接口与具体的组件交流(如果组件并没有设计一个接口,
也可以通过适配器与之交流)。但是,如果我们希望以不同的方式部署这个系统,就需要用插件
机制来处理服务之间的交互过程,这样我们才可能在不同的部署方案中使用不同的实现。
所以,现在的核心问题就是:如何将这些插件组合成一个应用程序?这正是新生的轻量级容器所
面临的主要问题,而它们解决这个问题的手段无一例外地是控制反转(Inversion of Control)
模式。
控制反转
几位轻量级容器的作者曾骄傲地对我说:这些容器非常有用,因为它们实现了“控制反转”。这
样的说辞让我深感迷惑:控制反转是框架所共有的特征,如果仅仅因为使用了控制反转就认为这
些轻量级容器与众不同,就好象在说“我的轿车是与众不同的,因为它有四个轮子”。
问题的关键在于:它们反转了哪方面的控制?我第一次接触到的控制反转针对的是用户界面的主
控权。早期的用户界面是完全由应用程序来控制的,你预先设计一系列命令,例如“输入姓名”、
“输入地址”等,应用程序逐条输出提示信息,并取回用户的响应。而在图形用户界面环境下,
UI 框架将负责执行一个主循环,你的应用程序只需为屏幕的各个区域提供事件处理函数即可。
在这里,程序的主控权发生了反转:从应用程序移到了框架。
对于这些新生的容器,它们反转的是“如何定位插件的具体实现”。在前面那个简单的例子中,
MovieLister 类负责定位MovieFinder 的具体实现——它直接实例化后者的一个子类。这样一
来,MovieFinder 也就不成其为一个插件了,因为它并不是在运行期插入应用程序中的。而这
些轻量级容器则使用了更为灵活的办法,只要插件遵循一定的规则,一个独立的组装模块就能够
将插件的具体实现“注射”到应用程序中。
因此,我想我们需要给这个模式起一个更能说明其特点的名字——“控制反转”这个名字太泛了,
常常让人有些迷惑。与多位IoC 爱好者讨论之后,我们决定将这个模式叫做“依赖注入”
(Dependency Injection)。
下面,我将开始介绍Dependency Injection 模式的几种不同形式。不过,在此之前,我要首
先指出:要消除应用程序对插件实现的依赖,依赖注入并不是唯一的选择,你也可以用Service
Locator 模式获得同样的效果。介绍完Dependency Injection 模式之后,我也会谈到Service
Locator 模式。
依赖注入的几种形式
Dependency Injection 模式的基本思想是:用一个单独的对象(装配器)来获得MovieFinder
的一个合适的实现,并将其实例赋给MovieLister 类的一个字段。这样一来,我们就得到了图2
所示的依赖图:
图2:引入依赖注入器之后的依赖关系
依赖注入的形式主要有三种,我分别将它们叫做构造子注入(Constructor Injection)、设值
方法注入(Setter Injection)和接口注入(Interface Injection)。如果读过最近关于IoC 的
一些讨论材料,你不难看出:这三种注入形式分别就是type 1 IoC(接口注入)、type 2 IoC
(设值方法注入)和type 3 IoC(构造子注入)。我发现数字编号往往比较难记,所以我使用了
这里的命名方式。
使用PicoContainer 进行构造子注入
首先,我要向读者展示如何用一个名为PicoContainer 的轻量级容器完成依赖注入。之所以从
这里开始,主要是因为我在ThoughtWorks 公司的几个同事在PicoContainer 的开发社群中
非常活跃——没错,也可以说是某种偏袒吧。
PicoContainer 通过构造子来判断“如何将MovieFinder 实例注入MovieLister 类”。因此,
MovieLister 类必须声明一个构造子,并在其中包含所有需要注入的元素:
class MovieLister...
public MovieLister(MovieFinder finder) {
this.finder = finder;
}
MovieFinder 实例本身也将由PicoContainer来管理,因此文本文件的名字也可以由容器注入:
class ColonMovieFinder...
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;
}
这段配置代码通常位于另一个类。对于我们这个例子,使用我的MovieLister 类的朋友需要在
自己的设置类中编写合适的配置代码。当然,还可以将这些配置信息放在一个单独的配置文件中,
这也是一种常见的做法。你可以编写一个类来读取配置文件,然后对容器进行合适的设置。尽管
PicoContainer 本身并不包含这项功能,但另一个与它关系紧密的项目NanoContainer 提供
了一些包装,允许开发者使用XML 配置文件保存配置信息。NanoContainer能够解析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());
}
尽管在这

抱歉!评论已关闭.