How to combine Javas Builder pattern with Scala case class with optional fields?

239 Views Asked by At

In Scala you often use Java APIs which use the Builder pattern, e.g Google Maps Java Client (used in the example below).

I use a Scala case class in our app to gather all values that I will use then for the Builder pattern of the Java API. I design optional values of the Builder API, idiomatically with Scalas Option type. However I scratch my head how to do this nicer than this example:

case class WlanHint(
    mac: String,
    signalStrength: Option[Int],
    age: Option[Int],
    channel: Option[Int],
    signalToNoiseRatio: Option[Int]
)
val builder = WifiAccessPoint.WifiAccessPointBuilder().MacAddress(wlanHint.mac)
val b       = wlanHint.signalStrength.map(signalStrength => builder.SignalStrength(signalStrength)).getOrElse(builder)
val b2      = wlanHint.age.map(age => b.Age(age)).getOrElse(b)
val b3      = wlanHint.channel.map(channel => b2.Channel(channel)).getOrElse(b2)
val b4      = wlanHint.signalToNoiseRatio.map(signalToNoiseRatio => b3.SignalToNoiseRatio(signalToNoiseRatio)).getOrElse(b3)
// TODO is there a better way to do this above?
b4.createWifiAccessPoint())
1

There are 1 best solutions below

3
Levi Ramsey On

You can hide the builder calls within the case class as methods and also (since a Java builder is typically mutable) use the foreach method on Option:

case class WlanHint(
  mac: String,
  signalStrength: Option[Int],
  age: Option[Int],
  channel: Option[Int],
  signalToNoiseRatio: Option[Int]
) {
  def asWifiAccessPoint: WifiAccessPoint = { // guessing about return type...
    val builder = WifiAccessPoint.WifiAccessPointBuilder().MacAddress(mac)
    
    // The ()s may not be needed if not warning about a discarded value...
    signalStrength.foreach { ss => builder.SignalStrength(ss); () }
    age.foreach { a => builder.Age(a); () }
    channel.foreach { c => builder.Channel(c); () }
    signalToNoiseRatio.foreach { snr => builder.SignalToNoiseRatio(snr); () }

    builder.createWifiAccessPoint()
  }
}

The first foreach in this example ends up being effectively the same as:

if (signalStrength.isDefined) {
  builder.SignalStrength(signalStrength.get)
}

If WlanHint is only intended to reify the arguments to the builder, it might make sense to have it be a Function0[WifiAccessPoint]:

case class WlanHint(
  mac: String,
  signalStrength: Option[Int],
  age: Option[Int],
  channel: Option[Int],
  signalToNoiseRatio: Option[Int]
) extends Function0[WifiAccessPoint] {
  def apply(): WifiAccessPoint = {
    // same body as asWifiAccessPoint in previous snippet
  }
}

This would then allow you to write:

val accessPoint = WifiAccessPoint("00:01:02:03:04:05", None, None, None, None)()