Scala dependency injection: alternatives to implicit parameters

[亡魂溺海] 提交于 2019-12-02 18:14:55
James Iry

The Scala standard library includes something like your hypothetical "usingContext" called DynamicVariable. This question has some information about it When we should use scala.util.DynamicVariable? . DynamicVariable does use a ThreadLocal under the hood so many of your issues with ThreadLocal will remain.

The reader monad is a functional alternative to explicitly passing an environment http://debasishg.blogspot.com/2010/12/case-study-of-cleaner-composition-of.html. The Reader monad can be found in Scalaz http://code.google.com/p/scalaz/. However, the ReaderMonad does "pollute" your signatures in that their types must change and in general monadic programming can cause a lot of restructuring to your code plus extra object allocations for all the closures may not sit well if performance or memory is a concern.

Neither of these techniques will automatically share a context over an actor message send.

A little late to the party, but have you considered using implicit parameters to your classes constructors?

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

If you wanted to have a new biz for each call to f() you could let the implicit parameter be a function returning a new biz:

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

Now you simply need to provide the context when constructing Foo. Which you can do like this:

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()
}

Note that the implicit biz method will not be visible in UI. So we basically hide away those details :)

I wrote a blog post about using implicit parameters for dependency injection which can be found here (shameless self promotion ;) )

I think that the dependency injection from lift does what you want. See the wiki for details using the doWith () method.

Note that you can use it as a separate library, even if you are not running lift.

You asked this just about a year ago, but here's another possibility. If you only ever need to call one method:

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)

If you need all the methods to be externally visible:

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)

This is basically the cake pattern, except that if all your stuff fits into a single file, you don't need all the messy trait stuff to combine it into one object: you can just nest them. Doing it this way also makes cx properly lexically scoped, so you don't end up with funny behavior when you use futures and actors and such. I suspect that if you use the new AnyVal, you could even do away with the overhead of allocating the Contextual object.

If you want to split your stuff into multiple files using traits, you only really need a single trait per file to hold everything and put the MyContextType properly in scope, if you don't need the fancy replaceable-components-via-inheritance thing most cake pattern examples have.

// 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)

It looks kinda messy in a small example, but remember, you only need to split it off into a new trait if the code is getting too big to sit comfortable in one file. By that point your files are reasonably big, so an extra 2 lines of boilerplate on a 200-500 line file is not so bad really.

EDIT:

This works with asynchronous stuff too

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)

It Just Works using nesting. I'd be impressed if you could get similar functionality working with DynamicVariable.

You'd need a special subclass of Future that stores the current DynamicVariable.value when created, and hook into the ExecutionContext's prepare() or execute() method to extract the value and properly set up the DynamicVariable before executing the Future.

Then you'd need a special scala.collection.parallel.TaskSupport to do something similar in order to get parallel collections working. And a special akka.actor.Props in order to do something similar for that.

Every time there's a new mechanism of creating asynchronous tasks, DynamicVariable based implementations will break and you'll have weird bugs where you end up pulling up the wrong Context. Every time you add a new DynamicVariable to keep track of, you'll need to patch all your special executors to properly set/unset this new DynamicVariable. Using nesting you can just let lexical closure take care of all of this for you.

(I think Futures, collections.parallel and Props count as "layers in between that aren't my code")

Similar to the implicit approach, with Scala Macros you can do auto-wiring of objects using constructors - see my MacWire project (and excuse the self-promotion).

MacWire also has scopes (quite customisable, a ThreadLocal implementation is provided). However, I don't think you can propagate context across actor calls with a library - you need to carry some identifier around. This can be e.g. through a wrapper for sending actor messages, or more directly with the message.

Then as long as the identifier is unique per request/session/whatever your scope is, it's just a matter of looking things up in a map via a proxy (like the MacWire scopes do, the "identifier" here isn't needed as it is stored in the ThreadLocal).

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!