简体   繁体   English

斯卡拉蛋糕模式和依赖性冲突

[英]Scala Cake Pattern and Dependency Collisions

I'm trying to implement dependency injection in Scala with the Cake Pattern, but am running into dependency collisions. 我正在尝试使用Cake Pattern在Scala中实现依赖注入,但是遇到依赖冲突。 Since I could not find a detailed example with such dependencies, here's my problem: 由于我找不到具有此类依赖关系的详细示例,这是我的问题:

Suppose we have the following trait (with 2 implementations): 假设我们有以下特征(有2个实现):

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): 以下两个蛋糕模式模块(在这个例子中都是依赖于我们的HttpClient功能的API):

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. 现在,在创建使用这两个模块的最终应用程序时,我们需要为这两个模块提供httpClient依赖关系。 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)? 或者只是提供不同配置的不同实例(例如,使用不同的ExecutionContext )?

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? 我是否误解了蛋糕模式?

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. 如果两个模块依赖于某个对象(例如HttpClient)并碰巧以相同的名称声明它(如httpClient),则游戏结束 - 您不会在一个Cake中单独配置它们。 Either have two Cakes, like Daniel advises or change modules' sources if you can (as Tomer Gabel is hinting). 要么有两个蛋糕,就像丹尼尔建议或改变模块的来源,如果可以的话(正如Tomer Gabel暗示的那样)。

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. 因此,有些人(包括我)喜欢对这些问题免疫的解决方案,例如使用普通的旧构造函数并完全避免使用Cake。 If you measured it, they don't add much bloat to the code (Cake is already pretty verbose) and they're much more flexible. 如果你测量它,它们不会给代码增加太多膨胀(Cake已经非常冗长)并且它们更加灵活。

"You're doing it wrong" (TM). “你做错了”(TM)。 You'd have the exact same problem with Spring, Guice or any IoC container: you're treating types as names (or symbols); 您对Spring,Guice或任何IoC容器都有完全相同的问题:您将类型视为名称(或符号); you're saying "Give me an HTTP client" instead of "Give me an HTTP client suitable for communicating with fooApi". 你说“给我一个HTTP客户端”,而不是“给我一个适合与fooApi通信的HTTP客户端”。

In other words, you have multiple HTTP clients all named httpClient , which does not allow you to make any distinction between different instances. 换句话说,您有多个名为httpClient HTTP客户端,它们不允许您在不同实例之间进行任何区分。 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). 这有点像采用@Autowired HttpClient而没有某种方法来限定引用(在Spring的情况下,通常是通过外部连线的bean ID)。

In the cake pattern, one way to resolve this is to qualify that distinction with a different name: FooApiModule requires eg a def http10HttpClient: HttpClient and BarApiModule requires def connectionPooledHttpClient: HttpClient . 在蛋糕模式中,解决此问题的一种方法是使用不同的名称限定该区别: FooApiModule需要例如def http10HttpClient: HttpClientBarApiModule需要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, ie def fooHttpClient: HttpClient , which simply forces an explicit external wiring on whomever mixes your module in. 另一种选择(虽然在我看来并不干净)是简单地需要一个特定于模块的命名依赖项,即def fooHttpClient: HttpClient ,它只是强制在你的模块混合的任何人上进行显式的外部连接。

而不是将FooApiModuleBarApiModule到一个地方 - 这意味着它们共享依赖关系 - 使它们成为独立的对象,每个对象都相应地解决了它们的依赖关系。

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 ; 我们有L1 <- A, B1 ; L2 <- A, B2 . L2 <- A, B2 And you want to have Main <- L1, L2, A . 你想要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: Main我们将在蛋糕和两条腿上有共同点:

trait Main extends LegCommon {
  val l1 = new L(this) with B1module
  val l2 = new L(this) with B2module
  val a = new A
}

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/ ) (改编自此博客文章: http//www.cakesolutions.net/teamblogs/2011/12/19/cake-pattern-in-depth/

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM