-
-
Notifications
You must be signed in to change notification settings - Fork 0
NeuronDIForJavaTutorial
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));
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()
.
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!