質問

I have a lot of client code that build Map using the same keys (to query MongoDB).

My idea is to provide helper methods that hide keys.

First try, I have used default parameters (cf object Builder below) but the client hava to deal with Option

I now use a builder pattern (cf class Builder below)

Is there a better way ?

class Builder {
  val m = collection.mutable.Map[String, Int]()
  def withA(a: Int) = {m += (("a", a))}
  def withB(b: Int) = {m += (("b", b))}
  def withC(c: Int) = {m += (("c", c))}
  def build = m.toMap
}

object Builder {
  def build1(a: Option[Int] = None, b: Option[Int] = None, c: Option[Int] = None): Map[String, Int] = {
    val optPairs = List(a.map("a" -> _),
      b.map("b" -> _),
      c.map("c" -> _))
    val pairs = optPairs.flatten
    Map(pairs: _*)
  }
}

object Client {
  def main(args: Array[String]) {
    println(Builder.build1(b = Some(2)))

    println(new Builder().withB(2))
  }
}
役に立ちましたか?

解決

An easy solution to avoid having to deal with options when calling Builder.build1 is to define an implicit conversion to automatically wrap any value into an Some:

implicit def wrap[T]( x: T ) = Some( x )

And boom, you can omit the wrapping and directly do:

scala> Builder.build1( a = 123, c = 456 )
res1: Map[String,Int] = Map(a -> 123, c -> 456)

However, this is pretty dangerous given that options are pervasive and you don't want to pull such a general converion into scope. To fix this you can define your own "option" class that you'll use just for the purpose of defining those optional parameters:

abstract sealed class OptionalArg[+T] {
  def toOption: Option[T]
}
object OptionalArg{
  implicit def autoWrap[T]( value: T  ): OptionalArg[T] = SomeArg(value)
  implicit def toOption[T]( arg: OptionalArg[T] ): Option[T] = arg.toOption
}
case class SomeArg[+T]( value: T ) extends OptionalArg[T] {
  def toOption = Some( value )
}
case object NoArg extends OptionalArg[Nothing] {
  val toOption = None
}

You can then redefine Build.build1 as:

def build1(a: OptionalArg[Int] = NoArg, b: OptionalArg[Int] = NoArg, c: OptionalArg[Int] = NoArg): Map[String, Int]

And then once again, you can directly call Build.build1 without explicitely wrapping the argument with Some:

scala> Builder.build1( a = 123, c = 456 )
res1: Map[String,Int] = Map(a -> 123, c -> 456)

With the notable difference that now we are not pulling anymore a dangerously broad conversion into cope.


UPDATE: In response to the comment below "to go further in my need, arg can be a single value or a list, and I have awful Some(List(sth)) in my client code today"

You can add another conversion to wrap individual parameters into one element list:

implicit def autoWrapAsList[T]( value: T  ): OptionalArg[List[T]] = SomeArg(List(value))

Then say that your method expects an optional list like this:

def build1(a: OptionalArg[List[Int]] = NoArg, b: OptionalArg[Int] = NoArg, c: OptionalArg[Int] = NoArg): Map[String, Int] = {
  val optPairs = List(a.map("a" -> _.sum),
    b.map("b" -> _),
    c.map("c" -> _))
  val pairs = optPairs.flatten
  Map(pairs: _*)
}

You can now either pass an individual element or a list (or just like before, no argument at all):

scala> Builder.build1( a = 123, c = 456 )
res6: Map[String,Int] = Map(a -> 123, c -> 456)

scala> Builder.build1( a = List(1,2,3), c = 456 )
res7: Map[String,Int] = Map(a -> 6, c -> 456)

scala> Builder.build1( c = 456 )
res8: Map[String,Int] = Map(c -> 456)

One last warning: even though we have defined our very own "option" class, it is still true that you should always use implicit conversions with some care, so take some time to balance whether the convenience is worth the risk in your use case.

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top