Scala依赖注入:隐式参数的替代方案

时间:2011-12-08 11:38:09

标签: scala dependency-injection implicits

请原谅这个问题的长度。

我经常需要在我的代码的一层创建一些上下文信息,并在其他地方使用该信息。我通常发现自己使用隐式参数:

def foo(params)(implicit cx: MyContextType) = ...

implicit val context = makeContext()
foo(params)

这很有效,但是要求隐式参数传递很多,在介入函数布局后污染图层的方法签名,即使他们自己并不关心它。

def foo(params)(implicit cx: MyContextType) = ... bar() ...
def bar(params)(implicit cx: MyContextType) = ... qux() ...
def qux(params)(implicit cx: MyContextType) = ... ged() ...
def ged(params)(implicit cx: MyContextType) = ... mog() ...
def mog(params)(implicit cx: MyContextType) = cx.doStuff(params)

implicit val context = makeContext()
foo(params)

我发现这种方法很难看,但它确实有一个优点:它的类型安全。我确切地知道mog将收到正确类型的上下文对象,或者它不会编译。

如果我可以使用某种形式的“依赖注入”来定位相关的上下文,它将减轻这种混乱。这些引号表明这与Scala中常见的依赖注入模式不同。

起点foo和终点mog可能存在于系统的不同级别。例如,foo可能是用户登录控制器,mog可能正在进行SQL访问。可能有许多用户同时登录,但只有一个SQL层实例。每次mog由不同的用户调用时,都需要不同的上下文。因此,上下文不能被烘焙到接收对象中,也不想以任何方式合并这两个层(如Cake Pattern)。我还宁愿不依赖像Guice或Spring这样的DI / IoC库。我发现它们很重,不太适合Scala。

所以我认为我需要的是让mog在运行时为它检索正确的上下文对象,有点像ThreadLocal,其中有一个堆栈:

def foo(params) = ...bar()...
def bar(params) = ...qux()...
def qux(params) = ...ged()...
def ged(params) = ...mog()...
def mog(params) = { val cx = retrieveContext(); cx.doStuff(params) }

val context = makeContext()
usingContext(context) { foo(params) }

但是,只要异步参与者涉及链中的任何地方,这种情况就会失败。使用哪个actor库并不重要,如果代码在不同的线程上运行,那么它就会丢失ThreadLocal

所以...有一个我不知道的伎俩吗?在Scala中上传信息而不污染干预方法签名的方法,不会静态地将上下文烘焙到接收器中,并且仍然是类型安全的吗?

5 个答案:

答案 0 :(得分:10)

Scala标准库包含类似于假设的“usingContext”,名为DynamicVariable。这个问题有一些关于它的信息When we should use scala.util.DynamicVariable?。 DynamicVariable确实使用了ThreadLocal,因此ThreadLocal的许多问题都将保留下来。

读者monad是显式传递环境http://debasishg.blogspot.com/2010/12/case-study-of-cleaner-composition-of.html的功能替代方法。 Reader monad可以在Scalaz http://code.google.com/p/scalaz/中找到。但是,ReaderMonad会“污染”您的签名,因为它们的类型必须更改,并且通常monadic编程可能会导致对代码进行大量重组,如果性能或内存成为问题,则所有闭包的额外对象分配可能不会很好。

这些技术都不会通过演员信息发送自动共享上下文。

答案 1 :(得分:6)

派对有点晚了,但您是否考虑过对类构造函数使用隐式参数?

class Foo(implicit biz:Biz) {
   def f() = biz.doStuff
}
class Biz {
   def doStuff = println("do stuff called")
}

如果您希望每次调用f()都有新的商业广告,您可以让隐含参数成为返回新商家的函数:

class Foo(implicit biz:() => Biz) {
   def f() = biz().doStuff
}

现在,您只需在构建Foo时提供上下文。您可以这样做:

trait Context {
    private implicit def biz = () => new Biz
    implicit def foo = new Foo // The implicit parameter biz will be resolved to the biz method above
}

class UI extends Context {
    def render = foo.f()
}

请注意,隐式biz方法在UI中不可见。所以我们基本上隐藏了那些细节:)

我写了一篇关于using implicit parameters for dependency injection which can be found here(无耻的自我推销;)的博客文章。

答案 2 :(得分:2)

我认为电梯的依赖注入可以满足您的需求。有关使用doWith()方法的详细信息,请参阅wiki

请注意,即使您没有使用升降机,也可以将其用作单独的库。

答案 3 :(得分:1)

大约一年前你问过这个,但这是另一种可能性。如果您只需要调用一种方法:

def fooWithContext(cx: MyContextType)(params){
    def bar(params) = ... qux() ...
    def qux(params) = ... ged() ...
    def ged(params) = ... mog() ...
    def mog(params) = cx.doStuff(params)
    ... bar() ...
}

fooWithContext(makeContext())(params)

如果您需要外部可见所有方法:

case class Contextual(cx: MyContextType){
    def foo(params) = ... bar() ...
    def bar(params) = ... qux() ...
    def qux(params) = ... ged() ...
    def ged(params) = ... mog() ...
    def mog(params) = cx.doStuff(params)
}

Contextual(makeContext()).foo(params)

这基本上是蛋糕模式,除了如果你的所有东西都适合单个文件,你不需要所有杂乱的trait东西将它组合成一个对象:你可以嵌套它们。这样做也会使cx正确地限定词法范围,因此当你使用期货和演员等时,你最终不会产生有趣的行为。我怀疑如果你使用新的AnyVal,你甚至可以免除分配Contextual对象的开销。

如果您想使用trait将内容拆分为多个文件,那么每个文件只需要一个trait来保存所有内容并将MyContextType正确放入范围内,如果你不需要花哨的可替换组件 - 通过继承的东西大多数蛋糕模式的例子。

// file1.scala
case class Contextual(cx: MyContextType) with Trait1 with Trait2{
    def foo(params) = ... bar() ...
    def bar(params) = ... qux() ...
}

// file2.scala
trait Trait1{ self: Contextual =>
    def qux(params) = ... ged() ...
    def ged(params) = ... mog() ...
}

// file3.scala
trait Trait2{ self: Contextual =>
    def mog(params) = cx.doStuff(params)
}

// file4.scala
Contextual(makeContext()).foo(params)

在一个小例子中看起来有点混乱,但请记住,如果代码太大而无法在一个文件中使用,则只需要将其拆分为新的特征。到那时你的文件相当大,所以在200-500行文件上额外的两行样板文件并不是那么糟糕。

修改

这也适用于异步的东西

case class Contextual(cx: MyContextType){
    def foo(params) = ... bar() ...
    def bar(params) = ... qux() ...
    def qux(params) = ... ged() ...
    def ged(params) = ... mog() ...
    def mog(params) = Future{ cx.doStuff(params) }
    def mog2(params) = (0 to 100).par.map(x => x * cx.getSomeValue )
    def mog3(params) = Props(new MyActor(cx.getSomeValue))
}

Contextual(makeContext()).foo(params)

使用嵌套 Just Works 。如果您能够使用DynamicVariable获得类似的功能,我会留下深刻的印象。

你需要一个Future的特殊子类,它在创建时存储当前DynamicVariable.value,并挂钩ExecutionContext的{​​{1}}或prepare()在执行execute()之前提取value并正确设置DynamicVariable的方法。

然后你需要一个特殊的Future来做类似的事情,以使并行集合起作用。还有一个特殊的scala.collection.parallel.TaskSupport,以便为 做类似的事情。

每当有一种创建异步任务的新机制时,基于akka.actor.Props的实现都会中断,你会遇到奇怪的错误,最终导致错误的DynamicVariable。每次添加新的Context进行跟踪时,您都需要修补所有特殊执行程序,以正确设置/取消设置此新DynamicVariable。使用嵌套,你可以让词法封闭为你完成所有这些。

(我认为DynamicVariable s,Futurecollections.parallel计为“介于两者之间的不是我的代码的图层”)

答案 4 :(得分:1)

与隐式方法类似,使用Scala宏可以使用构造函数自动连接对象 - 请参阅我的MacWire项目(并原谅自我推销)。

MacWire也有范围(可定制,提供了ThreadLocal实现)。但是,我不认为您可以使用库传播actor调用的上下文 - 您需要携带一些标识符。这可以是例如通过包装器发送演员消息,或者更直接地发送消息。

然后,只要标识符对于每个请求/会话/您的范围是唯一的,只需通过代理在地图中查找内容(就像MacWire范围一样,这里的“标识符”不是需要,因为它存储在ThreadLocal)。