请原谅这个问题的长度。
我经常需要在我的代码的一层创建一些上下文信息,并在其他地方使用该信息。我通常发现自己使用隐式参数:
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中上传信息而不污染干预方法签名的方法,不会静态地将上下文烘焙到接收器中,并且仍然是类型安全的吗?
答案 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,Future
和collections.parallel
计为“介于两者之间的不是我的代码的图层”)
答案 4 :(得分:1)
与隐式方法类似,使用Scala宏可以使用构造函数自动连接对象 - 请参阅我的MacWire项目(并原谅自我推销)。
MacWire也有范围(可定制,提供了ThreadLocal
实现)。但是,我不认为您可以使用库传播actor调用的上下文 - 您需要携带一些标识符。这可以是例如通过包装器发送演员消息,或者更直接地发送消息。
然后,只要标识符对于每个请求/会话/您的范围是唯一的,只需通过代理在地图中查找内容(就像MacWire范围一样,这里的“标识符”不是需要,因为它存储在ThreadLocal
)。