2011-07-01 53 views
8

這比什麼都重要設計問題...使用Scala的case類作爲事實上的地圖

我真的很喜歡Scala的case類,並經常使用它們。然而,我發現我經常在Options(或者說,Lift的Boxes)中包裝我的參數,並設置默認值以允許靈活性並考慮到用戶可能並不總是指定所有參數。我認爲我採用了這種做法。

我的問題是,這是一個合理的方法?鑑於所有內容都是可選的,因此可以進行大量的樣板和檢查,以確定是否我不僅僅使用像Map[String, Any]這樣的案例類,並且懷疑我是否僅僅使用Map就更好。

讓我給你一個真實的例子。在這裏,我正在模擬匯款:

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) 

我覺得比較容易理解。在這個最簡單的,我們可能會宣佈一個Transaction像這樣:

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

我已經能想象你認爲這是冗長。如果我們指定的一切:

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

在另一方面,儘管有扔Full到處,你仍然可以做一些很好的模式匹配:

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 
} 

這是一個合理的做法?我會更好地使用Map?或者我應該使用案例類,但以不同的方式?也許使用整個層次的案例類來表示具有不同指定信息量的交易?

回答

4

使用案例類不如地圖靈活,因爲您只能分配/訪問預定義的字段。您需要事先構建完整的案例類層次結構。另一方面,case類提供了一種「編譯時驗證」,因爲所有類型都被明確定義(與Map[String,Any]不同),並且不能錯誤地分配/訪問非指定字段。案例類也應該更快,因爲您不需要遍歷地圖哈希表來查找您要查找的內容。

「冗長」問題來自案例類的不可改變的方面,但你會遇到與不可變映射完全相同的問題。該解決方案似乎是鏡頭。這裏有一個很好的談話:

http://www.youtube.com/watch?v=efv0SQNde5Q&list=PLEDE5BE0C69AF6CCE

+0

感謝鏈接到鏡頭談話,我現在正在聽它。但有沒有網頁的教程?我想了解更多! – pr1001

+0

從來沒有爲scala找到它們,但應該爲haskell而存在。 – paradigmatic

5

如果事情是真正可選的,那麼你真的沒有其他選擇。 null不是一個選項(沒有雙關語意)。

我會強烈建議不要使用Lift的盒子類型,除非您需要它專門處理Lift API。你只是引入了一個不必要的依賴。

我還會認真考慮一下,如果沒有指定貨幣的話Amount是否合理。如果有效,然後創建一個專用的「空對象」來表示未指定的貨幣會給你一個更簡潔的API:

class LocalCurrency extends Currency 

或者:

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

對於TransactionSide子類,我覺得奇怪的是,你可以指定CurrencyAmount分開(它已經嵌入了貨幣的概念)。我贊成:

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

最後...

是,利用地圖,如果他們與您的域吻合,他們會作出更清潔的代碼。

+0

謝謝,你的建議聽起來非常合理。我有第二種貨幣的原因是我可能會解析字符串,如'$ 100到DE'或'100 USD到DE'。 'LocalizedAmount'和'RawAmount'可能會有幫助,但我不確定。現在我用提取器匹配字符串:'msg.split(「」).toList match {case Amount(amount_in):: Currency(currency_in)::「to」:: Country(country_out):: Nil => ...}'。我很喜歡這個,不知道我是否可以避免第二個貨幣。 – pr1001

+0

至於Lift's Box,我很喜歡'Failure',這個代碼是在Lift應用程序的上下文中反正...... – pr1001

+0

作爲Box的替代品,可以考慮'Either [Exception,Option [T]]'。優點是可以將左投影(例如'myEither.left map {_.getMessage}')映射爲一個'Either [String,Option [T]]' - 適合將錯誤消息顯示給用戶。 –