#The Problem We just described standard design issues you have when you start creating layers of services, DAOs and other components to implement an application. That gist is here.
#Working through Layers If you compose services and DAOs the normal way, you typically get imperative style objects. For example, imagine the following:
object DomainObjects {
type UserId = Long;
case class User(id: UserId, properties: Map[String, Any])
}
import DomainObjects._
/**
* The service layer. Most service layers define the unit of work. Many
* times a unit of work in the service layer is the same as that implemented
* in the DAO layer, but that's because some internet examples are too small
* to show the nuanances. Since this layer defines units of work, this is where
* the transaction strategy is also implemented. Spring implements this with
* annotations and proxies/byte-code enhancers. We'll use explicit coding because we are not
* using proxies/byte-code enhancers like spring uses.
*
* Classic cake pattern. Any implementation must define
* a service val (or directly create an object) to satisfy
* this abstract type.
*/
trait UserServiceComponent {
val service: UserService
trait UserService {
def updateName(User: UserId, newName: String): Either[String, User]
def findById(name: UserId): Option[User]
}
}
/**
* This is the usual "interface" declaration that has the underlying technology
* parameterized. The component is parameterized on a context that allows
* concrete classes access to a specific execution environment called the context.
* The context will be implementation specific.
*
*/
trait UserDaoComponent {
val userDao: UserDao
trait UserDao {
/**
* Find a user by their id.
*/
def findById(user: UserId): Option[User]
/**
* Update the user properties, whatever has changed.
*/
def update(user: User): Unit
}
}
/**
* The simple application auditing component.
*/
trait AuditDaoComponent {
/**
* We define a def here. If you want singleton behavior,
* define a private member _auditDao, instantiate that
* then return it in the def. If you want a new auditDao
* each time, create a new one each time this def is called.
* DAOs traditionally do not hold any state so it which you
* choose is not too critical for most applications but the
* option is demonstated here by using a def instead of a val.
*/
def auditDao: AuditDao
trait AuditDao {
/**
* Send the changes to an audit log somewhere.
*/
def auditChange(user: User, changedProperties: Seq[String]): Unit
}
}Here's where you get stuck. This is not well parameterized. In spring, you would use the @Transactional and the container injection (with our without java config) to configure your objects. In other words, you would provide specific technology choices in the form of annotations like @Transactional or @Autowired. Since scala and the cake pattern does not use byte code generation or proxies, you have to be a bit more explicit with the technology choices.
#Improving Composability To improve composability, methods return functions (a Reader is a wrapper around a function) instead of just raw values. If we did not do this, the imperative nature of the DAO would not allow us to compose a sequence of DAO calls because queries are modeled as Strings versus directly in the scala type system. We could also put this context concept at the UserDao trait level (and make UserDao a class) so that it becomes a constructor argument but then a new UserDao must be constructed each time you need to use it. You may want this pattern or not so choose where you place that context based on your application needs and design. Based on the way we have defined this, the ExecutionContext could change on a per call basis and this make it appropriate to use when we need a transaction, session, entity manager or some other dependency.
We know that most likely our implementations need a technology-specific context
to execute under--an environment. So we provide that as an abstract. We use the Reader to access that context if needed. We could abstract this further so its not even dependent on Reader but that makes it hard to read.
A type parameter could also be used but that sometimes makes inheritance
more restricted. By using a abstract type member (the over all object is know
an existential type) we also allow ourselves the ability to combine the context
members across all components that are instantiated together to allow the context
to satisfy multiple component context needs.
We could also abstract the Reader away if we wanted to to say a Keisli object,
anything that lifts a function (A => M[B]) into an environment and can be
mapped on in some way.