matthewrk matthewrk - 14 days ago 6
Scala Question

Scala Dependency Injection with Cake Pattern

I've been following this article which describes how to achieve dependency injection in Scala via the Cake Pattern:
http://jonasboner.com/real-world-scala-dependency-injection-di/

I'm kind of new to Scala and I admit some of it went over my head, so far I've got the following working:

// Setup the component and interface
trait AccountRepositoryComponent {
val accountRepository: AccountRepositoryInterface

trait AccountRepositoryInterface {
def message: String
}
}

// An implementation
trait MyAccountRepositoryComponent extends AccountRepositoryComponent {
object AccountRepository extends AccountRepositoryInterface {
def message: String = "Hello"
}
}

// Object to configure which implementations to use and retrieve them
object ComponentRegistry extends MyAccountRepositoryComponent {
val accountRepository = AccountRepository
}

// Example service using the above
object AccountService {
val repo = ComponentRegistry.accountRepository
def say: String = repo.message
}

println(AccountService.say)


What I'm failing to understand is how I would now pass in a fake repository to Account Service, say to change the output to "Test" rather than "Hello"?

Answer

There are various ways this could be modified to achieve a workable result, depending on what counts as a workable result for your situation. I'll go through a simpler possibility here.

First, the ComponentRegistry needs to become a trait, so it can be mixed in to the AccountService:

// Trait to configure which component implementations to use and retrieve them
object ComponentRegistry extends MyAccountRepositoryComponent {
  val accountRepository = AccountRepository
}

// Example service using the above
object AccountService extends ComponentRegistry { 
  def say: String = accountRepository.message
}

println(AccountService.say)

This should print "Hello" as before. To set up a test case, add the following:

// Test implementation
trait TestAccountRepositoryComponent extends AccountRepositoryComponent {
  object AccountRepository extends AccountRepositoryInterface {
    def message: String = "Test"
  }
}

// trait to configure test component implementations
trait TestComponentRegistry extends TestAccountRepositoryComponent {
  val accountRepository = AccountRepository
}

Now we can set up a service that uses the test components:

// Example service using the above
object AccountService extends TestComponentRegistry { 
  //val repo = ComponentRegistry.accountRepository
  def say: String = accountRepository.message
}

println(AccountService.say)

This should print "Test".

Note that you would probably want your AccountService to define its functionality in terms of other mixins/traits, which would expect the appropriate components to be available (layered into the "cake"), but wouldn't know which implementation was in use. Eg:

trait CustomerApi {
  self: AccountRepositoryComponent => // Expects an implementation of AccountRepositoryComponent to be mixed in

  def say: String = accountRepository.message
}

Now the method say is implemented without knowing what version of AccountRepository it will interact with, but knowing one must be provided (checked at compile time). So we can write:

object AccountService extends CustomerApi with ComponentRegistry
object TestAccountService extends CustomerApi with TestComponentRegistry

Calling println(AccountService.say) will generate "Hello", while calling println(TestAccountService.say) will generate "Test".

Comments