Do you have any experience of using DI container framework for your Scala project? The most famous DI container frameworks is Google Guice. This library is widely adapted in enterprise and open source community. Since it is defact standard library, it is the best option to select Guice as your DI framework in Java project.
How about Scala project? Of course Scala project can be integrated with Java libraries and frameworks seamlessly. But isn’t there any other way to do DI in more Scala-style way? So that’s the reason why I write this post. Airframe is a library to enable us to do DI more sophisticated way in Scala project. You can write DI code easily without violating Scala style in your project. In this post, I’d like to introduce how to use Airframe in your Scala project.
Install
Like other Scala libraries, you can install by adding sbt dependencies. The latest version is 0.7.
libraryDependencies += "org.wvlet" %% "airframe" % "0.7"
Overview
You can inject an object through trait
in Scala as interface
or abstract class
in Guice. Injecting can be done with bind
method.
Dependency graph of each object is created by Session
in Airframe. It can be regarded as Injector
in Guice. Since Session
is created by
Design
, Design
is corresponding to Module
in Guice. So overall we show a list of correspondence of objects and methods between Airframe and Guice.
Guice | Airframe | |
---|---|---|
Injecting | inject |
bind |
Keeping dependency graph | Module |
Design |
Creating object with resolving dependency | Injector |
Session |
Except for the name difference, the usages are almost same between Airframe and Guice. Let’s see the usage of Airframe next. The whole code can be found in example.
Usage
First let’s define a trait which is injected by Airframe. Let’s assume building a airplane with various type of components.
import wvlet.airframe._
trait Left // Representation of left wing
trait Right // Representation of right wing
case class Wing(name:String) {
override def toString = f"Wing($name:[${hashCode()}%x])"
}
We define metrics object in order to collect information of each component.
trait Metric {
def report(key:String, value:Int)
}
object EmptyMetric extends Metric {
override def report(key: String, value: Int): Unit = {}
}
object MetricLogging extends Metric with LogSupport {
override def report(key: String, value: Int): Unit = {
warn(s"${key}:${value}")
}
}
// Plane type which keeps fuel tank size
case class PlaneType(tankSize:Int)
Next we will define other components which depends on Metric
and PlaneType
. We can use Airframe
binding instead of specifying concrete implementation here.
trait Fuel {
// Get a PlaneType with resolving dependency graph.
lazy val planeType = bind[PlaneType]
var remaining: Int = planeType.tankSize * 10
// Get a Metric object.
val metric = bind[Metric]
def burn(r:Int) {
metric.report("energy.consumption", r)
remaining -= r
}
}
trait Engine {
val engineType: String
// Get a Fuel object from Airframe design session.
val fuel = bind[Fuel]
def run(energy:Int)
}
Since Fuel
and Engine
are defined as trait
, we can bind any implementation of these traits
at creating design session. So let’s define some type of Engine
s.
trait GasolineEngine extends Engine with LogSupport {
val engineType = "Gasoline Engine"
def run(energy:Int) {
// Fuel implementation is fetched Airframe session
fuel.burn(energy)
}
}
case class SolarPanel() {
def getEnergy : Int = {
Random.nextInt(10)
}
}
trait SolarHybridEngine extends Engine with LogSupport {
val engineType = "Solar Hybrid Engine"
val solarPanel = bind[SolarPanel]
def run(energy:Int) {
val e = solarPanel.getEnergy
info(s"Get ${e} solar energy")
fuel.burn(math.max(0, energy - e))
}
}
Now all components needed for creating an airplane are completed to be defined. Airplane design graph can be like below.
trait AirPlane extends LogSupport {
// Binded object with tag.
val leftWing = bind[Wing @@ Left]
val rightWing = bind[Wing @@ Right]
val engine = bind[Engine]
info(f"Built a new plane left:${leftWing}, right:${rightWing}, fuel:${engine.fuel.remaining}, engine:${engine.engineType}")
def start {
engine.run(1)
showRemainingFuel
engine.run(10)
showRemainingFuel
engine.run(5)
showRemainingFuel
}
def showRemainingFuel : Unit = {
info(s"remaining fuel: ${engine.fuel.remaining}")
}
}
One interesting thing to note here is tagged object binding. You can bind different object instance against same class type.
In this case, leftWing
and rightWing
are different object instance even if they are same class.
It can be realized by object tagging provided by wvlet.
Dependency graph can be created by constructing Design
as described previously.
// This is a base design.
val coreDesign =
newDesign
.bind[Wing @@ Left].toInstance(new Wing("left")) // Binding with Left tag
.bind[Wing @@ Right].toInstance(new Wing("right")) // Binding with Right tag
.bind[PlaneType].toInstance(PlaneType(50)) // Initial tank size is 50 * 10
.bind[Metric].toInstance(EmptyMetric) // Binding to an instance
// Add an engine implementation to base design.
val simplePlaneDesign =
coreDesign
.bind[Engine].to[GasolineEngine]
// Add an engine and plane type implementation to base design.
val hybridPlaneDesign =
coreDesign
.bind[PlaneType].toInstance(PlaneType(10)) // Use a smaller tank (10 * 10)
.bind[Engine].to[SolarHybridEngine]
Design defined components whic are used to resolve dependency graph for creating a object.
Creating object can be done by build
method of Session
.
val simplePlane = simplePlaneDesign.newSession.build[AirPlane]
simplePlane.start
This code’s output is like this.
[Example$AirPlane] [info] Built a new plane left:Wing(left:[6ce65b1f]), right:Wing(right:[f2c0f1af]), fuel:500, engine:Gasoline Engine - (Example.scala:73)
[Example$AirPlane] [info] remaining fuel: 499 - (Example.scala:85)
[Example$AirPlane] [info] remaining fuel: 489 - (Example.scala:85)
[Example$AirPlane] [info] remaining fuel: 484 - (Example.scala:85)
We can see this plane uses GasolineEngine
as defined Airframe design.
When we use hybridPlaneDesign
, the implementation of AirPlane is different.
[Example$AirPlane] [info] Built a new plane left:Wing(left:[6ce65b1f]), right:Wing(right:[f2c0f1af]), fuel:100, engine:Solar Hybrid Engine - (Example.scala:73)
[Example$SolarHybridEngine] [info] Get 3 solar energy - (Example.scala:103)
[Example$AirPlane] [info] remaining fuel: 100 - (Example.scala:85)
[Example$SolarHybridEngine] [info] Get 0 solar energy - (Example.scala:103)
[Example$AirPlane] [info] remaining fuel: 90 - (Example.scala:85)
[Example$SolarHybridEngine] [info] Get 7 solar energy - (Example.scala:103)
[Example$AirPlane] [info] remaining fuel: 90 - (Example.scala:85)
In addition to changing Engine
, Fuel tank size becomes smaller.
Recap
Airframe enables us to do DI more easily in Scala like way. Airframe provides not only these functionalities but also building singleton, eagler singleton and lifecycle management etc. I want to explain these advanced features of Airframe if get a chance. Last but not least, Airframe is actively under development and distributed under Apache-2.0 license. Please give us any feedback and issues on GitHub.
Thanks!