Scala Cake Pattern and Dependency Collisions
Asked Answered
M

5

11

I'm trying to implement dependency injection in Scala with the Cake Pattern, but am running into dependency collisions. Since I could not find a detailed example with such dependencies, here's my problem:

Suppose we have the following trait (with 2 implementations):

trait HttpClient {
  def get(url: String)
}

class DefaultHttpClient1 extends HttpClient {
  def get(url: String) = ???
}

class DefaultHttpClient2 extends HttpClient {
  def get(url: String) = ???
}

And the following two cake pattern modules (which in this example are both APIs that depend on our HttpClient for their functionality):

trait FooApiModule {
  def httpClient: HttpClient        // dependency
  lazy val fooApi = new FooApi()    // providing the module's service

  class FooApi {
    def foo(url: String): String = {
      val res = httpClient.get(url)
      // ... something foo specific
      ???
    }
  }
}

and

trait BarApiModule {
  def httpClient: HttpClient        // dependency
  lazy val barApi = new BarApi()    // providing the module's service

  class BarApi {
    def bar(url: String): String = {
      val res = httpClient.get(url)
      // ... something bar specific
      ???
    }
  }
}

Now when creating the final app that uses both modules, we need to provide the httpClient dependency for both of the modules. But what if we want to provide a different implementation of it for each of the modules? Or simply provide different instances of the dependency configured differently (say with a different ExecutionContext for example)?

object MyApp extends FooApiModule with BarApiModule {
  // the same dependency supplied to both modules
  val httpClient = new DefaultHttpClient1()

  def run() = {
    val r1 = fooApi.foo("http://...")
    val r2 = barApi.bar("http://...")
    // ...
  }
}

We could name the dependencies differently in each module, prefixing them with the module name, but that would be cumbersome and inelegant, and also won't work if we don't have full control of the modules ourselves.

Any ideas? Am I misinterpreting the Cake Pattern?

Malpractice answered 8/12, 2013 at 22:18 Comment(0)
T
8

You get the pattern correctly and you've just discovered its important limitation. If two modules depend on some object (say HttpClient) and happen to declare it under the same name (like httpClient), the game is over - you won't configure them separately inside one Cake. Either have two Cakes, like Daniel advises or change modules' sources if you can (as Tomer Gabel is hinting).

Each of those solutions has its problems.

Having two Cakes (Daniel's advice) looks well as long they don't need some common dependencies.

Renaming some dependencies (provided it's possible) forces you to adjust all code that uses those.

Therefore some people (including me) prefer solutions immune to those problems, like using plain old constructors and avoid Cake altogether. If you measured it, they don't add much bloat to the code (Cake is already pretty verbose) and they're much more flexible.

Tucky answered 9/12, 2013 at 23:29 Comment(0)
I
3

"You're doing it wrong" (TM). You'd have the exact same problem with Spring, Guice or any IoC container: you're treating types as names (or symbols); you're saying "Give me an HTTP client" instead of "Give me an HTTP client suitable for communicating with fooApi".

In other words, you have multiple HTTP clients all named httpClient, which does not allow you to make any distinction between different instances. It's kind of like taking an @Autowired HttpClient without some way to qualify the reference (in Spring's case, usually by bean ID with external wiring).

In the cake pattern, one way to resolve this is to qualify that distinction with a different name: FooApiModule requires e.g. a def http10HttpClient: HttpClient and BarApiModule requires def connectionPooledHttpClient: HttpClient. When "filling in" the different modules, the different names both reference two different instances but are also indicative of the constraints the two modules place on their dependencies.

An alternative (workable albeit not as clean in my opinion) is to simply require a module-specific named dependency, i.e. def fooHttpClient: HttpClient, which simply forces an explicit external wiring on whomever mixes your module in.

Illailladvised answered 9/12, 2013 at 15:1 Comment(0)
D
2

Instead of extending FooApiModule and BarApiModule in a single place -- which would mean they share dependencies -- make them both separate objects, each with their dependencies solved accordingly.

Dome answered 8/12, 2013 at 22:56 Comment(3)
But wouldn't that destroy the entire concept of IoC, and negate the need for the Cake Pattern? I don't want to have to supply dependencies at the middle of the cake, I want them injected at the top level, in a central place. I don't want them scattered at various levels on the cake.Malpractice
It doesn't violate IoC, it merely covers additional semantics. See my answer below.Illailladvised
@Malpractice It's not the middle of the cake -- it is a central place, but since it's two sets of dependencies, you write two vals on main instead of having main extend everything.Dome
A
1

Seems to be the known "robot legs" problem. You need to construct two legs of a robot, however you need to supply two different feet to them.

How to use the cake pattern to have both common dependencies and separate?

Let's have L1 <- A, B1; L2 <- A, B2. And you want to have Main <- L1, L2, A.

To have separate dependencies we need two instances of smaller cakes, parameterized with common dependencies.

trait LegCommon { def a:A}
trait Bdep { def b:B }
class L(val common:LegCommon) extends Bdep { 
  import common._
  // declarations of Leg. Have both A and B.
}
trait B1module extends Bdep {
  val b = new B1
}
trait B2module extends Bdep {
  def b = new B2
}

In Main we'll have common part in cake and two legs:

trait Main extends LegCommon {
  val l1 = new L(this) with B1module
  val l2 = new L(this) with B2module
  val a = new A
}
Arbitrary answered 10/12, 2013 at 6:29 Comment(0)
K
0

Your final app should look like this:

object MyApp {
  val fooApi = new FooApiModule {
    val httpClient = new DefaultHttpClient1()
  }.fooApi
  val barApi = new BarApiModule {
     val httpClient = new DefaultHttpClient2()
  }.barApi
  ...

 def run() = {
  val r1 = fooApi.foo("http://...")
  val r2 = barApi.bar("http://...")
  // ...
 }
}

That should work. (Adapted from this blog post: http://www.cakesolutions.net/teamblogs/2011/12/19/cake-pattern-in-depth/)

Karikaria answered 11/12, 2013 at 6:59 Comment(2)
This could work, but your example is wrong – you'd have to use fooApi.fooApi.foo("http://..."). This is pretty ugly, but I guess it would otherwise work.Malpractice
You can add the .fooApi to the end of initialization and then the rest of the code is less ugly. The way I see it, you need to write this logic somewhere. It's just like you are defining a @Configuration class in Spring. Spring got around the ugliness by allowing you at annotations to your class (e.g. @Service) and allows you to by pass the whole Bean definition process. Assuming, however, you want control over the definition of your beans then you will always have some ugliness.Karikaria

© 2022 - 2024 — McMap. All rights reserved.