Pull to refresh

Использование паттерна Proxy для организации кэширования на PHP

Reading time 3 min
Views 8.1K
Постановка задачи. Имеется действующий отлаженный проект на PHP, содержащий десяток моделей, в каждой из которых по 5 методов выборок данных. Проект растет, всё хорошо, но в определенный момент, под тяжестью нагрузки, назревает необходимость добавить каким-то образом кэширование обращений к моделям.

Возможные решения.

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

  1. Нарушается один из принципов SOLID, «код должен быть открыт для расширения, но закрыт для изменений», т.е. мы берем и ломаем уже отлаженный выпущенный в продакшн код для того, чтобы добавить новую функциональность, а это всегда вызывает шквал ошибок и как следствие недовольство пользователей и заказчика.
  2. В одном и том же коде смешивается логика получения данных и кэширование, что приводит к распуханию классов и беспощадному повторению кода.
  3. Сделав так, мы лишаемся возможности получить живые данные в обход кэша (следующим шагом будет добавление флага $nocache).
  4. Очень высокая трудоёмкость впиливания кэширования таким способом и ещё большая трудоёмкость выпиливания его потом.

Второй способ, «расширяем классы моделей»: добавляем в модели методы-дублеры, которые оборачивают вызовы существующих методов в кэширование, например findById_Cached().
Вроде бы лучше, существующие методы не трогаем, вместо этого добавляем новые. Но остальные минусы на месте:
  1. Смешивание логики.
  2. Размеры классов растут ещё больше, чем в предыдущем способе.
  3. Очень высокая трудоемкость (добавить 50 новых методов, в нашем примере) + заменить везде в приложении вызовы старых методов, на новые, а если в будущем придется кэширование выпиливать, то еще и повторить все действия назад.


Третий способ «кэширующий прокси», очень простое и быстрое решение, поражающее своим изяществом и скоростью внедрения. Как его сделать – смотрим код.

Сначала у нас есть модель (образец):
<?php
namespace Storage 
{
    Class News 
    {
        public function getTodayNews()
        {
            return "today news";
        }
        
        public function searchNews( array $filter )
        {
            $key = \http_build_query($filter);
            return "search news where $key";
        }
    }
}
?>

И вызов её в приложении до кэширования:
<?php
    $news  = new \Storage\News;
    $todayNews  = $news->getTodayNews();
    $searchNews = $news->searchNews( array('tag' =>'sport') );
?>

Потом у нас появляется кэш (образец):
<?php
namespace Cache 
{
    Class Cache 
    {
        protected $data = array();
        
        public function get($key)
        {
            if ( isset($this->data[$key]) ) {
                return $this->data[$key];
            }
            else {
                return null;
            }
        }
        
        public function set($key, $val, $ttl = 60)
        {
            $this->data[$key] = $val;
        }
    }
?>

А вот и звезда этого топика — кэширующий прокси:
<?php
namespace Cache 
{
    Class Proxy
    {
        protected $realObject = null;
        protected $cache = null;
        protected $ttl = 0;

        public function  __construct( $object, $ttl = 60 )
        {
            $this->realObject = $object; 
             
            // для примера, сделаем по-простому 
            // без инверсий зависимости
            $this->cache = new Cache;  

            $this->ttl = $ttl;
        }
       
      
        // для перехвата вызовов несуществующих методов прокси
        // и трансляции их реальному объекту
        // используем магический метод __call() 
        public function  __call( $method, $args ) 
        {
            $cacheKey = $method . '(' . \serialize($args) . ')';
            
            $data = $this->cache->get( $cacheKey );
            
            if ( null === $data ) {
                $call = array( $this->realObject, $method );
                $data = \call_user_func_array( $call, $args );
                $this->cache->set( $cacheKey, $data, $this->ttl );
            }
            
            return $data; 
        }        
    }
}
?>


Используем:
<?php
    
    // $news   = new \Storage\News;   // меняем на :
    $realNews  = new \Storage\News; 
 
   /**
    * это хинт для IDE распознающих PhpDoc,
    * методы прокси нас не интересуют, нам важны методы реального объекта
    * 
    * @var \Storage\News $news;
    */
    $news   = new \Cache\Proxy( $realNews , 600 );
    
    $todayNews  = $news->getTodayNews();
    $searchNews = $news->searchNews( array('tag' =>'sport') );
?>


Как видно, прокси совершенно не важно какой объект и метод кэшировать, совсем не обязательно, что это будет класс работающий с БД. При необходимости, мы сохранили возможность получения живых данных. А огромный слой приложения отвечающий за кэширование, свёлся в итоге к маленькому классу, внедрение которого не вызывает трудностей.

UPD: Есть нюанс, $news в нашем примере стал объектом другого типа, и если где-то в коде есть проверки типа (например instanceof или тип данных в параметрах метода), то эти проверки сломаются. Чтобы этого избежать надо наследовать \Cache\Proxy от \Storage, разумеется универсальность кэширующего класса в этом случае снизится.
Tags:
Hubs:
-4
Comments 24
Comments Comments 24

Articles