What is the Scala equivalent to a Java builder pattern?

JavaDesign PatternsScala

Java Problem Overview


In the work that I do on a day to day in Java, I use builders quite a lot for fluent interfaces, e.g.: new PizzaBuilder(Size.Large).onTopOf(Base.Cheesy).with(Ingredient.Ham).build();

With a quick-and-dirty Java approach, each method call mutates the builder instance and returns this. Immutably, it involves more typing, cloning the builder first before modifying it. The build method eventually does the heavy lifting over the builder state.

What's a nice way of achieving the same in Scala?

If I wanted to ensure that onTopOf(base:Base) was called only once, and then subsequently only with(ingredient:Ingredient) and build():Pizza could be called, a-la a directed builder, how would I go about approaching this?

Java Solutions


Solution 1 - Java

Another alternative to the Builder pattern in Scala 2.8 is to use immutable case classes with default arguments and named parameters. Its a little different but the effect is smart defaults, all values specified and things only specified once with syntax checking...

The following uses Strings for the values for brevity/speed...

scala> case class Pizza(ingredients: Traversable[String], base: String = "Normal", topping: String = "Mozzarella")
defined class Pizza

scala> val p1 = Pizza(Seq("Ham", "Mushroom"))                                                                     
p1: Pizza = Pizza(List(Ham, Mushroom),Normal,Mozzarella)

scala> val p2 = Pizza(Seq("Mushroom"), topping = "Edam")                               
p2: Pizza = Pizza(List(Mushroom),Normal,Edam)

scala> val p3 = Pizza(Seq("Ham", "Pineapple"), topping = "Edam", base = "Small")       
p3: Pizza = Pizza(List(Ham, Pineapple),Small,Edam)

You can then also use existing immutable instances as kinda builders too...

scala> val lp2 = p3.copy(base = "Large")
lp2: Pizza = Pizza(List(Ham, Pineapple),Large,Edam)

Solution 2 - Java

You have three main alternatives here.

  1. Use the same pattern as in Java, classes and all.

  2. Use named and default arguments and a copy method. Case classes already provide this for you, but here's an example that is not a case class, just so you can understand it better.

     object Size {
         sealed abstract class Type
         object Large extends Type
     }
    
     object Base {
         sealed abstract class Type
         object Cheesy extends Type
     }
    
     object Ingredient {
         sealed abstract class Type
         object Ham extends Type
     }
    
     class Pizza(size: Size.Type, 
                 base: Base.Type, 
                 ingredients: List[Ingredient.Type])
    
     class PizzaBuilder(size: Size.Type, 
                        base: Base.Type = null, 
                        ingredients: List[Ingredient.Type] = Nil) {
    
         // A generic copy method
         def copy(size: Size.Type = this.size,
                  base: Base.Type = this.base,
                  ingredients: List[Ingredient.Type] = this.ingredients) = 
             new PizzaBuilder(size, base, ingredients)
    
    
         // An onTopOf method based on copy
         def onTopOf(base: Base.Type) = copy(base = base)
    
    
         // A with method based on copy, with `` because with is a keyword in Scala
         def `with`(ingredient: Ingredient.Type) = copy(ingredients = ingredient :: ingredients)
    
    
         // A build method to create the Pizza
         def build() = {
             if (size == null || base == null || ingredients == Nil) error("Missing stuff")
             else new Pizza(size, base, ingredients)
         }
     }
    
     // Possible ways of using it:
     new PizzaBuilder(Size.Large).onTopOf(Base.Cheesy).`with`(Ingredient.Ham).build();
     // or
     new PizzaBuilder(Size.Large).copy(base = Base.Cheesy).copy(ingredients = List(Ingredient.Ham)).build()
     // or
     new PizzaBuilder(size = Size.Large, 
                      base = Base.Cheesy, 
                      ingredients = Ingredient.Ham :: Nil).build()
     // or even forgo the Builder altogether and just 
     // use named and default parameters on Pizza itself
    
  3. Use a type safe builder pattern. The best introduction I know of is this blog, which also contains references to many other articles on the subject.

Basically, a type safe builder pattern guarantees at compile time that all required components are provided. One can even guarantee mutual exclusion of options or arity. The cost is the complexity of the builder code, but...

Solution 3 - Java

Case classes solve the problem as shown in previous answers, but the resulting api is difficult to use from java when You have scala collections in your objects. To provide a fluent api to java users try this:

case class SEEConfiguration(parameters : Set[Parameter],
                               plugins : Set[PlugIn])

case class Parameter(name: String, value:String)
case class PlugIn(id: String)

trait SEEConfigurationGrammar {

  def withParameter(name: String, value:String) : SEEConfigurationGrammar

  def withParameter(toAdd : Parameter) : SEEConfigurationGrammar

  def withPlugin(toAdd : PlugIn) : SEEConfigurationGrammar

  def build : SEEConfiguration

}

object SEEConfigurationBuilder {
  def empty : SEEConfigurationGrammar = SEEConfigurationBuilder(Set.empty,Set.empty)
}


case class SEEConfigurationBuilder(
                               parameters : Set[Parameter],
                               plugins : Set[PlugIn]
                               ) extends SEEConfigurationGrammar {
  val config : SEEConfiguration = SEEConfiguration(parameters,plugins)

  def withParameter(name: String, value:String) = withParameter(Parameter(name,value))

  def withParameter(toAdd : Parameter) = new SEEConfigurationBuilder(parameters + toAdd, plugins)

  def withPlugin(toAdd : PlugIn) = new SEEConfigurationBuilder(parameters , plugins + toAdd)

  def build = config

}

Then in java code the api is really easy to use

SEEConfigurationGrammar builder = SEEConfigurationBuilder.empty();
SEEConfiguration configuration = builder
    .withParameter(new Parameter("name","value"))
    .withParameter("directGivenName","Value")
    .withPlugin(new PlugIn("pluginid"))
    .build();

Solution 4 - Java

It's the same exact pattern. Scala allows for mutation and side effects. That said, if you'd like to be more of a purest, have each method return a new instance of the object that you're constructing with the element(s) changed. You could even put the functions within the Object of a class so that there's a higher level of separation within your code.

class Pizza(size:SizeType, layers:List[Layers], toppings:List[Toppings]){
    def Pizza(size:SizeType) = this(size, List[Layers](), List[Toppings]())

object Pizza{
    def onTopOf( layer:Layer ) = new Pizza(size, layers :+ layer, toppings)
    def withTopping( topping:Topping ) = new Pizza(size, layers, toppings :+ topping)
}

so that your code might look like

val myPizza = new Pizza(Large) onTopOf(MarinaraSauce) onTopOf(Cheese) withTopping(Ham) withTopping(Pineapple)

(Note: I've probably screwed up some syntax here.)

Solution 5 - Java

using Scala partial applies are feasible if you are building a smallish object that you don't need to pass over method signatures. If any of those assumptions don't apply, I recommend using a mutable builder to build an immutable object. With this being scala you could implement the builder pattern with a case class for the object to build with a companion as the builder.

Given that the end result is a constructed immutable object I don't see that it defeats any of the Scala principles.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJakub KorabView Question on Stackoverflow
Solution 1 - JavaJames StrachanView Answer on Stackoverflow
Solution 2 - JavaDaniel C. SobralView Answer on Stackoverflow
Solution 3 - JavaEIIPIIView Answer on Stackoverflow
Solution 4 - JavawheatiesView Answer on Stackoverflow
Solution 5 - JavaAndrew NormanView Answer on Stackoverflow