Using built-in dependency injection with Liftweb

Dependency injection in liftweb is sort of a point of contention for a lot of people. It used to be like that for me as well. And initially, I too would have preferred using Guice (or some other specific framework) for my DI needs. However, I think lift’s built-in DI mechanisms are good enough for most of my needs, even with a project that has literally thousands of scala classes. Thanks to David Pollak’s comments, I (would like to) think that my eyes have been opened :grin:. The abstraction turtles have to end somewhere.

I am writing this post to enumerate some basic DI utilities that lift has, since these don’t seem to be well-documented. This allows all the four most commonly used scopes that are possible with spring: singleton, prototype, request, and session. The others can be achieved relatively easily if you want, but I won’t focus on those.

Lift’s DI is based on two main traits Injector and Maker. However, most of the time, you won’t need to deal with them directly. The elements that you would use are:

  1. Factory trait
  2. FactoryMaker, which is an abstract class inside inside the [Factory] trait.
  3. Inject, an abstract class within the SimpleInjector trait.

FactoryMaker and Injector serve the same purpose, with the former having more features. The difference is important. Notes on this are at the end. I’ll continue with FactoryMaker for the sake of examples, and then note the differences between the two at the end.

To start with, here’s 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.

Note that FactoryMaker constructor takes a stock scala function that returns an instance of the needed type. A Function0 to be precise 1. 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 converts that instance to a Function0). This is similar to the singleton scope in spring DI.

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. 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. I’ll be using vend for further examples just so that it’s clear what’s happening.

Calling DependencyFactory.sequenceNumberService.vend will return a new number every time, since that’s how it’s been setup. This is, in spring terminology, the prototype scope.

An alternative way is to use DependencyFactory.inject. But it returns Box and it’s only needed if 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].

Overriding dependencies for sessions or requests

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 equivalent to the session-scoped dependency in spring. 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 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 as identified by spring.

Session or request scoped dependencies

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 the request scoped dependencies. With session scoped dependencies, we want a new instance to be created for each session, for all the users.

Request scope with request lifecycle hooks

In your Boot.scala, which is 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.

Session scope with session lifecycle hooks

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’s pretty much it.

Overriding dependency factory for tests and mocking dependencies

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. I’m sure David Pollak is smiling right now. This is what he keeps saying repeatedly (at least I hope so). It’s 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 using 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 possible problem with this approach (I haven’t tested it). If your test suites are running in parallel, this set/reset of the default instance will be problematic. I don’t recommend this approach 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
    ...
  }
}

Differences between FactoryMaker and Inject

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.

Conclusion

Most of the time, you should be able to do away with any specialized DI framework. But I’d be curious to find out if there are some specific cases that can’t be handled by lift’s built-in DI.

  1. It’s also an instance of the Vendor trait, but that’s not important here 


comments

comments powered by Disqus