Сейчас работаю над доработкой/переписыванием проекта, который был написан, ну скажем так, «не совсем грамотно». По ходу есть задача оптимизировать работу, т.к. код изначально был написан крайне неоптимально. Среди работ по оптимизации прикручивается кэш.
В проекте есть несколько разных источников данных, результаты работы которых хорошо было бы кэшировать, основной — конечно БД. Хотелось решения прозрачного, с минимальной кровью. В один прекрасный момент надоедает писать конструкции вида
И хочется чего-то другого. Конечно, код можно вынести в отдельную функцию или метод, но это как-то скучно и к тому же, для каждого разного вызова (а там есть не только $db->queryAll, а несколько разных вариантов) нужен будет свой код и своя функция/метод.
С другой стороны, добавлять код кэширования непосредственно в источники данных тоже не очень правильно — в конце концов, они этим не должны заниматься (именно поэтому Трейты тоже не подходят). Создавать отдельный класс кэша тоже не очень удобно.
В общем, хотелось единого, универсального решения, которое бы подошло для разных источников данных, с разными интерфейсами, но в то же время было единообразным. Было решено сделать «волшебный» декоратор.
Если вы не в курсе, что такое декоратор, то в общих словах: Декоратор — это шаблон проектирования, цель которого в динамическом подключении нового поведения к объекту. Таким образом, в нашем случае, объект доступа к данным останется для системы вроде как тем же самым, с точно тем же интерфейсом и поведением, но у него появляется некое новое (кэширующее) поведение.
Чего именно хочется: чтобы в объекте источника данных появились дополнительные методы вида cached*. Например, был метод getData(), в дополнение к нему появится метод cachedGetData(), с тем же самым интерфейсом, что и getData(). Декоратор будем делать на «волшебных» методах.
Итак, пишем:
Инициализация декоратора будет выглядить примерно так:
Но пока что наш декоратор совсем не декоратор и совсем не ведёт себя так, как декорируемый объект. Исправим это, добавив волшебства (добавляем геттеры/сеттеры, проброс вызовов):
Отлично, теперь поведение объекта идентично натуральному (ну, почти, но в нашей ситуации этого достаточно, если вам чего-то не хватает, добавьте нужных волшебных методов).
Обычно, в декоратор добавляются простые методы. Но мы хотим волшебства, поэтому сделаем так:
Собственно всё. Теперь, задекорировав нужный источник данных мы можем писать вместо
Просто:
Вот так просто, и больше не нужно городить никакие огороды для работы с кэшем.
Update: Тут в личку пишут, что теряется гибкость, нет возможности указывать время жизни кэша. В моём случае это просто не актуально, используется время заданное при инициализации объекта кэша. Но если вам это нужно, можно просто расширить декоратор. Можно, например, поменять интерфейс для cached* функций, добавив первым параметром время жизни кэша. Или добавить больше волшебных методов, которые будут использовать разное время жизни кэша, например, fastCached* и slowCached* (для часто и редко обновляемых данных, соответственно).
В проекте есть несколько разных источников данных, результаты работы которых хорошо было бы кэшировать, основной — конечно БД. Хотелось решения прозрачного, с минимальной кровью. В один прекрасный момент надоедает писать конструкции вида
$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* (для часто и редко обновляемых данных, соответственно).