-
-
Notifications
You must be signed in to change notification settings - Fork 0
NeuronDIForScalaTutorial
Please read the project setup page first.
Next, import the API of Neuron DI For Scala:
import global.namespace.neuron.di.scala._
Now consider the following clock interface:
@Neuron
trait Clock {
/** A synapse method which returns a new date on each call. */
def now: java.util.Date
}
A class or trait annotated with @Neuron
is called a "neuron class" or "neuron trait".
The annotation signals that the neuron class or trait is subject to dependency injection at runtime using the
Incubator
class (see below).
Within a neuron class or trait, any abstract, parameterless def
or val
is called a "synapse method" or
"synapse value".
No special annotation is required for synapse methods or values, however.
Synapse methods and values 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 or a synapse value is accessed.
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 (using ScalaTest matchers):
val clock = Incubator.breed[Clock]
clock.now should not be theSameInstanceAs(clock.now)
clock.now should be <= new java.util.Date
Note that we don't have to implement Clock
:
When breeding an instance, the incubator will introspect the given class or trait and figure what to do:
If the class or trait is annotated with @Neuron
, the incubator will apply itself recusively to any synapse method and
synapse value.
Otherwise, the incubator will call the public, parameterless constructor of the non-neuron class or trait.
If no such constructor is available, an exception is thrown.
In the example above, the incubator figures that Clock
is a neuron trait 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, put it in another neuron class or trait and use a val
definition:
@Neuron
trait ClockModule {
/** A synapse value which holds some clock. */
val clock: Clock
}
Now you can get and test the singleton clock from the module like this:
val module = Incubator.breed[ClockModule]
val clock = module.clock
clock should be theSameInstanceAs module.clock
clock.now should not be theSameInstanceAs(clock.now)
clock.now should be <= new java.util.Date
Again, when breeding a ClockModule
, the incubator figures that it's a neuron trait and lazily applies itself when the
synapse value clock
is accessed for the first time.
Because a val
definition is used for clock
, any subsequent access will yield the same instance again.
For testing purposes, you may want to fix the singleton clock like this:
object FixedClockModule extends ClockModule {
lazy val clock: Clock = new Clock {
def now: java.util.Date = new java.util.Date(0)
}
}
Note that this is a Scala class without any dependency on Neuron DI.
Now we can create and test a fixed clock module like this:
val clock = FixedClockModule.clock
clock should be theSameInstanceAs FixedClockModule.clock
clock.now should not be theSameInstanceAs(clock.now)
clock.now shouldBe new java.util.Date(0)
Consider the following neuron trait:
@Neuron
trait WeatherStation extends Clock {
def temperature: Temperature
@Neuron
trait Temperature {
val value: Double
val unit: String
}
}
This neuron trait has two synapse methods:
temperature
and now
- the latter being inherited from the neuron trait Clock
.
Likewise, the enclosed neuron trait Temperature
has two synapse values:
value
and unit
.
The val
definitions of these members imply that the implementations need to yield the same value on each access.
Now, let's see how to bind these dependencies using Neuron DI:
@Neuron
trait AprilWeatherStation extends WeatherStation {
def temperature: Temperature = {
Incubator
.wire[Temperature]
.bind(_.value).to(value)
.bind(_.unit).to("˚ Celsius")
.breed
}
private def value: Double = java.util.concurrent.ThreadLocalRandom.current.nextDouble(5D, 25D)
}
The method temperature
in the neuron trait AprilWeatherStation
breeds a neuron of the type Temperature
and binds
its synapse value value
to a by-name parameter which calls the method value
of the april weather station in order
to compute a random temperature value between 5/25 inclusive/exclusive.
Also, the synapse value unit
is bound to another by-name parameter which yields the string literal "˚ Celsius"
.
Because of the val
definitions for the synapses of the neuron trait Temperature
, the behavior is that any instance
will compute a random temperature value just in time upon the first access to its synapse value value
and cache it for
subsequent access.
If you wanted the temperature value to be computed eagerly when the temperature neuron is breeded instead, you need to
write the following:
def temperature: Temperature = {
val theValue = value
Incubator
.wire[Temperature]
.bind(_.value).to(theValue)
.bind(_.unit).to("˚ Celsius")
.breed
}
Note that the by-name parameter of the to(theValue)
call is now precomputed.
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.
Finally, lets write a test for the neuron trait AprilWeatherStation
:
val station = Incubator.breed[AprilWeatherStation]
station.now should not be theSameInstanceAs(station.now)
new java.util.Date should be <= station.now
val temperature = station.temperature
temperature should not be theSameInstanceAs(station.temperature)
temperature.value shouldBe temperature.value
temperature.value should be >= 5D
temperature.value should be < 25D
temperature.unit shouldBe temperature.unit
temperature.unit shouldBe "˚ 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 now: java.util.Date
, which is inherited from the neuron trait 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]
instead of a more complex
Incubator.wire[AprilWeatherStation].bind(_.now).to(new java.util.Date).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
trait AprilWeatherStation extends WeatherStation {
def temperature: Temperature = wire[Temperature]
def value: Double = java.util.concurrent.ThreadLocalRandom.current().nextDouble(5D, 25D)
val unit: String = "˚ Celsius"
}
Note how much simpler the wiring statement is:
You don't have to specify any bindings at all.
Instead, the statement wire[Temperature]
(actually a macro call) tells the incubator to bind any synapse methods or
synapse values of the neuron trait Temperature
to equal named values in the current scope.
The value to be bound will be looked up in the current scope and can be a def
or a val
, as long as its name equals
and its type is assignment-compatible to the synapse method or synapse value!
In this particular example, the synapse value Temperature.value
is bound to the private method
AprilWeatherStation.value
and the synapse value Temperature.unit
is bound to the private value
AprilWeatherStation.unit
.
Again, this would work even if these values were defined anywhere else in the current scope, e.g. a super class or trait
of the neuron class AprilWeatherStation
.
If no such member is found however, an exception is thrown at compile-time.
That's it, now you are ready to master Neuron DI for Scala!