don't worry, it's probably fine

Build Your Own Dependency Injection Library - Part 2

13 Nov 2020

nablopomo java

In the last post, we set up a minimum viable dependency injection library. It can create new objects provided they have zero dependencies.

This is a bit of a rubbish dependency injection library, so let’s add the ability to register multiple components and instantiate them on the fly.

Multi-dependency hierarchy

Rather than using Class.newInstance(), which is deprecated, we can iterate through the results of Class.getConstructors().

This returns an array of Constructor<?>, one for each constructor on the class.

Each Constructor knows the classes it expects to be passed and can expose those using Constructor.getParameterTypes().

We still need a way to inform the injector which classes are “in play” and available for injection. Let’s call this method bind().

In pseudo-code, the second iteration of the injector looks like this:

fn bind(class):
    add class to bound_classes

fn get(class):
    for constructor in class:
        if parameter_types subset of bound_classes:
            constructor.call( get(type) for type in parameter_types)

This should work but this iteration of the injector has a few short-comings:

  • The heuristic to find a constructor is greedy - the first match it finds, it will use.
  • It cannot handle binding to instances, or to interfaces / superclasses.
  • It currently cannot handle dependency cycles - it will overflow the stack with recursive calls and crash.

We will deal with these in the future.

Implementation

A couple of things here: we need at least one public constructor for each class, otherwise we cannot discover them in the current implementation.

The test has a single level of dependencies, but will generalise to multiple levels of dependencies because of the recursive call to get().

MultipleDependencyTest.java
class MultipleDependencyTest {
    
    static class One {
        public One() {}

        public String say() {
            return "Hello, world!";
        }
    }

    static class Two {
        private final One one;

        public Two(One one) {
            this.one = one;
        }

        public String say() {
            return this.one.say();
        }
    }

    @Test
    public void canInstantiateAClassWithDependencies() {
        var injector = new TinyInjector();
        injector.bind(One.class);
        injector.bind(Two.class);

        var instance = injector.get(Two.class);

        assertThat(instance.say(), is("Hello, world!"));
    }
}
TinyInjector.java
class TinyInjector {
    private final List<Class<?>> boundClasses = new ArrayList<>();

    public <T> void bind(Class<T> klass) {
        boundClasses.add(klass);
    }

    public <T> T get(Class<T> klass) {
        try {
            for (Constructor<?> constructor : klass.getConstructors()) {
                var parameterTypes = constructor.getParameterTypes();

                if (stream(parameterTypes).anyMatch(not(boundClasses::contains))) {
                    continue;
                }

                return klass.getConstructor(parameterTypes)
                        .newInstance(stream(parameterTypes).map(this::get).toArray());
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        throw new RuntimeException("Could not find a valid constructor");
    }
}

We also need a “break-glass” exception at the end of the method for the scenario where there are no valid constructors on the class. In this case, the call to .get() will throw an exception upwards because there is no way to continue.

In a future step, we will replace this with a custom exception to enable clearer error behaviour.

That will do for now!

In the next post, we’ll be introducing bindInstance(class, instance) and bind(superclass, class) which will require changing how we keep track of bindings within the TinyInjector.


November is National Blog Posting Month, or NaBloPoMo. I’ll be endeavouring to write one blog post per day in the month of November 2020 - some short and sweet, others long and boring.