Search code examples
scalaextension-methodsfluent-interfacebuilder-pattern

What is the difference between an extension method, the builder pattern and the fluent interface?


In Scala - we can do extension methods like this:

object MyExtensions {
  implicit class RichInt(val i: Int) extends AnyVal {
    def square = i * i
  }
}

We can use it like this:

import MyExtensions._
Int i = 2;
Val squared = i.square()
Val cubed = i.square().square()

We can do the builder pattern like this:

sealed abstract class Preparation  
case object Neat extends Preparation
case object OnTheRocks extends Preparation
case object WithWater 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, val isDouble:Boolean, val glass:Option[Glass])

class ScotchBuilder {
  private var theBrand:Option[String] = None
  private var theMode:Option[Preparation] = None
  private var theDoubleStatus:Option[Boolean] = None
  private var theGlass:Option[Glass] = None

  def withBrand(b:Brand) = {theBrand = Some(b); this} /* returning this to enable method chaining. */
  def withMode(p:Preparation) = {theMode = Some(p); this}
  def isDouble(b:Boolean) = {theDoubleStatus = some(b); this}
  def withGlass(g:Glass) = {theGlass = Some(g); this}

  def build() = new OrderOfScotch(theBrand.get, theMode.get, theDoubleStatus.get, theGlass);
}



object BuilderPattern {
  class ScotchBuilder(theBrand:Option[String], theMode:Option[Preparation], theDoubleStatus:Option[Boolean], theGlass:Option[Glass]) {
    def withBrand(b:String) = new ScotchBuilder(Some(b), theMode, theDoubleStatus, theGlass)
    def withMode(p:Preparation) = new ScotchBuilder(theBrand, Some(p), theDoubleStatus, theGlass)
    def isDouble(b:Boolean) = new ScotchBuilder(theBrand, theMode, Some(b), theGlass)
    def withGlass(g:Glass) = new ScotchBuilder(theBrand, theMode, theDoubleStatus, Some(g))

    def build() = new OrderOfScotch(theBrand.get, theMode.get, theDoubleStatus.get, theGlass);
  }

  def builder = new ScotchBuilder(None, None, None, None)
}

We can use it like this:

import BuilderPattern._

val order =  builder withBrand("Takes") isDouble(true) withGlass(Tall)  withMode(OnTheRocks) build()

We can do the fluent interface like this:

class Person {
    protected var fname = ""
    protected var lname = ""
    def setFirstName(firstName: String): this.type = {
        fname = firstName
        this
    }
    def setLastName(lastName: String): this.type = {
        lname = lastName
        this
    }
}

class Employee extends Person {
  protected var role = ""
  def setRole(role: String): this.type = {
      this.role = role
      this
  }
  override def toString = {
      "%s, %s, %s".format(fname, lname, role)
  }
}

We can use it like this:

object Main extends App {
    val employee = new Employee
    // use the fluent methods
    employee.setFirstName("Al")
            .setLastName("Alexander")
            .setRole("Developer")
    println(employee)
}

All three give similar interfaces for an internal DSL.

My question is: What is the difference between an extension method, the builder pattern and the fluent interface?


Solution

  • These are three completely separate concepts that do different things.

    An extension method allows you to add a method to a class that already exists. This can lead to a nicer API than creating a method that takes in an object of that class as a parameter.

    The builder pattern allows you to construct an object with many options and parameters by setting these parameters in a mutable object first, then calling a "build" method to initialize your (typically immutable) object you are creating. This helpful because it removes the need to a huge constructor with many arguments and is especially useful when some of those arguments are optional with defaults.

    A fluent API means that "setter" methods will return the object itself instead of returning Unit/void. This allows for an API where you can chain method calls together. for example using a fictional Point2d class

    val point = new Point2d().setX(3).setY(5)
    

    Is using a fluent API

    val point = new Point2d()
    point.setX(3)
    point.setY(5)
    

    is without a fluent API