11

我正在尝试使用 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://...")
    // ...
  }
}

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

有任何想法吗?我误解了蛋糕图案吗?

4

5 回答 5

8

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

这些解决方案中的每一个都有其问题。

拥有两个 Cakes(Daniel 的建议)看起来不错,只要它们不需要一些常见的依赖项。

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

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

于 2013-12-09T23:29:58.527 回答
3

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

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

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

另一种选择(可行,尽管在我看来不是那么干净)是简单地要求一个特定于模块的命名依赖项,即def fooHttpClient: HttpClient,它只是强制对混合模块的人进行显式的外部接线。

于 2013-12-09T15:01:45.800 回答
2

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

于 2013-12-08T22:56:46.913 回答
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
}
于 2013-12-10T06:29:43.413 回答
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/

于 2013-12-11T06:59:06.207 回答