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

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

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

    $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* (для часто и редко обновляемых данных, соответственно).

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 45

      0
      А почему нельзя использовать интерфейсы и наследование вместо костыльной перегрузки с помощью магических методов?
        +1
        Потому что источников данных несколько, они разные (это не только бд), интерфейсы у них совсем разные. Для каждого из них нужно будет создавать свой класс?
        • UFO just landed and posted this here
            +1
            DataMapper на legacy-коде без оного и без вообще нормальных понятий об ООП? Вы представляете объём изменений для его внедрения?
            • UFO just landed and posted this here
                0
                В статье четко говорится: «Сейчас работаю над доработкой/переписыванием проекта, который был написан, ну скажем так, «не совсем грамотно».»
                  0
                  Кстати, я бы не сказал, что это — «чётко».
            +1
            Я прочитал в тексте «Источники данных» и подумал, что у них, наверное, интерфейс доступа-то общий, раз вы их понятийно объединили.

            Тогда вопрос снимаю. Согласен, если надо абстракцию от плохой архитектуры, то наслоить сверху декоратор — проще и себе дешевле, чем ковыряться в глубинах легаси-ада.
        • UFO just landed and posted this here
            0
            Про отсутствие подсказок в IDE, согласен.

            Ещё раз: в проекте много legacy кода, который быстро не заменишь. А результата нужно добиваться здесь и сейчас (при этом ничего не убить). Потом, вы снова не поняли — источников данных несколько, они принципиально разные, бд тут просто как пример. Организовать кэширование нужно для всех их. В этой ситуации нужно будет плодить кэширующие версии классов источников данных.

            Ваша реализация, возможно, имеет право на жизнь, но в данной ситуации это получается значительно сложнее и куда более громоздко.
            • UFO just landed and posted this here
                0
                Ну тогда придётся писать отдельный класс для каждого источника данных и соответствующие методы. Или напихать все методы от всех интерфейсов в один класс, но это уже совсем какая-то ересь получится. Конечно, методы можно дорисовать и в нынешней реализации, исключительно ради IDE, но хотелось продемонстрировать именно «волшебный» декоратор, а не обычный.
                • UFO just landed and posted this here
                    +1
                    Думаю TiGR имел в виду ситуацию не только с несколькими источниками данных но и с несколькими классами их получения. Например в вашем варианте для 3-х классов нужно сделать 3 декоратора:
                    interface AlbumDbMapperInterface
                    {
                    public function getAlbums()
                    public function getAlbum($id)
                    }
                    interface ArtistDbMapperInterface
                    {
                    public function getArtists()
                    public function getArtist($id)
                    }
                    interface TagMongoDbMapperInterface
                    {
                    public function getTags()
                    }

                    class AlbumDbMapper implements AlbumDbMapperInterface
                    {
                    public function getAlbums(){}
                    public function getAlbum($id){}
                    }
                    class ArtistDbMapper implements ArtistDbMapperInterface
                    {
                    public function getArtists(){}
                    public function getArtist($id){}
                    }
                    class TagMongoDbMapper implements TagMongoDbMapperInterface
                    {
                    public function getTags(){}
                    }

                    abstract class CachingDecorator{}
                    class AlbumDbMapperCachingDecorator extends CachingDecorator implements AlbumDbMapperInterface
                    {
                    public function getAlbums(){}
                    public function getAlbum($id){}
                    }
                    class ArtistDbMapperCachingDecorator extends CachingDecorator implements ArtistDbMapperInterface
                    {
                    public function getArtists(){}
                    public function getArtist($id){}
                    }
                    class TagMongoDbMapperCachingDecorator extends CachingDecorator implements TagMongoDbMapperInterface
                    {
                    public function getTags(){}
                    }
                      0
                      Многовато получилось)
                      • UFO just landed and posted this here
                    0
                    У любой магии есть две стороны.

                    А представьте, вы захотите для одного из источников данных сменить тип кеширования, для одного или пары его методов. Вам придется вставлять костыль либо в декоратор, либо опять по всему коду исправлять методы.

                    Вообще было бы логично вообще не использовать префикс cached а навесить декоратор прямо на get-методы. И внутри декоратора научиться отслеживать, когда этот кеш нужно сбросить. Это также плохо совместимо с универсальной магией.
                    0
                    Подсказок в данном случае можно и через phpDoc добиться.
                0
                Посмотрите в сторону Шаблона Cache Management. Есть описан в Книге Шаблоны проектирования Java.
                Вот ссылка на общую структуру шаблона img821.imageshack.us/img821/5167/cachemanagement.png
                  0
                  Не понимаю, как Cache Management поможет в описанной ситуации.
                    0
                    Вы хоть на картинку посмотрите, потом уже задавайте вопросы. То что сделали вы, и то что там нарисовано, это решение вашей задачи.
                      0
                      Я смотрел, честно. Долго думал. Правда так и не понял, как мне поможет этот кэш объектов и чем этот вариант лучше. То есть по сути, всё сводится к переносу всего кода в codeManager. Этому менеджеру нужно будет уметь работать со всеми разношёрстными источниками данных. Короче, вернулись туда, откуда начали.

                      Или я просто вас не понимаю.
                        +5
                        У вас может быть много источников даных, это не проблема. Все равно, вы к ним обращаетесь, через класы и методы.
                        Вместо

                        $class->cachedMethodName($arguments1, ..., N);

                        у вас будет что-то типа такого

                        $cacheManager->getData($instanceofClass, $methodName, $arguments, $ttl);

                        Таким образом, вы во внешнем класе реализовуете кеширование, там же у вас логика формирования всех ключей кеша. Например по имени класа/метода и аргументов, тут же можна указать время жизни.
                        Вы не нарушаете 1 принцип SOLID — На каждый объект должна быть возложена одна единственная обязанность. Кешировать данные и доставать их это уже 2.

                        Где-то так.
                  0
                  В своем проекте решил проблему кеширования подобным образом, единственное, над чем вам еще стоит поработать — правильная условная инвалидация кеша. (к примеру очистить кеш всех методов класса или метода с определенным параметром) + реализовать единоразовове выполнение незакешированного метода (при больших нагрузках 2 запроса могут получить значение «кеша нет, выполнить метод», тем самым выполнить ресурсоемкую работу несколько раз)
                    0
                    Очень давно, нужно было на один старый сайт добавить поддержку мемкэшед, решил это похожим образом

                    github.com/Rpsl/MySimpleCache
                      0
                      Простите, что тут похожего? Подобные менеджеры кэша над БД давным-давно я тоже писал, но тут этот вариант не подходит (много источников данных с разным интерфейсом). Декоратор куда более изящное решение — хотя бы сравните объём кода.
                      +4
                      Батенька, да вы извращенец, вы не думали что кэш можно включать явно
                      $db->enableCache()->queryAll($query);
                      $db->enableCacheOnce()->queryAll($query);
                      $db->enableCacheOnce($time, $tags)->queryAll($query);
                      Это решение нагляднее и гибче.
                        0
                        Тоже хотел написать про такой вариант, только я использую один метод с расширяемыми настройками вроде
                        $whatever->cached(expires, additional_settings)->get(criteria);
                        по criteria составляется ключ (в моем случае) мемкеша.
                        Ну или
                        $whatever->get(criteria)->cached(expires, settings)->fetch();
                        если
                        $whatever->get(criteria)->cached(expires, settings)
                        у нас вернет курсор.

                        Хотя это уже кому как удобней наверное или кто как реализовал.
                          0
                          Здесь определяющим является — кто больше знает. В данном случае модель лучше знает что ей нужно кешировать, а главное как с этим быть дальше, например, инвалидация кеша при обновлении-удалении.
                        +1
                        При использовании такого подхода (вообще любого «декоратора») не стоит забывать, что в новых версиях PHP есть возможность указывать класс, который принимается в параметрах функции:
                        function doSomething(MyDBClass $db) { ... }
                        


                        И если туда передать декоратор вместо класса-наследника MyDBClass, то это будет сразу Fatal error. Например, с Propel у меня была проблема, что поскольку базовые классы автогенерируются, а вносить изменения в готовую библиотеку — нехорошо, то передавать декоратор вместо нормального класса у меня не получилось — пришлось писать подкласс и извращаться с перечислением всех нужных методов и свойств вручную.
                          0
                          В статье используется немного незаконченный декоратор, так как он не имеет общего интерфейса с компонентом. Достаточно дописать
                          class CachingDecorator implements MyDBInterface {}

                          где MyDBInterface — интерфейс, имплементируемый декорируемым классом MyDBClass, и проблема будет решена
                          function doSomething(MyDBInterface $db) { ... } 

                          Скорей всего данный нюанс неактуален в проекте автора, вон он и не довел до конца.
                            0
                            Как я уже сказал, нужно передавать этот объект функции, определение которой менять нельзя :), потому что очень не хочется вносить изменения в готовые чужие библиотеки, которые часто обновляются
                          +2
                          >>Декоратор — это шаблон проектирования, цель которого в динамическом подключении нового поведения к объекту. Таким образом, в нашем случае, объект доступа к данным останется для системы вроде как тем же самым, с точно тем же интерфейсом и поведением, но у него появляется некое новое (кэширующее) поведение.
                          В вашем примере интерфейс поменялся, вместо getData() стало cachedGetData().
                          Но в общем суть понятна, спасибо.
                            0
                            А как быть с инвалидацией?
                              +2
                              Ни в коем случае не кешируйте ничего без установки флагов.

                              Алгоритм кэширования должен быть примерно таким
                              1. Проверяем TTL объекта кэша. Если объект устарел, проверяем наличие флага обновления. Если флаг есть то берем объект из кэша и продолжаем.
                              2. Если флага нет, то запускаем процесс обновления объекта кэша. При этом ставим флаг, который говорит всем остальным процессам что идет процесс обновления данных. Объект кэша создаем с временным именем. По окончанию процесса объект в кэше надо подменить атомарной операцией. mv в файловой системе.
                              3. Снимаем флаг

                              Ни в коем случае нельзя удалять ничего из кэша. Это в многопоточных системах может вызвать лавинообразно растущие нагрузки на ресурсоемкие места в программах, которые могут привести к печальным последствиям.
                                0
                                > Если флаг есть то берем объект из кэша и продолжаем.

                                А если какой-то процесс установил этот флаг и отвалился/умер/завис? Что делать остальным?

                                >Объект кэша создаем с временным именем. По окончанию процесса объект в кэше надо подменить атомарной операцией. mv в файловой системе.

                                А как это сделать на PHP + memcached?
                                  0
                                  >А если какой-то процесс установил этот флаг и отвалился/умер/завис? Что делать остальным?

                                  Пользователи будут видеть старую версию данных.
                                  Проверять жив ли процесс. В php есть возможность посмотреть на pid своего серверного процесса.
                                  Кусок кода с php.net

                                  <?php
                                  $lockfile = sys_get_temp_dir(). '/myScript.lock';
                                  $pid = file_get_contents($lockfile);
                                  if (posix_getsid($pid) === false) {
                                  print «process has died! restarting...\n»;
                                  file_put_contents($lockfile, getmypid()); // create lockfile
                                  } else {
                                  print «PID is still alive! can not run twice!\n»;
                                  exit;
                                  }
                                  ?>

                                  >А как это сделать на PHP + memcached?

                                  Там нету функции переименования объектов?
                                    0
                                    Сразу: я не докапываюсь, а пытаюсь понять как правильно сделать…

                                    Итак, некий процесс проверил TTL, все дела, проверяет флаг — он установлен,
                                    дальше что? Найти какой процесс его установил и проверить не завис ли он?
                                    Если завис — самому снять этот флаг и установить от своего имени?
                                    Что-то как-то навороченно получается…
                                    Пока я тут с флагами, да pid'ами прыгаю данные 200 раз устареют.
                                      0
                                      Ну почему же 200. Сделайте TTL меньше примерно на 1.5*время обновления объекта кэша.
                                      И я всегда считал что TTL на несколько порядков больше чем время обновления объекта, поэтому несколько дополнительных проверок не помешают.

                                      Ну наворочено-не наворочено… У меня просто сайты на полдня отключали из-за проблемы отсутствия объектов в кэше и гонок при их обновлении.

                                      Но на самом деле у меня более чем прилично работала реализация без проверки существования процесса обновления. Надо просто хорошо отладить этот код и погонять его в реальных условиях. И настроить сервер так чтобы исключить неожиданности.

                                      Вот можете поглядеть на код который предложил EugeneOZ ниже.
                                        0
                                        Флаг положить в мемкэш со временем удаления чуть больше, чем время пересоздания кэша. Нужна всего лишь команда add мемкэша с соответствующими параметрами.
                                        Первый процесс не находит в кэше данные или находит старые, ставит флаг и начинает пересоздавать кэш. Второму на попытку add приходит ошибка. Он либо отдает старые данные, либо ждет появления новых (если нету в кэше), либо умирания флага.
                                        В общем, ничего искать не надо, пиды тоже не нужны, кода строчек десять.
                                    • UFO just landed and posted this here
                                        0
                                        Спасибо!
                                    +1
                                    No refactoring, just decorate!

                                    Декоратор очень часто используется как наследник базового.
                                    А еще самое главное, базовый класс и его декоратор должны(!) имплементировать один и тот же интерфейс!
                                    У Вас это не так(
                                      0
                                      Вопрос «знатокам паттернов»
                                      Почему автор назвал своё решение «кеширующий декоратор» а не «кеширующий прокси»?
                                        –1
                                        Главная цель прокси — контролировать доступ к объекту, а цель декоратора — динамически наращивать функционал. Прокси обычно сам контролирует создание и доступ к объекту. А в случае с декоратором, в большинстве случаев, декорируемый объект передаётся в декоратор явно. Иначе говоря, отношения прокси → проксируемый объект задаются при компиляции, т.к. явно прописаны в коде прокси. А в случае с декоратором декорирование значительно гибче и происходит во время исполнения (возможно комбинирование в зависимости от ситуации разных декораторов и даже их рекурсивное использование).

                                      Only users with full accounts can post comments. Log in, please.