【问题标题】:Scala dependency injection: alternatives to implicit parametersScala 依赖注入:隐式参数的替代方案
【发布时间】:2012-01-15 20:34:42
【问题描述】:

请原谅这个问题的长度。

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

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 被不同的用户调用时,都需要不同的上下文。所以上下文不能被烘焙到接收对象中,你也不想以任何方式合并两个层(如蛋糕模式)。我也不想依赖像 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 中按上下文传递信息的方法,它不会污染干预方法签名,不会将上下文静态地烘焙到接收器中,并且仍然是类型安全的?

【问题讨论】:

    标签: scala dependency-injection implicits


    【解决方案1】:

    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 确实“污染”了您的签名,因为它们的类型必须更改,并且通常一元编程可能会导致对您的代码进行大量重组,如果性能或内存是一个问题,所有闭包的额外对象分配可能不会很好。

    这些技术都不会自动共享参与者消息发送的上下文。

    【讨论】:

    • 标准 Scala 库中隐藏的宝石令人惊叹。我从没听说过DynamicVariable
    • 嗨,马库斯!请重新考虑使用动态。重新考虑您要实现的目标。
    • 您能解释一下您的反对意见吗?你是说DynamicVariable 的实现有问题,还是整个想法有问题?我认为我的问题很清楚我在寻找什么——甚至可能过于冗长。网络上的共识似乎是以DynamicVariable的代码为起点,而不是简单地接受它;它并没有做我想要的一切(我需要找出一个解决方案来与演员跨越线程边界)。但它非常接近,只要稍微小心地包装代码,就可以使代码既是类型安全的又是空值安全的。
    • 从我有限的阅读和更有限的理解来看,reader monad 似乎有些不同:它在计算中单独处理各种环境因素,但没有得到那些首先将值放在正确的区域。我确实想到 DynamicVariable 应该被视为 monad - 也许添加 toOption 方法将是它所需要的全部 - 而不是理所当然地认为它的地位。
    • reader monad 基本上是一个巧妙的技巧,可以隐藏传递上下文的管道。或者,另一种看待方式是 reader monad 是 state monad 的只读一半,这本身就是一个巧妙的技巧,可以隐藏传递不可变值的管道,这些值可以在传递之前“更改”(转换为新值)将它们转移到其他功能上。
    【解决方案2】:

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

    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的博文(无耻的自我宣传;))

    【讨论】:

      【解决方案3】:

      我认为来自 lift 的依赖注入可以满足您的需求。使用 doWith() 方法的详细信息请参见wiki

      请注意,您可以将其用作单独的库,即使您没有运行 Lift。

      【讨论】:

      • 这肯定比我见过的任何东西都近。我会看看我是否可以让它做我需要的。我没有看到任何可以解决线程/参与者问题的内容,但我可能还没有完全理解它。
      【解决方案4】:

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

      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 具有正确的词法范围,因此当您使用future 和actor 等时不会出现有趣的行为。我怀疑如果您使用新的 AnyVal,您甚至可以消除分配 Contextual 对象的开销。

      如果您想使用traits 将您的内容拆分为多个文件,您只需要每个文件一个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 行文件上额外增加 2 行样板文件确实不是那么糟糕。

      编辑:

      这也适用于异步的东西

      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,并挂钩到ExecutionContextprepare()execute() 方法以提取value 并正确设置在执行 Future 之前先向上 DynamicVariable

      然后您需要一个特殊的scala.collection.parallel.TaskSupport 来执行类似的操作,以使并行集合正常工作。还有一个特殊的akka.actor.Props,以便为那个做类似的事情。

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

      (我认为 Futures、collections.parallelProps 算作“中间的层不是我的代码”)

      【讨论】:

      • 我不确定这是否解决了中间有层不是我的代码的情况?
      • 编辑回答我认为你的问题是什么
      【解决方案5】:

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

      MacWire 也有作用域(非常可定制,提供了ThreadLocal 实现)。但是,我认为您不能使用库在参与者调用之间传播上下文 - 您需要携带一些标识符。这可以是例如通过用于发送参与者消息的包装器,或者更直接地与消息一起发送。

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-12-29
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多