#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:
/**
* 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.
*
* 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.
*
*/
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.