Sunday, June 10, 2012

Cake Pattern Practices

Intro

Cake Pattern is a Scala-specific solution to Dependency Injection problem. In Java, from where Scala has inherited a lot, DI problem is solved by either passing environment as a parameter (Env monad), or using specific ad-hoc tools, like Guice. Scala is a much more powerful language, and Cake Pattern is available here. In my opinion, Cake Pattern is the best. It is similar to passing parameters, but differently. The first publication was by Jonas Bonér in 2008, Real-World Scala: Dependency Injection (DI) I personally found the article kind of hard to read; hope this one is easier. The pattern itself is easy.

Real World Example

We have an application which depends on configuration and credential files, and talks to a server via some proprietary API. We want to work on this application. By working I mean freely refactoring it from its initial prototype state to a well-written piece of code, enriched with logging, statistics, status reporting, etc., and keep it alive, that is, modifiable. One cannot seriously do it using real world connections; tests take minutes to run, connections are brittle etc. So we want to be able to run logic without the need to connect to anything. And this includes also System.out output and System.exit() calls. How do we do it? Let's start with the "real world" part.

Abstracting The File System

The full source code can be found on github, here is a small piece.
import io.{Source => ioS}
import java.io.{PrintWriter, File}

trait FS {

  implicit def file(path: String)                       = new File(path)
  implicit def asFile(file: TextFile)                   = file.file
  implicit def textFile(file: File):       TextFile     = TextFile(file)
  implicit def textFile(path: String):     TextFile     = TextFile(file(path))
  def tempFile(prefix: String)                          = TextFile(File.createTempFile(prefix, "tmp"))

  def exists(file: File): Boolean = file.exists

  protected class Entry(val file: File) {
    lazy val canonicalFile = file.getCanonicalFile.getAbsoluteFile
    def parent = Folder(canonicalFile.getParentFile)
    def delete = file.delete
    override def toString = canonicalFile.toString
  }

  case class Folder(path: File) extends Entry(path) {
    def file(path: String): TextFile = TextFile((canonicalFile /: (path split "/")) (new File(_, _)))

  class TextFile(file: File) extends Entry(file) {
    def text = ioS.fromFile(file).mkString

    def text_=(s: String) {
      val out = new PrintWriter(file, "UTF-8")
      try{ out.print(s) } finally{ out.close }
    }
  }
}

object FS extends FS
To use it in your application, instead of "manually" finding/opening/closing files, one just has to write
import FS._
somewhere in the client code.

Mocking the File System

To avoid creating multiple files just to make sure the code works in various circumstances, we better mock the file system. Here's an example:
val mockFS = new FS {
    class MockFile(path: String, content: String) extends TextFile(new File(path)) {
      override def text = content
    }
    val files = new scala.collection.mutable.HashMap[String, MockFile]
    def add(file: MockFile) { files.put(file.getPath, file)}
    def file(pair: (String, String)) { add(new MockFile(pair._1, pair._2)) }

    file("badcredo.txt"           -> "once upon a midnight dreary while I pondered weak and weary")
    file("credoNoUser.txt"        -> """
    password = "Guess me if you can!"
    realm = fb
    """)
    file("credoBadUser.txt"        -> """
    password = "Guess me if you can!"
    realm = fb
    username=
    """)
    file("credoBadPassword.txt"        -> """
    password =
    realm = fb
    username=badpwd
    """)

    override def exists(file: File) = true // egg or chicken problem
    override def textFile(path: String) = files.getOrElse(path, throw new FileNotFoundException("what is this " + path + "?"))
  }
We can throw in tons of such cases, covering all the situations, which is less probable if you have to deal with dozens of actual files. Now good, but how can we replace a file system with this mock file system? This is how.

Eating The Cake

trait MyApp {
  val FileSystem
  /*...*/
  def login(cred: String) {
    val (userid, password, realm) = parse(FileSystem.textFile(cred).text)
    /*...*/
}

object MyApp {
  val FileSystem = FS
}
Here
MyApp
is a trait, with an accompanying object. In production circumstances we use the accompanying object, which instantiates FileSystem as FS, the object representing a regular file system. In the trait, FileSystem is represented as an abstract value; so if we extend MyApp in our test suite, we can pass our test file system:
val SUT extends MyApp {
  val FileSystem = mockFS
}
Now we can manipulate the app to our pleasure.

Intercepting Everything

In addition, in our test suite we can define this:
  trait Testable {
    val console = new StringWriter
    val out = new PrintWriter(console)
    def printed = console.toString
    class Exit(code: Int) extends Exception(code)
    def print(s: String) = out.print(s)
    def println(s: String) = out.println(s)
    def exit(code: Int) = throw new Exit(code);
And then write
val SUT extends MyApp with Testable {
  val FileSystem = mockFS
}
Provided that we did define the default implementations of println, exit etc, we are now in control of the application.

Rinse, Repeat

But what if MyApp uses another piece of code that talks to an external service? We have to do some instrumentation in that other piece of code, using the same cake pattern; the default implementation would be coming from the accompanying object, but in the case of test suite we can instantiate with something completely different, and pass it along to MyApp.

Questions?

2 comments:

  1. You seem to just customize some strategy/environment via extending a class and overriding a val, all this instead of passing the same mockFS as a constructor parameter. Is this so much a difference? Or am I missing something completely obvious and great?

    BTW how would Scala compiler infer the type of "val FileSystem" in trait MyApp?

    ReplyDelete
  2. Peter, yes, passing a parameter is one of the solutions; but having an abstract member seems to be more light-weight, since you do not have to list all your dependences; more, the abstract member solution could be mixed in via a trait ("inheritance-composition pattern), which I did not cover here.
    Say, we define trait WithFS { val fs = FS } and trait WithMockFS { val fs = MockFS }

    ReplyDelete