Постановка задачи. Имеется действующий отлаженный проект на 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, разумеется универсальность кэширующего класса в этом случае снизится.