Skip to main content

Getter-based Dependency Injection

Posted by crazybob on May 26, 2004 at 2:41 PM PDT

Like service locators, dependency injection containers save you from implementing the same dynamic loading and configuration code over and over in different ways; you can configure your entire application consistently in one place. Unlike service locators, dependency injection containers don't require component dependence on a service locator API which facilitates sharing between teams that may have their own service locators.

Cedric and I chatted last week about his "useless methods" blog and the benefits of getter-based dependency injection over setter or constructor-based injection (a la pico container and Spring). Cedric compares CMP entity beans with theoretical getter-based containers which extend classes at runtime and override getter methods to return injected dependencies. The overridden getters may be bean-style (for easier testing) or abstract (for a cleaner, field free implementation). Such a framework would not require classes to declare setter methods, constructors or fields. Getter injection better enables the container to lazy load dependencies and simplifies injecting different dependencies of the same type.

I implemented a simple proof of concept using dynaop which you can find in the dynaop.aspects.ioc package in the aspects directory in CVS. The API consists of a single class Container. The Container.add() methods configure new components. For each component, you specify an implementation class, an optional ID, and optional constructor arguments. You can look up component instances from the container by type or ID with the Container.get() methods. The container overrides component instances' getter methods. The container first looks for a component with an ID that matches the getter method's name; for example, an abstract method getCustomerDataSource() returns an instance of a component with the ID "customerDataSource". If the container can't find a component with a matching ID, it next looks for the first component that implements the method's return type. Finally, if the container still can't find a match, it does not override the method.

I've expanded on and slightly modified the example from the pico container Two-minute Tutorial. The original example has two components, Boy and Girl. The Girl depends on the Kissable interface which the Boy implements:

  public interface Kissable {
    public void kiss();
  }

  public class Boy implements Kissable {
    boolean kissed;
    public boolean isKissed() {
      return kissed;
    }
    public void kiss() {
      kissed = true;
    }
  }

  public abstract class Girl {
    public abstract Kissable getKissable();
    public void kissEveryone() {
      getKissable().kiss();
    }
  }

The pico container example passes the Kissable implementation to the Girl instance via the constructor. Using constructor-based injection, sans a lazy-loading stub, the Kissable instance must be created before the Girl instance. Also, things can get hairy if the Girl depends on two different Kissable implementations at the same time. In our example above (modified to use getter-based injection), the container will implement the abstract Girl.getKissable() method. The container can wait to instantiate the Kissable instance until the girl actually uses it. We can differentiate between different implementation of the same interface by using different method names. To configure and use a Girl instance with the dynaop container:

  import dynaop.aspects.ioc.Container;
  ...

  // configure container
  Container c = new Container();
  c.add(Boy.class);
  c.add(Girl.class);
  ...

  // look up and use Girl instance
  Girl girl = (Girl) c.get(Girl.class);

  // this returns an instance of Boy
  Kissable kissable = girl.getKissable();

Using getter-based injection, we can easily modify Girl to depend on two different Kissable instances by adding a differently named method. Using the configuration in our running example, each getter method will return a different instance of Boy:

  public abstract class Girl {
    public abstract Kissable getKissable();
    public abstract Kissable getPrinceInTraining();
    public void kissEveryone() {
      getKissable().kiss();
      getPrinceInTraining().kiss();
    }
  }

Let's create a second implementation of Kissable:

  public class Frog implements Kissable {
    boolean turnedIntoPrince;
    public boolean isTurnedIntoPrince() {
      return turnedIntoPrince;
    }
    public void kiss() {
      turnedIntoPrince = true;
    }
  }

We can now configure Girl.getPrinceInTraining() to return our new Frog implementation instead of Boy without modifying our component or client code directly using a component ID "princeInTraining" which matches our new method name:

  Container c = new Container();
  c.add(Boy.class);
  c.add("princeInTraining", Frog.class);
  c.add(Girl.class);

Finally, we implement a JUnit test that verifies our assertions:

  import dynaop.aspects.ioc.Container;
  ...

  public void testInsatiableGirl() {
    // create and configure IoC container
    Container c = new Container();
    c.add(Boy.class);
    c.add("princeInTraining", Frog.class);
    c.add(Girl.class);

    // look up and invoke method on Girl instance
    Girl girl = (Girl) c.get(Girl.class);
    girl.kissEveryone();

    // test that getKissable() returns instance of Boy
    assertTrue(girl.getKissable() instanceof Boy);
    assertTrue(((Boy) girl.getKissable()).isKissed());

    // test that getPrinceInTraining() returns instance of Frog
    assertTrue(girl.getPrinceInTraining() instanceof Frog);
    assertTrue(((Frog) girl.getPrinceInTraining()).isTurnedIntoPrince());
  }

From a testability standpoint, we can use the container directly in our test as in the example above. We could also provide concrete getter methods in our component class which the container will override at runtime. A third option is to create a MockGirl class that extends Girl. Almost any IDE can automatically override the abstract methods with default implementations, and we can provide test-specific implementations as needed using anonymous inner classes.

In this particular situation, dynaop outshines both the OO and alternate AOP approaches in simplicity and performance. I used custom method pointcuts that pick methods based on the container configuration. The container determines at load time exactly which dependency resolution code if any to apply to methods. For example, the container only applies the interceptor that resolves dependencies by ID to methods whose name and return type correspond to a configured component.

You can apply your own custom aspects to the components by passing an Aspects instance to the container constructor. Future enhancements might include singleton-scoped components, container hierarchies, and XML and BeanShell configurations.