问题
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))
}
}
回答1:
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.
来源:https://stackoverflow.com/questions/21603743/how-to-provide-helper-methods-to-build-a-map