Skip to content

NeuronDIForJavaTutorial

Christian Schlichtherle edited this page Feb 6, 2019 · 4 revisions

Neuron DI For Java - Tutorial

Please read the project setup page first.

Next, import the API of Neuron DI For Java:

import global.namespace.neuron.di.java.*;

Now consider the following clock interface:

@Neuron
interface Clock {

    /** A synapse method which returns a new date on each call. */
    java.util.Date now();
}

A class or interface annotated with @Neuron is called a "neuron class" or "neuron interface". The annotation signals that the neuron class or interface is subject to dependency injection at runtime using the Incubator class (see below). Within a neuron class or interface, any abstract, parameterless method is called a "synapse method". No special annotation is required for synapse methods, however. Synapse methods are the injection points in Neuron DI, so the incubator will resolve and return an instance of the return type just in time whenever a synapse method is called.

As an implication of this design, all dependency resolution is inherently lazy. This is fundamentally different from any JSR-330 implementation (e.g. Guice), where all dependency injection into constructors, fields or methods is happening at construction time and hence is inherently eager. As a workaround, to support lazy dependency resolution in JSR-330, you need to inject an instance of the javax.inject.Provider interface. In contrast, there is no need for such an interface in Neuron DI.

Considering the example again, you can create and test a clock like this:

Clock clock = Incubator.breed(Clock.class);
assert clock.now() != clock.now();
assert clock.now().compareTo(new java.util.Date()) <= 0;

Note that we don't have to implement Clock: When breeding an instance, the incubator will introspect the given class or interface and figure what to do: If the class or interface is annotated with @Neuron, the incubator will apply itself recursively to any synapse method. Otherwise, the incubator will call the public, parameterless constructor of the non-neuron class or interface. If no such constructor is available, an exception is thrown.

In the example above, the incubator figures that Clock is a neuron interface so that it needs to recursively apply itself when the synapse method now is called. The synapse method now returns a java.util.Date, which isn't a neuron class, so when called, the incubator simply calls new java.util.Date(), which is exactly what a clock is supposed to do.

If you want the clock to be a singleton, just put it in another neuron class or interface and apply the @Caching annotation to the synapse method:

@Neuron
interface ClockModule {

    /** A synapse method which returns the same clock on each call. */
    @Caching
    Clock clock();
}

Now you can get and test the singleton clock from the module like this:

ClockModule module = Incubator.breed(ClockModule.class);
Clock clock = module.clock();
assert clock == module.clock();
assert clock.now() != clock.now();
assert clock.now().compareTo(new java.util.Date()) <= 0;

Again, when breeding a ClockModule, the incubator figures that it's a neuron interface and recursively applies itself when the synapse method clock() is called for the first time. Because the @Caching annotation is applied to the synapse method, any subsequent call will return the same instance again.

For testing purposes, you may want to fix the singleton clock like this:

@Neuron
interface FixedClockModule extends ClockModule {

    @Caching
    default Clock clock() { return () -> new java.util.Date(0); }
}

Note that this is a Java 8 interface with a default method and a lambda expression. It has just been annotated with @Neuron so that we can apply the @Caching annotation and take advantage of its associated thread-safe caching strategy. This is necessary because in Java 8, interfaces are stateless so you couldn't even add a field to hold the cached dependency! The @Caching annotation of Neuron DI alleviates this constraint.

Now we can create and test a fixed clock module like this:

ClockModule module = Incubator.breed(FixedClockModule.class);
Clock clock = module.clock();
assert clock == module.clock();
assert clock.now() != clock.now();
assert clock.now().equals(new Date(0));

The Neuron Binding DSL

Consider the following neuron interface:

@Neuron
interface WeatherStation extends Clock {

    Temperature temperature();

    @Neuron
    interface Temperature {

        @Caching
        double value();

        @Caching
        String unit();
    }
}

This neuron interface has two synapse methods: temperature() and now(), the latter being inherited from the neuron interface Clock. Likewise, the enclosed neuron interface Temperature has two synapse methods: value() and unit(). The @Caching annotations on these methods imply that the implementations need to return the same value on each call.

Now, let's see how to bind these dependencies using Neuron DI:

@Neuron
public abstract class AprilWeatherStation implements WeatherStation {

    public Temperature temperature() {
        return Incubator
                .wire(Temperature.class)
                .bind(Temperature::value).to(this::value)
                .bind(Temperature::unit).to("˚ Celsius")
                .breed();
    }

    private double value() {
        return java.util.concurrent.ThreadLocalRandom.current().nextDouble(5D, 25D);
    }
}

The method temperature() in the neuron class AprilWeatherStation breeds a neuron of the type Temperature and binds its synapse method value() to the method value() of the april weather station in order to compute a random temperate value between 5/25 inclusive/exclusive. Also, the synapse method unit() is bound to the string literal "˚ Celsius".

Because of the @Caching annotations on the synapse methods of the neuron interface Temperature, the behavior is that any instance will compute a random temperature value just in time upon the first call to its synapse method value() and cache it for subsequent calls. If you wanted the temperature value to be computed eagerly when the temperature neuron is breeded instead, you need to write the following:

public Temperature temperature() {
    return Incubator
            .wire(Temperature.class)
            .bind(Temperature::value).to(value())
            .bind(Temperature::unit).to("˚ Celsius")
            .breed();
}

Note that there is no lambda expression in the call to(value()). Note also that we did not have to change the Temperature interface to alter the behavior: Choosing between an eager or lazy dependency resolution strategy is an implementation detail in Neuron DI. Contrast this to JSR-330 where you have to make javax.inject.Provider an integral part of your design and refactor your code if you forget to do this in advance.

Next, lets write a test for the neuron class AprilWeatherStation:

WeatherStation station = Incubator.breed(AprilWeatherStation.class);
assert station.now() != station.now();
assert new java.util.Date().compareTo(station.now()) <= 0;
WeatherStation.Temperature temperature = station.temperature();
assert temperature != station.temperature();
assert temperature.value() == temperature.value();
assert temperature.value() >= 5D;
assert temperature.value() < 25D;
assert temperature.unit().equals(temperature.unit());
assert temperature.unit().equals("˚ Celsius");

We need to use the incubator again to breed the april weather station because we want it to apply itself to the synapse method java.util.Date now(), which is inherited from the neuron interface Clock, and return a new java.util.Date() on each call. Thus, we don't need to wire anything and can use the simplified breeding statement Incubator.breed(AprilWeatherStation.class) instead of a more complex Incubator.wire(AprilWeatherStation.class).bind(AprilWeatherStation::now).to(Date::new).breed().

Auto-wiring

This is all nice and type-safe, but on a large scale, the default behavior of Incubator.breed(*) won't do and then again writing statements of the form Incubator.wire(*).bind(*).to(*)...breed() all the time is quite tedious. Therefore, Neuron DI also supports auto-wiring dependencies. Let's refactor AprilWeatherStation to take advantage of auto-wiring:

@Neuron
public abstract class AprilWeatherStation implements WeatherStation {

    public Temperature temperature() { return Incubator.wire(Temperature.class).using(this); }

    private static double value() { return java.util.concurrent.ThreadLocalRandom.current().nextDouble(5D, 25D); }

    private static final String unit = "˚ Celsius";
}

Note how much simpler the wiring statement is: You don't have to specify any bindings at all. Instead, the statement Incubator.wire(Temperature.class).using(this) tells the incubator to bind any synapse methods of the neuron interface Temperature to equal named members of this object of the neuron class AprilWeatherStation.
The member to be bound can be a field or a method and can have any modifier, including static and private, and be located in any superclass or implemented interface of the neuron class AprilWeatherStation, as long as its name equals and its type is assignment-compatible to the synapse method!

In this particular example, the synapse method Temperature.value() is bound to the private, static method AprilWeatherStation.value() and the synapse method Temperature.unit() is bound to the private, static field AprilWeatherStation.unit. Again, this would work even if these members were defined in a superclass or interface of the neuron class AprilWeatherStation.

If no such member is found however, an exception is thrown at runtime. This is weaker than a wiring statement of the form Incubator.wire(*).bind(*).to(*)...breed(), where the compiler can tell you if the member doesn't exist or is not assignment-compatible, but it's considerably less typing effort. As a compromise, you can mix the two approaches by writing a statement of the form Incubator.wire(*).bind(*).to(*)...using(*). Then again, any synapse method explicitly bound using bind(*).to(*) will not be looked up in the object referenced by using(*).

That's it, now you are ready to master Neuron DI for Java!

Clone this wiki locally