Skip to content

Latest commit

 

History

History
378 lines (296 loc) · 15.3 KB

dependency-injection-liftweb-scala.adoc

File metadata and controls

378 lines (296 loc) · 15.3 KB

Dependency Injection in Lift

Introduction

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.

Why Lift’s Dependency Injection

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.

Getting started

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:

  1. The Factory trait

  2. The FactoryMaker, an abstract class inside inside the Factory trait.

  3. Inject, an abstract class within the SimpleInjector 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].

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 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.

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 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 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.

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 is 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. 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
    ...
  }
}

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.

Lift’s dependency injection and Spring’s dependency 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.

Conclusion

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.