ScotchBuilder with Scala 2.8
While writing my internal SQL DSL example I stumbled upon this blogpost about a Type-safe Builder Pattern in Scala a couple of times. One of the issues I have with that code is the unnecessary use of mutable state in the example. Which are probably there to demonstrate the validation of the result at the end of the article.
The blogpost concludes with some complex implicit solution to have the builder only create valid configurations, but (IMHO) fails to address the fact that it is still possible to create OrderOfScotch variants which are invalid through its' constructor.
With Scala 2.8 it is possible to vastly simplify the example by using the the copy method on the case classes and default arguments:
-
sealed abstract class Preparation
-
case object Neat extends Preparation
-
case object OnTheRocks extends Preparation
-
case object WithExtraWater extends Preparation
-
-
sealed abstract class Glass
-
case object Short extends Glass
-
case object Tall extends Glass
-
case object Tulip extends Glass
-
-
case class OrderOfScotch(val brand:String , val mode:Preparation = Neat, val double:Boolean = false, val glass:Option[Glass] = None) {
-
val invalid: Boolean = double && glass.isDefined && glass.get == Short
-
if(invalid) {
-
throw new IllegalStateException("Illegal combination")
-
}
-
-
def prepare(p:Preparation) = copy(mode = p)
-
def isDouble(d:Boolean) = copy(double = d)
-
def inGlass(g:Glass) = copy(glass = Option(g))
-
}
-
-
object OrderBuilder {
-
def scotch(brand:String) = OrderOfScotch(brand)
-
}
Using the above you can write something like:
-
val scotchOrder1 = scotch("Glenfoobar") prepare WithExtraWater inGlass Tulip
-
val scotchOrder2 = scotch("Talisker") inGlass Tall prepare Neat
When using an incorrect configuration (in the example implementation a double scotch, in a short glass) creating the Order throws an exception. And since we in effect only use the constructor to create the Order this works in any case.
Consider the following tests:
-
class ScotchBuilderSpec extends Spec with ShouldMatchers {
-
import OrderBuilder._
-
describe("given two scotches with the same configuration") {
-
val constructorResult = OrderOfScotch("Glenfoobar", WithExtraWater, false, Option(Tulip))
-
val builderResult = scotch("Glenfoobar") prepare WithExtraWater inGlass Tulip
-
-
describe("when created by constructor and by the builder") {
-
it("they should return identical results") {
-
constructorResult should be (builderResult)
-
println("success!")
-
}
-
}
-
}
-
-
describe("trying to create an illegal order") {
-
describe("like combining a double portion with a short glass") {
-
it("should throw an exception, for now") {
-
evaluating { OrderOfScotch("Glenfoobar", Neat, true, Option(Short)) } should produce[Throwable]
-
evaluating { scotch("Glenfoobar") isDouble(true) inGlass Short } should produce[Throwable]
-
}
-
}
-
}
-
}
As you can see the amount of boilerplate code was reduced dramatically, we've got rid of all mutable state and moved validation of the Order to a (IMHO) better location!
One 'drawback' of my solution is of course that it only works on runtime; whereas the solution presented in the catches incorrect configurations created by the builder at compiletime... which was the entire point of that blogpost. I'll try and see if I can factor that into a more compact form as well lator on!
2 comments2 Comments so far
Leave a reply
Another drawback of this solution is that problems without a good set of default values can not be expressed this way due to the validation check in the constructor.
BTW, I think the IllegalStateException should be an IllegalArgumentException
You are right. Both cases. I will need to revise my refactor; I simplified the validation part way to much.