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:
[code]
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)
}
[/code]
Using the above you can write something like:
[code]
val scotchOrder1 = scotch("Glenfoobar") prepare WithExtraWater inGlass Tulip
val scotchOrder2 = scotch("Talisker") inGlass Tall prepare Neat
[/code]
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:
[code]
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]
}
}
}
}
[/code]
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!
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.