Как стать автором
Обновить

Волшебный кэширующий декоратор

Время на прочтение3 мин
Количество просмотров4.6K
Сейчас работаю над доработкой/переписыванием проекта, который был написан, ну скажем так, «не совсем грамотно». По ходу есть задача оптимизировать работу, т.к. код изначально был написан крайне неоптимально. Среди работ по оптимизации прикручивается кэш.

В проекте есть несколько разных источников данных, результаты работы которых хорошо было бы кэшировать, основной — конечно БД. Хотелось решения прозрачного, с минимальной кровью. В один прекрасный момент надоедает писать конструкции вида

$query = "Select something";
$result = $cache->get($query, $tag);
if (!$result) {
    $result = $db->queryAll($query);
    $cache->set($query, $tag);
}

И хочется чего-то другого. Конечно, код можно вынести в отдельную функцию или метод, но это как-то скучно и к тому же, для каждого разного вызова (а там есть не только $db->queryAll, а несколько разных вариантов) нужен будет свой код и своя функция/метод.

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

В общем, хотелось единого, универсального решения, которое бы подошло для разных источников данных, с разными интерфейсами, но в то же время было единообразным. Было решено сделать «волшебный» декоратор.

Если вы не в курсе, что такое декоратор, то в общих словах: Декоратор — это шаблон проектирования, цель которого в динамическом подключении нового поведения к объекту. Таким образом, в нашем случае, объект доступа к данным останется для системы вроде как тем же самым, с точно тем же интерфейсом и поведением, но у него появляется некое новое (кэширующее) поведение.

Чего именно хочется: чтобы в объекте источника данных появились дополнительные методы вида cached*. Например, был метод getData(), в дополнение к нему появится метод cachedGetData(), с тем же самым интерфейсом, что и getData(). Декоратор будем делать на «волшебных» методах.

Итак, пишем:

class CachingDecorator {

    /**
     * @var object Ссылка на декорируемый объект.
     */
    protected $obj;
    
    /**
     * @var object Ссылка на объект кэша.
     */
    protected $cache;
    
    /**
     * @var string Дополнительный параметр для кэша - тэг.
     */
    protected $cacheTag;

    /**
     * @param type $object Декорируемый объект
     * @param type $cache Объект кэша
     * @param type $cacheTag Тэг для кэша
     */
    public function __construct($object, $cache, $cacheTag = 'query') {
        $this->obj = $object;
        $this->cache = $cache;
        $this->cacheTag = $cacheTag;
    }
}

Инициализация декоратора будет выглядить примерно так:

$data = new CachingDecorator($data, $cache, 'remote');

Но пока что наш декоратор совсем не декоратор и совсем не ведёт себя так, как декорируемый объект. Исправим это, добавив волшебства (добавляем геттеры/сеттеры, проброс вызовов):

    public function __get($name) {
        return $this->obj->$name;
    }
    
    public function __set($name, $value) {
        return $this->obj->$name = $value;
    }
    
    public function __call($name, $args) {
        return call_user_func_array(array($this->obj, $name), $args);
    }

Отлично, теперь поведение объекта идентично натуральному (ну, почти, но в нашей ситуации этого достаточно, если вам чего-то не хватает, добавьте нужных волшебных методов).

Обычно, в декоратор добавляются простые методы. Но мы хотим волшебства, поэтому сделаем так:

    public function __call($name, $args) {
        if (strtolower(substr($name, 0, 6)) == 'cached') {
            $name = substr($name, 6);
            $cacheName = md5(serialize($args));
            $result = $this->cache->get($cacheName, $this->cacheTag);
            if ($result === false) {
                $result = call_user_func_array(array($this->obj, $name), $args);
                $this->cache->save($result, $cacheName, $this->cacheTag);
            }
            return $result;
        } else {
            return call_user_func_array(array($this->obj, $name), $args);
        }
    }

Собственно всё. Теперь, задекорировав нужный источник данных мы можем писать вместо

$result = $data->getDataById($id);

Просто:

$result = $data->cachedGetDataById($id);

Вот так просто, и больше не нужно городить никакие огороды для работы с кэшем.

Update: Тут в личку пишут, что теряется гибкость, нет возможности указывать время жизни кэша. В моём случае это просто не актуально, используется время заданное при инициализации объекта кэша. Но если вам это нужно, можно просто расширить декоратор. Можно, например, поменять интерфейс для cached* функций, добавив первым параметром время жизни кэша. Или добавить больше волшебных методов, которые будут использовать разное время жизни кэша, например, fastCached* и slowCached* (для часто и редко обновляемых данных, соответственно).
Теги:
Хабы:
+20
Комментарии45

Публикации

Истории

Работа

PHP программист
157 вакансий

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн