- Introduction
- Why Lift’s Dependency Injection
- Getting started
- Overriding dependencies for sessions or requests
- Session or request scoped dependencies
- Overriding dependency factory for tests and mocking dependencies
- Differences between FactoryMaker and Inject
- Lift’s dependency injection and Spring’s dependency scopes
- Conclusion
Dependency injection is an important topic in the JVM world. With Java, we usually go with Guice or Spring whenever we need it. However, Scala provides some unique advantages that allow many of the features needed for dependency injection without requiring an entire framework. For example, Scala allows the use of cake pattern, which facilitates composing complex classes out of Scala traits; however, this by itself is not enough to provide complete dependency injection functionality. In particular, it is less helpful in terms of allowing you to make dynamic choices about which combination of dependencies to vend in a given situation. Lift provides some additional features that complete the dependency injection puzzle.
Many people come to Scala from a Java background, so it is only natural to want to use Guice/Spring, which do a great job in the java world. While those can be used, there is a better, more direct way that is less magical.
Lift’s philosophy is to keep things as simple as possible (and no simpler), and allow full control to the developers over how things work. To paraphrase David Pollak, "The abstraction turtles have to end somewhere", and Lift’s dependency injection features end those turtles quite early.
Lift’s dependency injection is based on two main traits: Injector
and Maker
; however, most of the time,
three higher level elements can be used:
-
The
Factory
trait -
The
FactoryMaker
, an abstract class inside inside theFactory
trait. -
Inject
, an abstract class within theSimpleInjector
trait.
FactoryMaker
and Inject
serve the same purpose, with the former
having more features. An important difference between them is related to performance,
and is discussed in the section Differences between FactoryMaker and Inject.
Note that the following examples will continue using the FactoryMaker
trait; however,
you should be able to use Inject
identically.
To start with, here is an example of how these are supposed to be used:
object DependencyFactory extends Factory {
private val seq = new AtomicLong(0)
object emailService extends FactoryMaker(emailServiceImpl)
object sequenceNumberService extends FactoryMaker(seq.incrementAndGet _)
private def emailServiceImpl: EmailService = Props.mode match {
case Props.RunModes.Production => new RealEmailService
// A stub for local development
case _ => new TestEmailService
}
}
This defines two managed dependencies. In the case of the
emailService
, the dependency changes based on whether we are running
on production/staging mode or some other mode (e.g. development). The
emailService
is a singleton dependency. That is, everyone
who injects this dependency will get the same instance throughout the
app. This is sometimes referred to as the singleton
scope.
Note that the FactoryMaker
constructor takes a stock scala function that
returns an instance of the needed type — a Function0
to
be precise. If you pass in a pre-created instance, as we are
doing in the case of emailService
, that instance will always be
returned when this dependency is injected (scala promotes that
instance to a Function0
).
However, since this is a normal scala function, you can write whatever
you want within that function. For ex. as the sequenceNumberService
illustrates, you can generate a new instance every time it needs to be
injected. This is sometimes called the prototype
scope. Instances could be generated every time, or they could be
generated fresh if some condition is met and so on and so forth.
Here’s how you can use these dependencies:
class SomeClass {
private val emailService = DependencyFactory.emailService()
// Or alternatively, if you don't have the FactoryMaker for a given type
private val someType =
DependencyFactory.inject[SomeType]
.openOrThrowException("No instance of SomeType found")
}
You can use the apply
or vend
method on the FactoryMaker directly,
which will give you the instance you need. The following examples will be
using vend
just so that it is clear what is happening.
Calling DependencyFactory.sequenceNumberService.vend
will return a
new number every time, since that is how it has been setup.
An alternative way is to use DependencyFactory.inject
. But it
returns Box
and it is only needed if you don’t have a FactoryMaker
for a
given dependency. Which brings us to the fact that there are other
ways of registering dependencies apart from the FactoryMaker. For ex.
class SomeOtherClass {
private val someTypeInstance = new SomeType
DependencyFactory.registerInjection[SomeType](() => someTypeInstance)
}
This can be used by arbitrary code in your app to register injectable
instance. Here again, a singleton is registered. In this case, when
you need to access the registered instance, you have to necessarily
use DependencyFactory.inject[SomeType]
.
If you have a scenario where you need to override a given dependency for the duration of the current session, you can do something like following (for ex. in your snippet code):
val customEmailService = new CustomEmailService(currentUser)
DependencyFactory.emailService.session(customEmailService)
This will set the emailService
to always return the
customEmailService
instance for the duration of the current session
for the current user.
Note that is not a session-scoped dependency (as recognized by Spring, for ex.). This is done explicitly in your application code. You are overriding a default dependency with some custom instance for the duration of this session. No other user is going to see it. And as soon as this session is over, it will be gone until you set it explicitly again.
You can do something similar when you need to override a dependency during a given request.
val customEmailService = new CustomEmailService(currentUser)
DependencyFactory.emailService.request(customEmailService)
Again, this is not a request scoped dependency, it is an override for the duration of a given request.
The above examples only set the dependencies for the duration of a given session or request, and only when the relevant code that sets those dependencies was executed.
What if you want to always create a session/request scoped dependency for all the users. Let’s talk about session scoped dependencies. The discussion would be identical for request scoped dependencies. With session scoped dependencies, we want a new instance to be created for each session, for all the users.
In your Boot.scala
, which is generally used for instantiating and configuring
various stuff in Lift:
class Boot {
LiftSession.onBeginServicing = ((sess: LiftSession, req: Req) => {
DependencyFactory.awesomeService.request.set(new AwesomeService {})
}) :: LiftSession.onBeginServicing
...
}
This will set a new instance on every request, right at the beginning
of the request servicing. So, calling
DependencyFactory.awesomeService.vend
will return the instance
created for the particular request.
Similarly, you can do it for sessions in Boot.scala
:
class Boot {
LiftSession.afterSessionCreate = ((_: LiftSession, req: Req) => {
DependencyFactory.awesomeService.session.set(new AwesomeService {})
}) :: LiftSession.afterSessionCreate
...
}
That is pretty much it.
The above cases handle most of the stuff you will need. When testing,
all of your tests might need to mock some of the services, without
affecting other tests. Doing this manually would be nightmarish,
extremely prone to errors. The way to do it is to have isolated
dependency graph for your tests. The key is realizing that the
DependencyFactory
could be just a normal Scala instance that itself
can be injected as needed. See, there are no turtles all the way!.
This is what David Pollak keeps saying repeatedly about Lift and its simplicity.
It is just Scala code. There is no magic here.
A trait that represents your DependencyFactory
import net.liftweb.util.Vendor
trait DependencyFactory extends Factory {
object cardService extends FactoryMaker(cardServiceVendor)
...
// the default implementation of card-service
// this is the method you override when needed
protected def cardServiceVendor: Vendor[CardService] = new PaymentCardService
// other such vendors
...
}
object DependencyFactory extends Factory {
// the default instance that will be used unless overridden
private val DefaultInstance = new DependencyFactory {}
object instance extends FactoryMaker[DependencyFactory](DefaultInstance)
// Allow making calls directly on DependencyFactory companion object
//instead of having to use DependencyFactory.instance
implicit def depFactoryToInstance(dft: DependencyFactory.type)
: DependencyFactory = instance.vend
// you shouldn't write code that needs this, this is just an example
def resetDefault = instance.default.set(DefaultInstance)
}
Now, when you do DependencyFactory.cardService.vend
, it will use
the DefaultInstance
. Your call will be implicitly translated to
DependencyFactory.instance.vend.cardService.vend
. This is the part
that allows you to completely override everything you need in your
dependency graph. For ex. you could do this in your tests:
class SomeSpec {
override def beforeAll = DependencyFactory.instance.default.set({
new DependencyFactory {
override def cardServiceVendor: Vendor[CardService] = mock[CardService]
}
})
override def afterAll: Unit = DependencyFactory.resetDefault
}
However, there is a potential problem with this approach. If your test suites are running in parallel, this set/reset of the default instance will be problematic. This approach is not recommended unless you know what you are doing.
One safe way of doing this is to use the stackable nature of the
Makers
:
private val customDepFactory = new DependencyFactory {
override def cardServiceVendor: Vendor[CardService]
= mock[CardService]
}
DependencyFactory.instance.doWith(customDepFactory) {
// write all your tests here
}
And this would work as expected. You can try to come up with variation on how to do this without the added indentation though. For ex. you can do following with scalatest:
trait DependencyOverrides extends SuiteMixin { self: Suite =>
// Just override this and your tests will be executed with that overridden DependencyFactory instance.
protected def dependencyFactory: Vendor[DependencyFactory] = DependencyFactory.instance
// Run the tests with the given dependency-factory instance.
abstract override def withFixture(test: NoArgTest): Outcome = {
DependencyFactory.instance.doWith(dependencyFactory.vend) {
super.withFixture(test)
}
}
}
Scalatest has something called fixtures that comes in
really handy here. Any test where you need to provide a custom
DependencyFactory
instance should override this trait and just
override with the custom implementation. For ex.
class SomeSpec extends ... with DependencyOverrides {
override val dependencyFactory: Vendor[DependencyFactory] = new DependencyFactory {
override def cardServiceVendor: Vendor[CardService] = mock[CardService]
// other overrides
...
}
}
You can also declare your dependencies using
the Inject
class, exactly like the
FactoryMaker
. For ex.
object cardServiceFactoryMaker extends FactoryMaker(cardServiceVendor)
object cardServiceInject extends Inject(cardServiceVendor)
Both of these can be identically used, with one major
difference: Inject
doesn’t have session/request scoped
dependencies. To quote Antonio (with some modification):
FactoryMaker
can have a very high overhead for simple injection needs (on the order of 100+ms I think) due to the fact that it checks for session-scoped overrides, which require synchronized blocks.Inject
doesn’t have that overhead.
You can see the locking here. This applies to
SessionVar
instances in general. So, there you go. Use Inject
if
you don’t need the session/request scopes.
Lift’s dependency injection easily allows all the four most commonly used scopes that are possible with spring: singleton, prototype, request, and session, and then some!
For ex. the singleton scope is achieved when
the function (the Vendor
) used for constructing the FactoryMaker
or Inject
returns the same instance every time.
The prototype scope is
achieved when that function returns a new instance every time it is called. Request and session scopes are explained
in the section Session or request scoped dependencies. The other scopes can be achieved relatively easily if needed.
Most of the time, you should be able to do away with any specialized dependency injection framework. Lift provides powerful and flexible mechanisms for vending instances based on a global function, call stack scoping, request and session scoping and provides more flexible features than most Java-based dependency injection frameworks, without resorting to XML for configuration or byte-code rewriting magic.