Cameron Hotchkies

Categories

  • Coding

Tags

  • dependency-injection
  • development

This goes out to all the professional software developers out there working with time constraints just trying to get their job done. Dependency injection (DI) is one of those kinds of concepts that fall through the cracks and becomes assumed knowledge at some point. At a high level it means anything your classes depend on are passed in (usually via a constructor) and it helps make more testable code. Does that make sense? No. not even a little bit. That explanation is terrible.

If you are working in a project that has some level of DI going on (commonly Dagger or Guice in the JVM world) but are anywhere from slightly apprehensive to downright fearful of it, this will help take the edge off. Get it? Edges.. because there’s graphs.. wait, what?

What is Dependency Injection?

A pragmatic way to look at dependency injection is that you have two groups of classes in your programs. Things that depend on resources and things that do not. What’s a resource? Here’s a non-exhaustive set of examples:

  • databases
  • configuration files
  • external caches
  • a third party API

Why does that matter? When you test your code, you can have overly, and unnecessarily complex integration test setups, or you can pass in a class that represents the database. A class that responds the way the test needs it to to validate the specific condition it is testing. A different class allows you to run tests without ever hitting the internet or relying on some sketchy docker image that clones your external service.

Singletons are resources

The other big advantage? Object lifecycle when it comes to singletons. If you see tests that are littered with test-only methods that clear caches or reset long lived data structures, that means the code is not taking advantage of DI for rebuilding an object graph.

A singleton object is a resource like any of the previously mentioned ones, except it’s just managed internally in the same process as your program. They tend to either guard other resources or retain global data the same way an external cache layer would.

Singletons are also the main way developers use DI and then stop. Since the lifecycle of the singleton is managed by the DI framework, it tends to be a top level class when the program starts and can be set up without delving too deep into how the rest of DI stuff is intended to work.

What is the object graph?

At the most simple level, one could achieve dependency injection by passing an initialized database connection pool from the first function down through every other function until it gets to the lowest one on the call graph that needs it. Doing this for every resource possibly needed is brutal, and would result in function calls with a ridiculous amount of parameters. This is what a DI Framework (like Dagger) does for you behind the scenes. It silently makes this available to the classes that need it, and let you skip passing it for the ones that do not.

This requires a bit of a change in how you instantiate objects in general.

For example, let’s assume you had a Java program that would normally create an instance of a Frobulator, and that Frobulator internally requires a Frob (obviously, what else would require frobulation?)

public class Frobulator {
  private final frob;

  public Frobulator() {
    this.frob = new Frob();
  }

  public void start() {
    frob.frobulate();
  }
}

With dependency injection the same class could look like this:

public class Frobulator {

  private final Frob frob;

  @Inject
  public Frobulator(Frob frob) {
    this.frob = frob;
  }

  public void start() {
    frob.frobulate();
  }
}

The common terminology for the difference in classes is Newable for classes instantiated the classic way and Injectable for classes instantiated via DI.

The object graph is the construction of all the dependencies required by the program and ensuring that they are defined somewhere enough for the program to function at runtime.

Providers

The previous example assumes that Frob either has an empty constructor or another class that functions as a Provider.

A Provider (commonly grouped inside a class with the name Module) can really just be thought of as an external constructor. The benefit of this external constructor is that it can return subclasses and do some preprocessing on the objects being instantiated.

For example, if our program needs to frobulate, the implementation can provide a very specific instance. This example defines Ore and Heater inline, but could do so in separate files.

public static class Ore {
  @Inject
  Ore() {}
}

public static class Heater {
  @Inject
  Heater() {
    // Initialization here
  }
}
public class MetalModule {
  public Frob frob(Ore ore, Heater heater) {
    return new MetalFrob(ore, heater);
  }
}

Since Ore and Heater are not doing anything tricky with construction, using the @Inject annotation on the constructor will create an implicit provider to save us some boilerplate code.

If the only dependencies for a MetalFrob are also injectables, we could even rewrite the MetalModule as follows:

public class MetalModule {
  public Frob frob(MetalFrob metalFrob) {
    return metalFrob;
  }
}

The benefit for testing here is that now when a Frob is needed in our tests, we can replace it with a slightly different object by creating a secondary module:

public class CrackingMetalModule {
  public static class CrackedFrob {
    public frobulate() {
      throw new RuntimeException("The frob cracked during frobulation");
    }
  }

  public Frob frob() {
    return new CrackedFrob();
  }
}

By merging this object graph with the main one used by the software, we can get more touch points into the software, with more direct control over how simulated resources respond.

At the end of the day, that’s really all a Module is. A definition of how to instantiate interfaces or how to override the construction of injected objects.

More than one instance

One way this tends to break down in practice is when we need to instantiate on demand as opposed to only during construction.

Take this newable class that has a startFresh() method that will be called several times:

public class FreshFrobulator {
  public void startFresh() {
    Frob frob = new Frob();
    frob.frobulate();
  }
}

This runs into a problem that our previous example can’t solve. Luckily, the Provider comes to the rescue again.

public class FreshFrobulator {
  final Provider<Frob> frobProvider;

  public FreshFrobulator(Provider<Frob> frobProvider) {
    this.frobProvider = frobProvider;
  }

  public void startFresh() {
    Frob frob = frobProvider.get();
    frob.frobulate();
  }
}

With Dagger (other DI frameworks have similar concepts) the Provider is just another class that can be passed around, and is itself generated when the object graph is created at startup.

Parameterized Constructors

Parameterized construction is the second wrinkle that appears fairly quickly in a DI environment, and is discussed slightly less frequently. This occurs when there is an instance attribute that would be defined in the constructor if it were a newable class, but the constructor is not available for injectables.

Newable with instance parameters:

public class LimitedFrobulator {
  private final int maxFrobulations;
  private final Frob frob;

  public LimitedFrobulator(Frob frob, int maxFrobulations) {
    this.frob = frob;
    this.maxFrobulations = maxFrobulations;
  }
}

A simple approach to solve this for injectables would be to make a version of the class that requires the DI parameters only in the constructor, then other attributes are set with setter field accessors.

Here is a naive injectable without instance parameters:

/* Anti-pattern Alert */
public class LimitedFrobulator {
  private int maxFrobulations;
  private final Frob frob;

  @Inject
  public LimitedFrobulator(Frob frob) {
    this.frob = frob;
  }

  public void setMaxFrobulations(int value) {
    this.maxFrobulations = value;
  }
}

This is problematic and should be avoided. The reason here is that

  1. It’s lame
  2. We can do better
  3. There’s a race condition that has now been introduced between construction and all of the required fields being set

In the naive example there is a period between when the object is instantiated and when setMaxFrobulations() is called, if at all. This is something that a normal constructor solves for us that in the naive version creates complexity and reduces readability.

What we can do to work around this is to make the constructor private and apply a Factory pattern.

/* Possible Anti-pattern Alert */
public class LimitedFrobulator {
  private int maxFrobulations;
  private final Frob frob;

  public static class Factory {
    private final Frob frob;

    @Inject
    public Factory(Frob frob) {
      this.frob = frob;
    }

    public LimitedFrobulator create(int maxFrobulations) {
      return new LimitedFrobulator(this.frob, maxFrobulations);
    }
  }

  LimitedFrobulator(Frob frob, int maxFrobulations) {
    this.frob = frob;
  }
}

This allows the Factory to be constructed holding a reference to the Frob passed in by the injector, and then calling the private constructor in the create() method with any instance parameters.

There is one shortcoming of this design, the object graph is now broken. When it comes to object creation:

  • injectables can create both other injectables and newables
  • newables can only create other newables

This is due to the fact the injector that is quietly being passed around by the framework stops being passed at the newable boundary. Sometimes this is fine. If the classes you are creating genuinely do not need external resources or replaceable parts, DI may not provide you with much value. It is worth nothing the dependent code may change later, which create some extra work if care was not taken up front.

To support parameterization without breaking the object graph we can use the following factory pattern merging both previous examples:

public class LimitedFrobulator {
  private int maxFrobulations;
  private final Frob frob;

  @Singleton
  public static class Factory {
    private final Provider<LimitedFrobulator> frobulatorProvider;

    @Inject
    public Factory(Provider<LimitedFrobulator> frobulatorProvider) {
      this.frobulatorProvider = frobulatorProvider;
    }

    public LimitedFrobulator create(int maxFrobulations) {
      LimitedFrobulator frobulator = frobulatorProvider.get();
      frobulator.setMaxFrobulations(maxFrobulations);
      return frobulator;
    }
  }

  /* Note this is package-private instead of private */
  @Inject
  LimitedFrobulator(Frob frob) {
    this.frob = frob;
  }

  private void setMaxFrobulations(int value) {
    this.maxFrobulations = value;
  }
}

In the preceding example we now have a Factory.create() method that is an atomic action the same way our newable constructor was. Due to the way DI frameworks work, we can’t make the LimitedFrobulator constructor private, but package-private is good enough.

In this definition, we have also defined the LimitedFrobulator.Factory as a singleton simply by adding the @Singleton annotation. This means we don’t need to allocate a new Factory every time. The choice of when to make the Factory a singleton or not is really defined by the use case. If we needed a fresh Frob for every Frobulator then a singleton does not help us.

Once more unto the breach

Now that we have laid out some basic implementations using DI, let’s develop a more realistic example.

For this program we will have an apartment simulator. The apartment simulator would need an instance of an apartment injected in.

public class ApartmentSimulator {
  private final Apartment apartment;

  @Inject
  public ApartmentSimulator(Apartment apartment) {
    this.apartment = apartment;
  }
}

As part of the apartment simulator, we need a moveIn() operation. This will generate anywhere from 1-10 moving boxes l have an apartment simulator. The apartment simulator would need an instance of an apartment injected in.

public class Apartment {
  private final Provider<MovingBox> movingBoxProvider;

  @Inject
  public Apartment(Provider<MovingBox> movingBoxProvider) {
    this.movingBoxProvider = movingBoxProvider;
  }

  public Set<MovingBox> moveIn() {
    Random random = new Random();

    int boxCount = random.nextInt(10) + 1;

    return Stream
        .generate(() -> movingBoxProvider.get())
        .limit(boxCount)
        .collect(Collectors.toSet());
  }
}

Any time you find a box in the universe, there is a high likelihood that there will be a cat who is already living inside it.

public class MovingBox {
  private Cat resident;

  @Inject
  public MovingBox() {}

  void addCat(Cat resident) {
    this.resident = resident;
  }
}

Since the simulator may want to adjust the chance of a cat being in the box, we will read that from an environment variable in the Provider.

public class ApartmentSimulatorModule {
  public MovingBox movingBox(Provider<Cat> catProvider) {

    MovingBox movingBox = new MovingBox();

    Random random = new Random();

    int catChance = Integer.parseInt(
      System.getenv("CAT_CHANCE")
    );

    boolean catExists = random.nextInt(100) < catChance;

    if (catExists) {
      movingBox.addCat(catProvider.get());
    }
  }
}

This has the interesting effect that the object graph could have been broken here if we did not push an injected cat into the box. The other aspect worth noting is that depending we have hidden some logic inside the ApartmentSimulatorModule that may make sense directly inside the injectable’s constructor. Be wary of doing this.

Now, as we all know, because they like to be punctual, all cats have pocket watches tucked away somewhere:

public class Cat {
  private final PocketWatch pocketWatch;

  @Inject
  public Cat(PocketWatch pocketWatch) {
    this.pocketWatch = pocketWatch;
    this.pocketWatch.synchronize();
  }
}

But what is this? The pocketWatch needs to be synchronized, most likely to an NTP server.

public class PocketWatch {
  private final NTPServer ntpServer;
  private TimerLogic timerLogic;

  @Inject
  public PocketWatch(NTPServer ntpServer) {
    this.ntpServer = ntpServer;
    this.timerLogic = new TimerLogic();
  }

  public void synchronize() {
    timerLogic.reset(ntpServer.now());
  }
}

and

@Singleton
public class NTPServer {
  @Inject
  public NTPServer() {
    // magic!!
  }
}

This gives us a singleton NTP Server that can be replaced in unit tests to return a set time whenever the cats need to synchronize their pocket watches! Without dependency injection, we would have needed to pass this NTP server all the way down through each of the previous constructors just to get the same effect.