Skip to content

NeuronDIForScalaTutorial

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

Neuron DI For Scala - Tutorial

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)

The Neuron Binding DSL

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.

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
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!

Clone this wiki locally