/* * The good practices say we shouldn't use the same type for everything (domain, json, persistence, ...), but * most of the time those types are very similar - if not exactly identical - creating a lot of duplication. * * Inheritance to the rescue? Nah... not very FP and even the OOP folks are aware that we should * prefer composition over it. But composition has it's own challenges (Law of Demeter for instance). * * Let's see how the new `Export Clauses` in Scala 3 can help with that: * https://docs.scala-lang.org/scala3/reference/other-new-features/export.html * */ import io.circe.Encoder import io.circe.generic.semiauto.* import io.circe.syntax.* import java.util.UUID // the pure domain concept/entity // the base data is declared here, and only here case class Frunfles(a: String, b: Int, c: Boolean) object Frunfles: given Encoder[Frunfles] = deriveEncoder // persisted entity containing an id // no need to make the id optional // no need to duplicate Frunfles fields case class PersistedFrunfles(id: UUID, data: Frunfles): export data.* object PersistedFrunfles: // easy to avoid exposing the id in Json given Encoder[PersistedFrunfles] = Encoder.instance(_.data.asJson) // type-safe operations // explictly stating which operations require an entity with id def insert(f: Frunfles): PersistedFrunfles = ??? def update(f: PersistedFrunfles): PersistedFrunfles = ??? def delete(f: PersistedFrunfles): Unit = ??? // composition over inheritance, more FP-ish val f = Frunfles("Meh", 42, true) val pf = PersistedFrunfles(id = UUID.randomUUID().nn, data = f) // exported fields available without calling `.data`. Happy demeter! val id = pf.id val a2 = pf.a val b2 = pf.b val c2 = pf.c