I had similar considerations to make when designing a domain model for our database application. Instead of inheritance, I used case class
composition together with type classes:
case class Party(id:Long,version:Long,name:String,address:Address)
trait AsParty[A] {
def getParty (a: A): Party
def setParty (a: A, p: Party): A
def id (a: A): Long = getParty(a).id
def version ...
}
case class Person (party: Party)
object Person {
implicit val PersonAsParty = new Party[Person] {
def getParty (p: Person) = p.party
def setParty (pe: Person, pa: Party) = pe.copy(party = pa)
}
}
Now, this will naturally lead to a deeper nesting of case classes due to favoring composition
over inheritance. If you want to update fields in deeply nested data structures, Lenses (such as
those found in scalaz) provide a neat, composable way to do that. You would change type class
Party like so:
trait Party[A] {
def party: Lens[A, Party] =
def id: Lens[A, Long] = party andThen Party.id
def version: Lens[A,Long] = party andThen Party.version
}
And at the companion object of Party you add:
import scalaz.Lens
object Party {
val id: Lens[Party,Long] = Lens(_.id, (a,b) => a.copy(id = b))
val version: Lens[Party,Long] = Lens(_.version, (a,b) => a.copy(version = b))
...
}
If you are working with deeply nested case classes, Lenses are a must. There are several
talks on Lenses in scalaz and Tony Morris wrote an article about them which was presented
at ScalaDays 2012.
You can now in a similar way define type class AsPerson
//we CAN use inheritance for code reuse in type classes
trait AsPerson[A] extends AsParty[A] {
def person: Lens[A,Person]
def party: Lens[A,Party] = person andThen Person.party
}
case class Member (person: Person)
object Member {
implicit val MemberAsPerson = new AsPerson[Member] {
def person: Lens[Member,Person] = ...
}
}
And so on. All party related logic goes into the AsParty type class, all person related
logic goes into the AsPerson type class. Since type classes are defined as traits, they
can inherit from each other (like AsPerson extends AsParty) for maximal code reuse.
Defining all those lenses leads to some boilerplate, but this might be easily addressed
using macros in Scala 2.10 (not sure about that, I have not tried any macro hackery so far).
Regards, Stefan