Friday, October 29, 2010

Using Scala implicits to limit client's view on the domain model

This is common problem in the Java Enterprise applications: how do we limit client's view on the domain model? I mean, we use JPA Entity Beans to model our domain, then we use it within a transaction in the Service layer to do some stuff and then we want to provide the results to the Presentation layer where is no access to the transaction anymore. The problem is that actually we do not give access to the full domain model, but only to a limited part of it. So, generally speaking, you cannot get all employees of a department if there was no special measures taken about it in the Service layer (or any other layer beneath).

Wouldn't it be nice if compiler would perform such checks? To me it sounds crazy impossible, while certainly desirable feature and I still don't know how to implement it in Java. Commonly used DTO approach quickly leads to several DTOs per entity (like shallow and deep copies + all variations per related entities and their entities and so forth). Code quickly becomes verbose, clumsy and filled with plumbing. In the Java world there is no escape from this looser choice: either DTO rabbit farm or careful programming of the (Web?) GUI part.

I think I've found a way to let compiler solve this problem in Scala.

Let's start with our domain model:

class Person(val id:Long, val email:String,
protected[domain] var dep:Department = null,
protected[domain] var address:Address = null)
class Department(val code:String, val name:String)
class Address(val street:String)

As you can see, entity relationships are hidden from the code outside the domain package. To provide such access let's define following trait:

trait PersonDepartment {
implicit def toPersonDepartment(p:Person) = new {
def getDepartment = p.dep
def setDepartment(d:Department) = p.dep = d
}
}

The code for PersonAddress trait is essentially the same, so I omit it here. And now is the tricky part: how do we import this implicit? Scala doesn't allow to import members of traits, only members of object (and packages, ok). Well, how about this code (like Service layer function):

def doThis = {
val p = new Person(1, "bb")
val dataView = new PersonDepartment {}
import dataView._
p.setDepartment(new Department("aa", "Whatever"))
p
}

It compiles! There is just one step left: provide the client with our dataView object:

...
(p, dataView)
}

We return our Person object together with an object who's implicits provide access to protected members of the domain object. The client code will look like this:

val (p, dataView) = doThis
import dataView._
val dep = p.getDepartment

And it compiles as well! But this one doesn't:

val a = p.getAddress

So, type checking works, compiler does here exactly what we want. Just for fun, let's mixin PersonAddress in the dataView in our function:

def doThis = {
...
val dataView = new PersonDepartment with PersonAddress {}
...
}

And our p.getAddress on the client side compiles now! Notice that we didn't changed the client code, only the function in the Service layer and still we can get compilation error if the client tries to access parts of the domain model we do not want him to. So, we found a way to manange the client's view of our domain model and we can do it from the Service layer. We can write another function and define there another subset of relationships and tell our client about limitations in type-safe way. All this will be enforced then by Scala compiler. Traits don't have to provide access to the same entity class by the way. Like we can define DepartmentPerson trait and mix it in the dataView together with PersonAddress, all in the same object, there are no obstacles for that from the type system.

This approach can also be used to provide deep-enough copies of our domain objects that will automatically be limited to the selected relationships. I think that relflection will be needed anyway and it rises another questions, but it's possible to write such code.

Hope you enjoyed it and

May the sources be with you! :)

No comments:

Post a Comment