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

时间:2013-12-08 22:18:19

标签: scala dependency-injection cake-pattern

我正在尝试使用Cake Pattern在Scala中实现依赖注入,但是遇到依赖冲突。由于我找不到具有此类依赖关系的详细示例,因此这是我的问题:

假设我们有以下特征(有2个实现):

trait HttpClient {
  def get(url: String)
}

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

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

以下两个蛋糕模式模块(在此示例中都是依赖于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
      ???
    }
  }
}

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
      ???
    }
  }
}

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

我们可以在每个模块中以不同的方式命名依赖项,为它们添加模块名称前缀,但这样会很麻烦且不优雅,如果我们自己没有完全控制模块,也无法工作。

有什么想法吗?我是否误解了蛋糕模式?

5 个答案:

答案 0 :(得分:8)

您正确地获得了模式,并且您刚刚发现了它的重要限制。如果两个模块依赖于某个对象(例如HttpClient)并碰巧以相同的名称声明它(如httpClient),则游戏结束 - 您不会在一个Cake中单独配置它们。要么有两个蛋糕,就像丹尼尔建议或改变模块的来源,如果可以的话(正如Tomer Gabel暗示的那样)。

这些解决方案都存在问题。

有两个蛋糕(丹尼尔的建议)看起来很长,他们不需要一些共同的依赖。

重命名某些依赖项(如果可能)会强制您调整使用这些依赖项的所有代码。

因此,有些人(包括我)喜欢对这些问题免疫的解决方案,例如使用普通的旧构造函数并完全避免使用Cake。如果你测量它,它们不会给代码增加太多膨胀(Cake已经非常冗长)并且它们更加灵活。

答案 1 :(得分:3)

“你做错了”(TM)。您对Spring,Guice或任何IoC容器都有完全相同的问题:您将类型视为名称(或符号);你说“给我一个HTTP客户端”而不是“给我一个适合与fooApi通信的HTTP客户端”。

换句话说,您有多个名为httpClient的HTTP客户端,它们不允许您区分不同的实例。这有点像使用@Autowired HttpClient而没有某种方法来限定引用(在Spring的情况下,通常是通过外部连线的bean ID)。

在蛋糕模式中,解决此问题的一种方法是使用其他名称限定该区别:FooApiModule需要例如: def http10HttpClient: HttpClientBarApiModule需要def connectionPooledHttpClient: HttpClient。当“填写”不同的模块时,不同的名称都引用两个不同的实例,但也表示两个模块对其依赖关系的约束。

另一种选择(虽然在我看来并不干净)只是需要一个特定于模块的命名依赖项,即def fooHttpClient: HttpClient,它只会强制显示外部连线,无论是谁混合你的模块。

答案 2 :(得分:2)

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

答案 3 :(得分:1)

似乎是已知的“机器人腿”问题。你需要构建一个机器人的两条腿,但是你需要为它们提供两个不同的脚。

如何使用蛋糕模式同时具有公共依赖关系并分开?

我们有L1 <- A, B1; L2 <- A, B2。你想拥有Main <- L1, L2, A

要拥有单独的依赖项,我们需要两个较小的蛋糕实例,并使用公共依赖项进行参数化。

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
}

Main中,我们将在蛋糕和两条腿上有共同点:

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

答案 4 :(得分:0)

您的最终应用应如下所示:

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://...")
  // ...
 }
}

那应该有用。 (改编自此博文:http://www.cakesolutions.net/teamblogs/2011/12/19/cake-pattern-in-depth/