Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using Scala Case Classes as De-facto Maps

This is more a design question than anything else...

I really like Scala's case classes and use them often. However, I find that I'm often wrapping in my parameters in Options (or rather, Lift's Boxes) and setting default values to allow for flexibility and to account that a user might not always specify all the parameters. I think I adopted this practice from.

My question is, is this a reasonable approach? Given that everything may be optional, there can be a lot of boilerplate and checking, to the point whether I wonder whether I'm not just using my case classes like Map[String, Any] and wonder whether I wouldn't be better off just using a Map.

Let me give you a real example. Here I am modeling a money transfer:

case class Amount(amount: Double, currency: Box[Currency] = Empty)
trait TransactionSide
case class From(amount: Box[Amount] = Empty, currency: Box[Currency] = Empty, country: Box[Country] = Empty) extends TransactionSide
case class To(amount: Box[Amount] = Empty, currency: Box[Currency] = Empty, country: Box[Country] = Empty) extends TransactionSide
case class Transaction(from: From, to: To)

Relatively simple to understand, I think. At this simplest we might declare a Transaction like so:

val t = Transaction(From(amount=Full(Amount(100.0)), To(country=Full(US)))

Already I can imagine you think it's verbose. And if we specify everything:

val t2 = Transaction(From(Full(Amount(100.0, Full(EUR))), Full(EUR), Full(Netherlands)), To(Full(Amount(150.0, Full(USD))), Full(USD), Full(US)))

On the other hand, despite having to throw Full around everywhere, you can still do some nice pattern matching:

t2 match {
  case Transaction(From(Full(Amount(amount_from, Full(currency_from1))), Full(currency_from2), Full(country_from)), To(Full(Amount(amount_to, Full(currency_to1))), Full(currency_to2), Full(country_to))) if country_from == country_to => Failure("You're trying to transfer to the same country!")
  case Transaction(From(Full(Amount(amount_from, Full(currency_from1))), Full(currency_from2), Full(US)), To(Full(Amount(amount_to, Full(currency_to1))), Full(currency_to2), Full(North_Korea))) => Failure("Transfers from the US to North Korea are not allowed!")
  case Transaction(From(Full(Amount(amount_from, Full(currency_from1))), Full(currency_from2), Full(country_from)), To(Full(Amount(amount_to, Full(currency_to1))), Full(currency_to2), Full(country_to))) => Full([something])
  case _ => Empty
}

Is this a reasonable approach? Would I be better served by using a Map? Or should I use case classes but in a different fashion? Perhaps using a whole hierarchy of case classes to represent transactions with different amounts of information specified?

like image 349
pr1001 Avatar asked Jul 01 '11 11:07

pr1001


1 Answers

If something is genuinely optional, then you really have no other choice. null is not an option (no pun intended).

I would strongly advise against the uses of Lift's box type however, unless you need it for dealing specifically with Lift APIs. You're only introducing an unnecessary dependency.

I'd also give some serious thought as to whether it truly makes sense to have an Amount without a specified currency. If it is valid, then creating a dedicated "null object" to represent an unspecified currency would give you a cleaner API:

class LocalCurrency extends Currency

Alternatively:

sealed trait Amount
case class LocalisedAmount(value: Double, currency: Currency) extends Amount
case class RawAmount(value: Double) extends Amount

For the TransactionSide subclasses, I find it odd that you can specify Currency separately from Amount (which already embeds the notion of currency). I'd favour:

case class TxEnd(
    amount: Option[Amount] = None,
    country: Option[Country] = None)
case class Transaction(from: TxEnd, to: TxEnd)

Finally...

Yes, use maps if they fit well with your domain, they'll make for much cleaner code.

like image 147
Kevin Wright Avatar answered Sep 26 '22 04:09

Kevin Wright