Сейчас работаю над доработкой/переписыванием проекта, который был написан, ну скажем так, «не совсем грамотно». По ходу есть задача оптимизировать работу, т.к. код изначально был написан крайне неоптимально. Среди работ по оптимизации прикручивается кэш.
В проекте есть несколько разных источников данных, результаты работы которых хорошо было бы кэшировать, основной — конечно БД. Хотелось решения прозрачного, с минимальной кровью. В один прекрасный момент надоедает писать конструкции вида
И хочется чего-то другого. Конечно, код можно вынести в отдельную функцию или метод, но это как-то скучно и к тому же, для каждого разного вызова (а там есть не только $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* (для часто и редко обновляемых данных, соответственно).
