Lift and Dependency Injection November 7, 2010
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 a 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 (montly 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 because 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 SimpleInjectordef 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.injectval myThing = MyInjector.thing.vend // vend an instance of Thing
Inject
has a futher trick up its sleave... 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.