Dependency Injection
Lift and Dependency Injection
Dependency injection is an important topic in the Java world.
It's important because Java lacks certain basic features (e.g.,
functions) that tend to bind abstract interfaces to concrete
implementations. Basically, it's so much easier to do MyInterface
thing
=
new
MyInterfaceImpl()
, so most developers do just that.
Scala's cake
pattern goes a long way to help developers compose complex
behaviors by combining Scala traits. Jonas Bonér wrote an
excellent piece on Dependency
Injection.
The cake pattern only goes half way to giving a Java developer
complete dependency injection functionality. The cake pattern
allows you to compose the complex classes out of Scala traits, but the
cake pattern is less helpful in terms of allowing you to make dynamic
choices about which combination of cake to vend in a given
situation. Lift provides extra features that complete the
dependency injection puzzle.
Lift Libraries and Injector
Lift is both a web framework and a set of Scala libraries.
Lift's common
, actor
, json
,
and util
packages provide common libraries
for Scala developers to build their application. Lift's libraries
are well tested, widely used, well supported, and released on a well
defined schedule (monthly milestones, quarterly releases).
Lift's Injector trait forms the basis of dependency injection:
/**
* A trait that does basic dependency injection.
*/
trait Injector {
implicit def inject[T](implicit man: Manifest[T]): Box[T]
}
You can use this trait as follows:
object MyInjector extends Injector {...}
val myThing: Box[Thing] = MyInjector.inject
The reason that the instance of MyThing
is in a Box
is that we're
not guaranteed that MyInjector
knows how to create an
instance of
Thing
.
Lift provides an implementation of Injector
called SimpleInjector
that allows you to register (and re-register) functions for injection:
object MyInjector extends SimpleInjector
def buildOne(): Thing = if (testMode) new Thing with TestThingy {} else new Thing with RuntimeThingy {}
MyInjector.registerInjection(buildOne _) // register the function that builds Thing
val myThing: Box[Thing] = MyInjector.inject
This isn't bad... it allows us to define a function that makes the
injection-time decision, and we can change the function out during
runtime (or test-time.) However, there are two problems: getting
Box
es for each injection is less than optimal.
Further, globally
scoped functions mean you have to put a whole bunch of logic (test vs.
production vs. xxx) into the function. SimpleInjector has lots of
ways to help out.
object MyInjector extends SimpleInjector {
val thing = new Inject(buildOne _) {} // define a thing, has to be a val so it's eagerly evaluated and registered
}
def buildOne(): Thing = if (testMode) new Thing with TestThingy {} else new Thing with RuntimeThingy {}
val myThingBox: Box[Thing] = MyInjector.inject
val myThing = MyInjector.thing.vend // vend an instance of Thing
Inject
has a futher trick up its sleeve... with Inject
,
you can
scope the function... this is helpful for testing and if you need to
change behavior for a particular call scope:
MyInjector.thing.doWith(new Thing with SpecialThing {}) {
val t = MyInjector.thing.vend // an instance of SpecialThing
val bt: Box[Thing] = MyInjector.inject // Full(SpecialThing)
}
MyInjector.thing.default.set(() => new Thing with YetAnotherThing {}) // set the global scope
Within the scope of the doWith call, MyInjector.thing
will vend
instances of SpecialThing
. This is useful for
testing as well as
changing behavior within the scope of the call or globally. This
gives us much of the functionality we get with dependency injection
packages for Java. But within Lift WebKit, it gets better.
Lift WebKit and enhanced injection scoping
Lift's WebKit offers broad ranging tools for handling HTTP requests as well as HTML manipulation.
Lift WebKit's Factory
extends SimpleInjector
,
but adds the ability to scope the function based on current HTTP
request or the current container session:
object MyInjector extends Factory {
val thing = new FactoryMaker(buildOne _) {} // define a thing, has to be a val so it's eagerly evaluated and registered
}
MyInjector.thing.session.set(new Thing with ThingForSession {}) // set the instance that will be vended for the duration of the session
MyInjector.thing.request.set(new Thing with ThingForRequest {}) // set the instance that will be vended for the duration of the request
WebKit's LiftRules
is a Factory
and many
of the properties that LiftRules
contains are FactoryMakers
.
This means that you can change behavior during call scope (useful for
testing):
LiftRules.convertToEntity.doWith(true) {
... test that we convert certain characters to entities
}
Or based on the current request (for example you can change the rules for calculating the docType during the current request):
if (isMobileReqest) LiftRules.docType.request.set((r: Req) => Full(DocType.xhtmlMobile))
Or based on the current session (for example, changing
maxConcurrentRequests based on some rules when a session is created):
if (browserIsSomethingElse) LiftRules.maxConcurrentRequests.session.set((r: Req) => 32) // for this session, we allow 32 concurrent requests
Conclusion
Lift's SimpleInjector
/Factory
facilities
provide a powerful and flexible mechanism 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.
Comments are disabled for this space. In order to enable comments, Messages tool must be added to project.
You can add Messages tool from Tools section on the Admin tab.