Use of implicit to transform case classes in Scala
Using different entities for different layers like repository, service and view is a common (anti)pattern followed while developing web applications.
Using different model at each layer allows us to represent entities differently at each layer in a way such that the structure of the model makes sense at that layer. This is a good way of achieving separation of concern and lose coupling.
On the contrary use of single domain model across all layers simplifies writing the code in many cases but many times we end up polluting domain with view or repository layer related logic. Also use of single domain across layers brings in tight coupling between layers.
The most annoying problem with using separate entities at each layer is mapping them to and from other entities in neighboring layers. In languages like Java it’s very difficult to tackle this situation. Hence I prefer to use a single domain across all layers as much as possible and use separate entities only when it is absolutely needed.
Is it good or bad to use separate entity per layer is a bigger discussion and is not a part of this article. In this article we will try to address the issue of mapping entities with the help of implicit in Scala.
Let’s assume we want to find a person with given name from the database.
Following PersonEntity
case class represents a row in database and is used at repository layer to return a row from database.
PersonEntity.scalacase class PersonEntity(firstName: String, lastName: String, city: String, state: String, pin: String)
From the perspective of domain modelling in service layer, it makes sense to create a separate entity for Address
and then use it in a Person
entity as a composition.
Person.scalacase class Address(city: String, state: String, pin: String)
case class Person(firstName: String, lastName: String, address: Address)
Let getPerson
be a function in a repository which returns a Person
with given name.
PersonRepository.scaladef getPerson(name: String): Person = {
val personEntity: PersonEntity =
db.run {
persons.find(_.firstName == name).result
} Person(
personEntity.firstName,
personEntity.lastName,
Address(
personEntity.city,
personEntity.state,
personEntity.pin
)
)
}
If you observe the code snippet above, it’s too much of a work to transform PersonEntity
into Person
and we have to repeat this every time we want to map the entity.
Let’s add a as
method to PersonEntity
which takes a mapper function f
which will transform it into the target entity T
.
PersonEntity.scalacase class PersonEntity(firstName: String,
lastName: String,
city: String,
state: String,
pin: String) {
def as[T](f: PersonEntity => T) = f(this)
}
and let’s define a mapper function f
in a companion object of PersonEntity
, which transforms PersonEntity
into Person
case class.
PersonEntity.scalaobject PersonEntity {
def personMapper = (personEntity: PersonEntity) =>
Person(
personEntity.firstName,
personEntity.lastName,
Address(
personEntity.city,
personEntity.state,
personEntity.pin
)
)
}
This will simplify the repository method as follow:
PersonRepository.scaladef getPerson(name: String): Person = {
val personEntity: PersonEntity = ...
personEntity.as(PersonEntity.personMapper)
}
Now we can reuse the mapper function anywhere we want to map PersonEntity
into Person
. But still we have to always pass the person mapper explicitly. This can be avoided by the use of implicit.
Lets mark the mapper function parameter f
in as
function implicit.
PersonEntity.scalacase class PersonEntity(firstName: String,
lastName: String,
city: String,
state: String,
pin: String) {
def as[T](implicit f: PersonEntity => T): T = f(this)
}
Let’s also mark the personMapper function defined in the companion object as implicit.
PersonEntity.scalaobject PersonEntity {
implicit def personMapper = (personEntity: PersonEntity) =>
Person(...)
}
Then we can transform PersonEntity
into Person
using following concise syntax.
PersonRepository.scaladef getPerson(name: String): Person = {
val personEntity: PersonEntity = ...
personEntity.as[Person]
}
This is possible because we have marked the mapper function f
as implicit. And we have defined an implicit mapper function which goes from PersonEntity
to Person
(PersonEntity => Person)
.
Scala compiler find the implicitly defined mapping function which is in the lexical scope as it’s defined in the companion object of the PersonEntity
. It picks the appropriate function based on the generic type parameter passed to the application of as
function.
We can map the same entity into different target entities, since our as
method is generic. All we have to do is add another mapper function for the target entity type.
Let’s say we want to map the same PersonEntity
to Student
class.
Student.scalacase class Student(firstName: String, city: String)
All we have to do is define another mapper function in the companion object of PersonEntity
.
PersonEntity.scala
...
implicit def studentMapper = (personEntity: PersonEntity) =>
Student(personEntity.firstName, personEntity.city)
Then we can do the transformation similarly.
personEntity.as[Student]
This pattern using implicit simplifies the use of separate models at each level as the overhead of mapping entities is completely abstracted out.
Happy coding. :)