-
-
Save cb372/542c8d46cd0ead6f880c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import scalaz.Reader | |
case class User(id: Int, name: String) | |
case class Interest(name: String) | |
trait Database | |
trait Attempt1 { | |
// How every explanation of Reader monad I've seen/read goes: | |
def userForId(id: Int): Reader[Database, User] | |
def interestsForUser(user: User): Reader[Database, Seq[Interest]] | |
def sharedInterests(idA: Int, idB: Int): Reader[Database, Seq[Interest]] = for { | |
userA <- userForId(idA) | |
userB <- userForId(idB) | |
interestsA <- interestsForUser(userA) | |
interestsB <- interestsForUser(userB) | |
} yield { | |
interestsA.filter(interestsB.contains) | |
} | |
// So, the database is dependency injected through the reader monad. Great. But, I want to test sharedInterests, | |
// and I don't want to require a database in the tests, because it shouldn't need it. The Reader monad has exposed | |
// the dependencies of userForId and interestsForUser, meaning testing anything that uses them requires a database, | |
// ie, you have all the boiler plate of a DI solution without the prime benefit - testing in isolation. | |
} | |
trait Attempt2 { | |
// So, this is how I might do it: | |
trait UserDb { | |
def userForId(id: Int): User | |
def interestsForUser(user: User): Seq[Interest] | |
} | |
def sharedInterests(idA: Int, idB: Int): Reader[UserDb, Seq[Interest]] = Reader { (userDb: UserDb) => | |
val userA = userDb.userForId(idA) | |
val userB = userDb.userForId(idB) | |
val interestsA = userDb.interestsForUser(userA) | |
val interestsB = userDb.interestsForUser(userB) | |
interestsA.filter(interestsB.contains) | |
} | |
// So, now I can test sharedInterests, by supplying mocked versions of userForId and interestsForUser | |
def test = { | |
val mockUserDb = new UserDb { | |
val sarah = User(1, "Sarah") | |
val john = User(2, "John") | |
val users = Map(1 -> sarah, 2 -> john) | |
val interests = Map(sarah -> Seq(Interest("movies"), Interest("sport")), john -> Seq(Interest("sport"), Interest("music"))) | |
def userForId(id: Int) = users(id) | |
def interestsForUser(user: User) = interests(user) | |
} | |
assert(sharedInterests(1, 2).run(mockUserDb) == Seq("sport")) | |
} | |
// So then, if I run my application: | |
class UserDbImpl(database: Database) extends UserDb { | |
def userForId(id: Int): User = ??? | |
def interestsForUser(user: User): Seq[Interest] = ??? | |
} | |
def run1(database: Database) = { | |
sharedInterests(1, 2).run(new UserDbImpl(database)) | |
} | |
// This is where I get confused. I've only used the Reader monad for dependency injection of my sharedInterests | |
// function. My UserDbImpl is not using the Reader monad for dependency injection, it's constructor dependency | |
// injected. So, if I'm using constructor dependency injection there, why not just use it for my sharedInterests | |
// function as well? | |
class SharedInterestsCalculator(userDb: UserDb) { | |
def sharedInterests(idA: Int, idB: Int): Seq[Interest] = { | |
val userA = userDb.userForId(idA) | |
val userB = userDb.userForId(idB) | |
val interestsA = userDb.interestsForUser(userA) | |
val interestsB = userDb.interestsForUser(userB) | |
interestsA.filter(interestsB.contains) | |
} | |
} | |
def run2(database: Database) = { | |
new SharedInterestsCalculator(new UserDbImpl(database)).sharedInterests(1, 2) | |
} | |
// What do I gain by using the reader monad, other than added complexity of introducing a monad for no apparent reason? | |
} | |
trait Attempt3 { | |
import scalaz._ | |
import Scalaz._ | |
import scala.language.higherKinds | |
//def userForId(id: Int): Reader[Database, User] | |
//def interestsForUser(user: User): Reader[Database, Seq[Interest]] | |
/* | |
* Abstract away from Reader monad -> remove dependency on Database. | |
* In production code, `userForIdF` and `interestsForUserF` will be `Reader[Database, ...]`. | |
* In tests they can be whatever kind of monad you like. | |
*/ | |
def sharedInterests[M[_]: Monad]( | |
userForIdF: Int => M[User], | |
interestsForUserF: User => M[Seq[Interest]]) | |
(idA: Int, idB: Int): M[Seq[Interest]] = { | |
for { | |
userA <- userForIdF(idA) | |
userB <- userForIdF(idB) | |
interestsA <- interestsForUserF(userA) | |
interestsB <- interestsForUserF(userB) | |
} yield { | |
interestsA.filter(interestsB.contains) | |
} | |
} | |
} | |
/* | |
* In your tests you can use any Monad, so it's easy to mock up your 2 Reader monads. | |
* No sign of a `Database` anywhere near your test. | |
*/ | |
object UnitTest extends App with Attempt3 { | |
import scalaz.Scalaz.Id | |
def userForId(id: Int): Id[User] = { | |
// TODO: mock impl here | |
User(id, "Chris") | |
} | |
def interestsForUser(user: User): Id[Seq[Interest]] = { | |
// TODO: mock impl here | |
Seq(Interest("swimming"), Interest("reading")) | |
} | |
val subjectUnderTest = sharedInterests(userForId, interestsForUser) _ | |
val result = subjectUnderTest(1, 2) | |
assert(result == Seq(Interest("swimming"), Interest("reading"))) | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment