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


我正在尝试使用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) = ???
}

以下两个蛋糕模式模块(在这个例子中都是依赖于我们的API的API) HttpClient 为了他们的功能):

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

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

有任何想法吗?我是否误解了蛋糕模式?


1052
2017-12-08 22:18


起源



答案:


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

每种解决方案都有其问题。

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

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

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


8
2017-12-09 23:29





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

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

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

另一种选择(虽然在我看来并不干净)是简单地要求特定于模块的命名依赖,即 def fooHttpClient: HttpClient,这简单地强制显示外部布线,无论是谁混合您的模块。


3
2017-12-09 15:01





而不是延伸 FooApiModule 和 BarApiModule 在一个地方 - 这意味着他们共享依赖关系 - 使它们成为单独的对象,每个对象都相应地解决它们的依赖关系。


2
2017-12-08 22:56



但这不会破坏IoC的整个概念,并否定对蛋糕模式的需求吗?我不想在蛋糕中间提供依赖关系,我希望它们注入顶层,位于中心位置。我不希望它们分散在蛋糕上的各个层面。 - orrsella
它不违反IoC,它只是涵盖了额外的语义。请参阅下面的答案。 - Tomer Gabel
@orrsella它不是蛋糕的中间 - 它是一个中心位置,但由于它是两组依赖,你在main上写两个val而不是主扩展所有东西。 - Daniel C. Sobral


答案:


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

每种解决方案都有其问题。

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

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

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


8
2017-12-09 23:29





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

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

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

另一种选择(虽然在我看来并不干净)是简单地要求特定于模块的命名依赖,即 def fooHttpClient: HttpClient,这简单地强制显示外部布线,无论是谁混合您的模块。


3
2017-12-09 15:01





而不是延伸 FooApiModule 和 BarApiModule 在一个地方 - 这意味着他们共享依赖关系 - 使它们成为单独的对象,每个对象都相应地解决它们的依赖关系。


2
2017-12-08 22:56



但这不会破坏IoC的整个概念,并否定对蛋糕模式的需求吗?我不想在蛋糕中间提供依赖关系,我希望它们注入顶层,位于中心位置。我不希望它们分散在蛋糕上的各个层面。 - orrsella
它不违反IoC,它只是涵盖了额外的语义。请参阅下面的答案。 - Tomer Gabel
@orrsella它不是蛋糕的中间 - 它是一个中心位置,但由于它是两组依赖,你在main上写两个val而不是主扩展所有东西。 - Daniel C. Sobral


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

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

让我们 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
}

1
2017-12-10 06:29





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

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/


0
2017-12-11 06:59



这可能有用,但你的例子是错的 - 你必须使用 fooApi.fooApi.foo("http://...")。这非常难看,但我猜它会起作用。 - orrsella
您可以将.fooApi添加到初始化结束,然后其余代码就不那么难看了。我看到它的方式,你需要在某个地方写这个逻辑。就像你在Spring中定义一个@Configuration类一样。 Spring允许你注释你的类(例如@Service)并允许你传递整个Bean定义过程,从而避免了丑陋。但是,假设您想控制bean的定义,那么您将总是有些丑陋。 - netta