Наверняка большинство программистов, работающих с современными веб-фрейворками, реализующими схему MVC, сталкивалось с таким небольшим затруднением: кэширование фрагмента View.
Хорошие фреймворки предлагают инструменты для полного кэширования страниц, фрагментарного, или кэширования экшенов. Недавно я посмотрел
90 выпуск подкаста
Railscasts, посвященный именно фрагментарному кэшированию в Ruby on Rails и уважаемый автор решал проблему, как мне показалось, неоптимально.
Опишу ситуацию.
Мы в шаблоне страницы и хотим закэшировать ее часть, например, список новых товаров. Пока все хорошо, мы пользуемся встроенными во фреймворк удобными средствами и в две-три строчки окружаем блок - ура, он кэшируется. Но - чу!, контроллер-то об этом ничего не знает и продолжает выполнять свою работу по подготовке данных для View. Естественно, ведь проверка наличия кэша осуществляется уже из шаблона, а контроллер к тому моменту отработал.
Автор подкаста показывает некрасивое решение - перенос кода для подготовки данных в шаблон и тут же, естественно, отметает его, как "ugly". Что он предлагает - перенести этот код в модель. То есть, в модели товара создается специальный метод, который выбирает новые товары, и этот метод вызывается из шаблона. Это лучше, чем первый вариант, но все же недостаточно хорошо, так как в модели приходится реализовывать вещи, которые могут понадобиться в одном только месте, а при смене интерфейса сайта могут оказаться ненужными и скорее всего останутся болтаться в коде просто так.
Мое решение
Я работаю со своим фреймворком на PHP, и пример буду писать на PHP, но решение простое и реализуется на любом скриптовом язке.
view.php:
...
<? if !(cacher::start(`Cache_Name`)) { ?>
<ul>
<? foreach ($latest as $item) { ?>
<li><?=$item->name();?>: <?=$item->price();?></li>
<? } ?>
</ul>
<? cacher::end(); } ?>
...
controller.php:
...
$latest = new model_collection(`product`);
$latest->load_by( $condition, $order, $limit );
$this->export(`latest`, $latest);
...
Метод load_by(...) выполняет один или несколько запросов к базе данных и формирует набор моделей класса Product. То есть, тратятся ресурсы на запрос, да еще и память на экземпляры модели.
Хорошо бы как-то запомнить, что мы хотим сделать, а делать это только если кэша нет.
Напишем это.
utils.php:
...
class prepared extends stdClass // крохотный класс для хранения подготовленной операции
{
// не буду усложнять пример геттерами и сеттерами
public $obj, $method, $args;
}
class utils
{
...
public static function prepare( $obj, $method, $args = null )
{
$res = new prepared();
// метод принимает неограниченное количество параметров
$args = func_get_args();
$res->obj = array_shift($args);
$res->method = array_shift($args);
// запоминаем все остальные параметры
$res->args = $args;
return $res;
}
public static function run( $prepared )
{
// страховка: шаблон не должен думать, пришли ли реальные данные, или заготовка
if (!($prepared instance_of prepared)) return $prepared;
$obj = $prepared->obj;
// вариант с заготовленным статическим методом
if (`::` === mb_substr( $prepared->method, 0, 2)) $method = $obj.$prepared->method;
else $method = array( $obj, $prepared->method ); // обычный метод
return call_user_func_array( $method, $prepared->args );
}
...
}
...
Использование
controller.php:
...
$latest = new model_collection(`product`);
// ничего не грузим сразу
// $latest->load_by( $condition, $order, $limit );
// запоминаем, что мы хотим сделать, в самой переменной для шаблона
$latest = utils::prepare( $latest, `load_by`, $condition, $order, $limit );
$this->export(`latest`, $latest);
...
view.php:
...
<? if !(cacher::start(`Cache_Name`)) { ?>
<?
// только здесь выполняем запланированное, при этом шаблону не нужно знать, что именно делается
$latest = utils::run( $latest );
?>
<ul>
<? foreach ($latest as $item) { ?>
<li><?=$item->name();?>: <?=$item->price();?></li>
<? } ?>
</ul>
<? cacher::end(); } ?>
...
Предположим, в вашем фреймворке товары надо было бы грузить статическим методом. Пожалуйста, можно и так:
controller.php:
...
// ничего не грузим сразу
// $latest = Product::get_latest(...);
// запоминаем, что мы хотим сделать, в самой переменной для шаблона
$latest = utils::prepare( `Product`, `::get_latest`, ... );
$this->export(`latest`, $latest);
...
В шаблоне же даже ничего не нужно менять.
Этот способ я использую во множестве мест и пока он меня не подводил. Недостаток: пока не удается готовить наборы операций, но в таких извращенных случаях уже можно и метод где-нибудь добавить.
Буду рад комментариям.
Апдейт
В комментариях мне указывают на наличие компонентов и возможности кэшировать их целиком. Я вынужден пояснить - заметка не об этом. Приведу другой пример, из реальной жизни.
Страница со списком новостей, экшен `index` контроллера `news`.
...
$news = new model_collection(`news`); // или как у вас
$news->load_by( $conditions, $order, $limit );
$this->export(`news`, $news);
...
Шаблон со списком новостей вкладывается в лэйаут, в котором присутствует еще куча компонент (новые товары, курсы валют и прочее). Компоненты кэшируются целиком, естественно. Но вот именно "основной" экшен же нам надо выполнить, мы страницу целиком закэшировать чаще всего не можем.
Тут-то и пригождается описанный подход - данные не доставать сразу, а только приготовиться. Можно, конечно, вынести непосредственно вывод новостей в еще один экшен, но таким путем мы почти удвоим количество экшенов, а это явно неудобно.
Так должно быть понятней.