I was considering the excerise and reworked it into that an account holds a bag of properties. The simplest approach is to use a map:
case class Account(props: Map<String, Any>)
However, this can be improved somewhat by introducing typed properties:
case class PropertyBag(properties: Map[String, Any]) {
def get[A](property: Property[A]) : PropertyGetResult[A] = {
val name = property.name
properties.get(name) match {
case Some(v: A) => PropertyGetResult.HasValue[A](v)
case Some(v) => PropertyGetResult.TypeMismatch[A](name, v.getClass)
case None => PropertyGetResult.MissingValue[A](name)
}
}
def set[A](property: Property[A], v: A): PropertyBag = {
val name = property.name
new PropertyBag(properties.updated (name, v))
}
}
case class Property[+A](name: String, defaultValue: A)
This allows us to create a model of type properties:
object Properties {
object CreditCard {
val number = property("P_CC__NUMBER" , "" )
val expireDate = property("P_CC__EXPIRE_DATE", Instant.MIN )
val name = property("P_CC__NAME" , "" )
val cvc = property("P_CC__CVC" , "" )
}
}
A problem with this approach is how do we handle partial availability of data. Traditional error handling throws on the first missing data but I find that unsatisfactory. Instead I would like to have some way to collect all properties and all missing properties.
One way is to introduce a PropertyGetter
function:
sealed abstract class PropertyGetterTree
object PropertyGetterTree {
case class Empty() extends PropertyGetterTree
case class Leaf (failure: PropertyGetterFailure) extends PropertyGetterTree
case class Fork (left: PropertyGetterTree, right: PropertyGetterTree) extends PropertyGetterTree
}
case class PropertyGetterResult[+A](value: A, tree: PropertyGetterTree)
case class PropertyGetter[+A](f: PropertyBag => PropertyGetterResult[A])
The idea here is that the PropertyGetter
function applied to a property bad returns a value and potential errors while producing that value in the form of a tree. If no error were detected the the tree is empty.
This approach will allow for collection all errors while producing a value.
This function can be made into a Monad and Applicative for succinct composition:
// b is a property getter that extracts credit card info
val b =
unit(CreditCardInfo.curried) <*>
get(number) <*>
get(expireDate) <*>
get(name) <*>
get(cvc)
// Sets up a property bag with credit card info
val pb =
PropertyBag.empty
.set(number , "1234")
.set(expireDate , Instant.now())
.set(name , "Bill Gates")
.set(cvc , "123")
// Extract credit card info
val gr = PropertyGetter.run(b, pb)
println(s"Good result: $gr")
// Try to extract credit card info from empty bag,
// will fail
val br = PropertyGetter.run(b, PropertyBag.empty)
println(s"Bad result: $br")
Full source code in the same gist.
I have take this pattern to a more complete solution here (F#): https://gist.github.com/mrange/18ca0863c45a3c00a670afb09379d4c1