Composing Service Layers in Scala
We just described standard design issues you have when you start creating layers of services, DAOs and other components to implement an application. That blog/gist is here.
The goal is to think through some designs in order to develop something useful for an application.
If you compose services and DAOs the normal way, you typically get imperative style objects. For example, imagine the following:
The next step would be to create the implementation classes. But here's where you get stuck. 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. But we do not want to bake in a specific technology at the service and DAO "interface" level.
So what we really need to do is reframe the standard service and DAO model so that is more flexible--which in this case means more composable.
In this case, we really need to not return the actual return value directly, but a function that returns the return value. This way, we can than wrap the functions and compose them together with others. For example, we can call the function asynchronously or synchronously.
The most obvious approach is to use the Reader monad, say from scalaz:
Because we did not want to specify the actual "value" in the reader that would be "injected" into the function, we parameterized the type. We could also have the UserService take a constructor parameter that contains an environment. So we need to think through this. But there are issues with parameterization, namely that the parameter would get carried throughout the API. And we do not want to lock us into the Reader monad either as that reduces flexibility. So trying to genericize our DAO structures using type parameters can work but we may want to try existential types as well so we can mix in the context. The service/DAO object can consume it as it sees fit. Since existential types are a form of cake layer, we are choosing cake over type parameterization. The cake layer, at least using self-types, can also help us ensure that the dependencies are always available within the context so it also helps us force not only a certain way of composing our service/DAO but it also helps with dependency management which is one of our objectives.
First let's take a quick look at a standard approach of using a cake layer to weave in the database needed to open a transaction/session.
Let's at how to provide some simple slick-specific parts to the service and DAO implementations. We saw in a previous blog that we could do something like:
And then our service and DAO could be something like:
But this only helps with ensuring required dependencies are available (in this case a slick implementation) versus helping composability. What it does show us is that to get transactional behavior, we need a database-like object. Also, if, as in slick, queries are to be defined in the service, we would need a "profile" as well as a parameter. It's pretty easy to see that if we want to abstract away the specific technology choice but we need to introduce an abstraction at the "UserService" level and whatever we do introduce, needs to be technology agnostic.
So it looks like if we want to use a constructor parameter or cake layer, it will probably need to carry both the "database," the "profile," and something that defines a type for the service/DAO methods so it can return a function instead of a direct value.
To improve composability, methods need to return functions 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 or wrap functions within functions which is a more functional way of writing code.
We know that the implementations will need a technology-specific context to execute under--an environment. So we need an abstraction for the environment and an easy way to apply it. We have a bunch of choices:
- Put this technology-specific context 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 the DAO. This restricts your choices of how to handle the technology specific component and could limit future design choices.
- Provide a type parameter could also be used but that sometimes makes inheritance more restricted.
- Use an 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. But this sounds a bit too open ended.
- Use the Reader from scalaz or, if we wanted to to say a Keisli object, either of which could lift a function (A => M[B]) into an environment. But using Reader is rather restrictive actually. Perhaps someone wants to use their own Reader or equivalent in their functions.
So the options all look good, but we take some lessons from the typesafe slick driver. The slick drivers use a lifted-embedded type design to lift the queries (which are object types separate from the driver) into the driver for execution. It uses implicit defs that when brought into scope using
import profile.simple._lift the query object into the appropriate driver. That seems like a decent way to do this.
We need an abstraction that allows us to store some state, if needed, specify a return type from methods in our service/DAO method calls, allow each method to obtain a context value when called and allows us to compose the functions together. The cue from the slick layer suggests:
This seems useful. It essentially is technology agnostic, that is, the Environment is technology agnostic but specifies all of the elements we had itemized. The trait Method is used as the return type from every service/DAO method that you wish to make more composable.
We can now define the UserServiceComponent using this mixin:
We see that the service/DAO is still technology agnostic but does use types from our environment. We can now make a Slick specific environment:
This just uses the Session object from slick as the context and the environment ensures that a profile and database are defined and available. The profile is available through a well-formed path. We can now define the slick-specific service component:
And create a small program to test it:
That seems to meet the objectives. You could create some Executors that allow asynchronous execution using, say, a future. There are lots of choices you can make with the Executor. Since existential types are used, you can also create multiple, finely-sliced mixins and mix them together as needed.
Environment is not prefect and can be improved and there are a few issues buried in it, but the idea helps take a step in the right direction.Read the entire article, including the code on gisthub: https://gist.github.com/aappddeevv/8509607
The article touches on: scala, cake pattern, spring, DAO, slick, RDBMS and service layers.