I've followed the design principle from the book Functional and Reactive Modeling.
So all the service methods return Kleisli
.
The question is how can I add an updatable cache over these services.
Here is my current implementation, is there a better way (existing combinators, more functional approach, …) ?
import scala.concurrent.duration.Duration
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.{Await, Future}
import scalaz.Kleisli
trait Repository {
def all : Future[Seq[String]]
def replaceAll(l: Seq[String]) : Future[Unit]
}
trait Service {
def all = Kleisli[Future, Repository, Seq[String]] { _.all }
def replaceAll(l: Seq[String]) = Kleisli[Future, Repository, Unit] { _.replaceAll(l) }
}
trait CacheService extends Service {
var cache : Seq[String] = Seq.empty[String]
override def all = Kleisli[Future, Repository, Seq[String]] { repo: Repository =>
if (cache.isEmpty) {
val fcache = repo.all
fcache.foreach(cache = _)
fcache
}
else
Future.successful(cache)
}
override def replaceAll(l: Seq[String]) = Kleisli[Future, Repository, Unit] { repo: Repository =>
cache = l
repo.replaceAll(l)
}
}
object CacheTest extends App {
val repo = new Repository {
override def replaceAll(l: Seq[String]): Future[Unit] = Future.successful()
override def all: Future[Seq[String]] = Future.successful(Seq("1","2","3"))
}
val service = new CacheService {}
println(Await.result(service.all(repo), Duration.Inf))
Await.result(service.replaceAll(List("a"))(repo), Duration.Inf)
println(Await.result(service.all(repo), Duration.Inf))
}
[update] Regarding the comment of @timotyperigo, I've implementing the caching at the repository level
class CachedTipRepository(val self:TipRepository) extends TipRepository {
var cache: Seq[Tip] = Seq.empty[Tip]
override def all: Future[Seq[Tip]] = …
override def replace(tips: String): Unit = …
}
I'm still interested for feedback to improve the design.
Timothy is totally right: caching is an implementation feature of the repository (and not of the service). Implementation features / details should not be exposed in contracts and at this point you are doing good with your design (not with your implementation, though!)
Digging a little deeper in your design problem, it is interesting to you look at how dependency injection can be done in Scala:
The cake pattern and the constructor injection have one similarity: dependencies are bound at creation time . With the Reader monad (Kleisli just provides an additional layer on top of it) you delay binding which results in more composability (due to the combinators), more testability and more flexibility
In case of decorating an existing TipRepository
by adding caching functionality, the benefits of Kleisli would probably not be needed and they might even make the code harder to read. Using constructor injection seems appropriate, since it is the simplest pattern that let you do things "well"
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With