Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement caching with Kleisli

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.

like image 889
Yann Moisan Avatar asked Nov 09 '22 17:11

Yann Moisan


1 Answers

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:

  1. Constructor injection
  2. Cake pattern
  3. Reader monad

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"

like image 188
Edmondo1984 Avatar answered Nov 14 '22 22:11

Edmondo1984