Постановка задачи. Имеется действующий отлаженный проект на PHP, содержащий десяток моделей, в каждой из которых по 5 методов выборок данных. Проект растет, всё хорошо, но в определенный момент, под тяжестью нагрузки, назревает необходимость добавить каким-то образом кэширование обращений к моделям.
Возможные решения.
Первый способ «в лоб»: в каждый метод модели добавляем кэширование по стандартной схеме: проверить кэш, если есть актуальные данные, возвращаем их, если нет — выполняем метод, как было до этого и плюс в конце еще пишем, полученные из базы, данные в кэш. Сказать, что это ужасный способ значит, ничего не сказать, поэтому просто скажу, почему это плохо:
Второй способ, «расширяем классы моделей»: добавляем в модели методы-дублеры, которые оборачивают вызовы существующих методов в кэширование, например findById_Cached().
Вроде бы лучше, существующие методы не трогаем, вместо этого добавляем новые. Но остальные минусы на месте:
Третий способ «кэширующий прокси», очень простое и быстрое решение, поражающее своим изяществом и скоростью внедрения. Как его сделать – смотрим код.
Сначала у нас есть модель (образец):
И вызов её в приложении до кэширования:
Потом у нас появляется кэш (образец):
А вот и звезда этого топика — кэширующий прокси:
Используем:
Как видно, прокси совершенно не важно какой объект и метод кэшировать, совсем не обязательно, что это будет класс работающий с БД. При необходимости, мы сохранили возможность получения живых данных. А огромный слой приложения отвечающий за кэширование, свёлся в итоге к маленькому классу, внедрение которого не вызывает трудностей.
UPD: Есть нюанс, $news в нашем примере стал объектом другого типа, и если где-то в коде есть проверки типа (например instanceof или тип данных в параметрах метода), то эти проверки сломаются. Чтобы этого избежать надо наследовать \Cache\Proxy от \Storage, разумеется универсальность кэширующего класса в этом случае снизится.
Возможные решения.
Первый способ «в лоб»: в каждый метод модели добавляем кэширование по стандартной схеме: проверить кэш, если есть актуальные данные, возвращаем их, если нет — выполняем метод, как было до этого и плюс в конце еще пишем, полученные из базы, данные в кэш. Сказать, что это ужасный способ значит, ничего не сказать, поэтому просто скажу, почему это плохо:
- Нарушается один из принципов SOLID, «код должен быть открыт для расширения, но закрыт для изменений», т.е. мы берем и ломаем уже отлаженный выпущенный в продакшн код для того, чтобы добавить новую функциональность, а это всегда вызывает шквал ошибок и как следствие недовольство пользователей и заказчика.
- В одном и том же коде смешивается логика получения данных и кэширование, что приводит к распуханию классов и беспощадному повторению кода.
- Сделав так, мы лишаемся возможности получить живые данные в обход кэша (следующим шагом будет добавление флага $nocache).
- Очень высокая трудоёмкость впиливания кэширования таким способом и ещё большая трудоёмкость выпиливания его потом.
Второй способ, «расширяем классы моделей»: добавляем в модели методы-дублеры, которые оборачивают вызовы существующих методов в кэширование, например findById_Cached().
Вроде бы лучше, существующие методы не трогаем, вместо этого добавляем новые. Но остальные минусы на месте:
- Смешивание логики.
- Размеры классов растут ещё больше, чем в предыдущем способе.
- Очень высокая трудоемкость (добавить 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, разумеется универсальность кэширующего класса в этом случае снизится.