Chris W.
Chris W.

Reputation: 2386

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

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())

Upvotes: 0

Views: 355

Answers (1)

Levi Ramsey
Levi Ramsey

Reputation: 20611

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)()

Upvotes: 2

Related Questions