Skip to content

Instantly share code, notes, and snippets.

@joescii
Last active March 8, 2016 14:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joescii/687ffe9090498a643ccf to your computer and use it in GitHub Desktop.
Save joescii/687ffe9090498a643ccf to your computer and use it in GitHub Desktop.
Scala dependency injection with traits
package com.joescii
package object code {
val DbThing = new DbThingImpl()
val HttpThing = new HttpThingImpl()
val BizThing = new BizThingImpl(DbThing, HttpThing)
trait DbThing {
def getDbStuff(params):Future[DbStuff]
}
trait HttpThing {
def getHttpStuff(params):Future[HttpStuff]
}
trait BizThing {
def doComplicatedStuff(params):Future[BizStuff]
}
private [code] class DbThingImpl extends DbThing {
override def getDbStuff(params):Future[DbStuff] = ???
}
private [code] class HttpThingImpl extends HttpThing {
override def getHttpStuff(params):Future[HttpStuff] = ???
}
private [code] class BizThingImpl(db:DbThing, http:HttpThing) extends BizThing {
override def doComplicatedStuff(params):Future[BizStuff] =
db.getDbStuff(params).zip(http.getHttpStuff(params)).map {
case (dbStuff, httpStuff) => ???
}
}
}
@joescii
Copy link
Author

joescii commented Feb 10, 2016

The tests for BizThing can be given fake DbThing and HttpThing. Anyone using this code (not inside package code) can only get the real ones without knowing what is needed.

@joescii
Copy link
Author

joescii commented Feb 10, 2016

(This is what happens when a Java dev discovers an immutable hammer and cool syntax)

@lucamolteni
Copy link

Something among these lines

import scala.concurrent.Future

case class DBStuff()
case class HTTPStuff()
case class Param()

def bizThingImpl(getDBStuff : Param => Future[DBStuff])
                (httpStuff: Param => Future[HTTPStuff]): Unit = {

  getDbStuff.zip(httpStuff(params)).map {
    case (dbStuff, httpStuff) => ???
  }
}

@joescii
Copy link
Author

joescii commented Feb 10, 2016

The caller of bizThingImpl would still need to know where to get db and http stuff. I felt it would be cleaner if the consumer didn't care.

Copy link

ghost commented Feb 10, 2016

This seems almost right. Next, rather than Future, maybe do:

trait BizThing {
    def toComplicatedStuff[M[_]: Monad](params: Params[M]): M[BizStuff]
}

In this case, you move the effect types that are available into the parameters - the parameters now including things like your DbThing[M] and your HttpThing[M].

However, you don't necessarily want to be passing DbThing and HttpThing as parameters explicitly everywhere you want to make that call, right? So, instead, you start doing things like this:

case class Env[M[_]](dbThing: DbThing[M], httpThing: HttpThing[M])

trait BizThing {
    def doComplicatedStuff[M[_]: Monad](params: WhateverYourNormalParamsWere): ReaderT[M, Env[M], BizStuff]
}

Copy link

ghost commented Feb 10, 2016

@lucamolteni what's with the Unit there? Surely you mean Future[BizStuff]. In any case, this is similar to the first step of my transformation; the ReaderT takes the responsibility for knowing where to get a DbThing and an HttpThing away from the caller. Of course, the ReaderT-ness continues to propagate outward - at the outermost level, something needs to actually run the ReaderT and pass in the Env - this is how ReaderT substitutes for dependency injection.

If some caller needs something else in the environment, they create their own MyEnv type that encapsulates the Env needed by their callees, and expresses their own needs to their caller, transitively.

Copy link

ghost commented Feb 10, 2016

This is essentially the same approach I described in http://logji.blogspot.com/2014/02/the-abstract-future.html, but that uses traits and the Cake pattern instead of ReaderT, and is consequently a bit more complicated, and initialization order can be a pain. I've come to the conclusion that ReaderT is a lot simpler to maintain.

@sleepynate
Copy link

Something as simple as changing lines 31-33 to something more like

  def f = (dbStuff:DbStuff, httpStuff: HttpStuff) => ???

  private [code] class BizThingImpl(db:DbThing, http:HttpThing) {
    override def doComplicatedStuff(params): Future[BizStuff] = {
      db.getDbStuff(params).zip(http.getHttpStuff(params)).map(f)
    }
  }

Will help to separate your business logic from your concurrency logic for independent pure testing. What you probably care about actually testing for is in f. This is what @mbbx6spp and I were getting at on Twitter. 💖

@ssanj
Copy link

ssanj commented Feb 10, 2016

@pellunutty should
case class Env[M[_]](dbThing: DbThing[M], httpThing: HttpThing[M]) be
case class Env[M[_]](dbThing: M[DbThing], httpThing: M[HttpThing]) ?

If not could you definite what DbThing and HttpThing would look like?

@joescii
Copy link
Author

joescii commented Feb 10, 2016

Thanks for all the tips! I feel like I'm actually in pretty good shape today, but with some cool ideas to try soon. I often hear about ReaderT btw. Maybe it's about time to dive into that one.

@ssanj
Copy link

ssanj commented Feb 10, 2016

I was going for something like what @sleepynate did. Just separate out the purecode (f) from the impure side-effecting code (BizThingImpl). That way you can unit test f easily and then integration test BizThingImpl with Wiremock etc.

@pellunutty's solution looks pretty nice. Because M is a Monad you could substitute Future with the Identity Monad for your tests (or any other Monad you want to).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment