Scala

индекс
42,65

Scala: Кэширование результатов исполнения методов

Иногда возникает необходимость кэширования результатов исполнения методов. Одно из возможных решений для java описано здесь. Всё, в принципе, тривиально: EHCache, Spring AOP для перехвата вызовов, немножко кода.

Рассмотрим, как мне кажется, более элегантное решение на scala.

Сформулируем задачу более конкретно. Требуется создать mixin, при добавлении которого к реализации сервиса, можно будет добавлять кэширование результатов исполнения методов следующим образом:

    final val CACHE_FIRST_USER_CREATED = "firstUserCreated"
    def isFirstUserCreated:Boolean = cache (
      {userDAO.getCount>0} withKey CACHE_FIRST_USER_CREATED forever
    )


Начнем с простенького DSL для определения параметров кэширования. Нам потребуется класс для описания параметров кэширования, содержащий:
  • ключ,
  • время жизни записи,
  • собственно функция вычисляющая возвращаемое значение.

  class CacheOptions[T](fn: =>T) {
    var time = -1
    var key:String = "key"
  }

Определим методы forever, expirationTime, withKey, умолчательное преобразование для функций для заполнения обьектов этого класса, а также метод _execFn, который будет вычислять значение fn, переданное по имени.

  class CacheOptions[T](fn: =>T) {
    var time = -1
    var key:String = "key"
    def _execFn:T = fn
    def forever = {this.time= -1;this}
    def expirationTime(time:Int) = {this.time=time;this}
    def withKey(key:String) = {this.key=key;this}
  }

  implicit def fn2co[T](fn: =>T) = new CacheOptions[T](fn)


Таким образом выражение вида:
{ "lazy value" } withKey "mykey" expirationTime 30000

Будет возвращать объект CacheOptions[String] с заполненными полями time, key и fn.

Перейдем непосредственно к кэшированию. В статье, которую я упомянул в начале, для кэширования результатов используется EHCache. Ничто не мешает поступить так же, однако, для простоты я покажу кэширование результатов в простом ConcurrentHashMap.

Итак, собственно метод cache:
  private val cache = new ConcurrentHashMap[String,CacheRecord]

  def cache[T](co:CacheOptions[T]):T = {
    val timestamp = System.currentTimeMillis()
    val cr = cache.get(co.key)
    (cr==null) match {
      case true => refresh(timestamp,co)
      case false => 
        if (co.time>0 && (timestamp-cr.timestamp)>co.time)
          refresh(timestamp,co)
        else
          cr.obj.asInstanceOf[T]
    }
  }

  private def refresh[T](timestamp:Long, co:CacheOptions[T]):T = {
    val cr = new CacheRecord(timestamp, co._execFn.asInstanceOf[AnyRef])
    cache.put(co.key, cr)
    cr.obj.asInstanceOf[T]
  }


Никаких космических технологий — попытка выбрать из кэша, проверка timestamp записи, исполнение и кэширование, если это необходимо.

Добавим простенький метод для принудительного исключения записей из кэша:
  def evict(key:String):Unit = cache.remove(key)

Готово!

Минус, в сравнении с использованием spring aop — необходимо описывать процедуру создания ключа, в зависимости от входных параметров (в решении на spring aop это происходит автоматически). Плюсы — простота, возможность принудительного удаления записей.

Для примера я показал кэширование в ConcurrentHashMap, минусы такого решения, в сравнении с использованием EHCache, очевидны — нет возможности ограничить количество записей в кэше и определить стратегию удаления записей, необходимо самостоятельно следить за тем чтобы возвращаемые значения были immutable, иначе будет существовать возможность изменить значения, находящиеся в кэше. Но всех этих проблем можно избежать, «переключив» этот trait на использование EHCache или любой другой библиотеки. Это дело 5 минут.
+2
15 сентября 2009, 09:55
3

комментарии (2)

0
SMiX #
Спасибо за статью!
Перенесите в блог Scala. Она нынче популярна ,)
+1
victorb #
готово!

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.