5

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.

Yann Moisan
  • 8,161
  • 8
  • 47
  • 91
  • 1
    Just a thought...it seems to me that something like caching really isn't a domain behavior (ie, something which should be part of your service), but perhaps rather a property of the Repository implementation. Your service would then contain only the actions necessary to carry out the required behavior, but (should you desire) your application could choose between caching and non-caching Repositories. Within the repository implementation, you could use something like the State monad for a more functional approach to caching. – Timothy Perrigo Apr 07 '16 at 12:02

1 Answers1

1

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"

Edmondo
  • 19,559
  • 13
  • 62
  • 115