Pull to refresh

Фрагментарное кэширование в MVC веб-фреймворках

Reading time 4 min
Views 2.7K
Наверняка большинство программистов, работающих с современными веб-фрейворками, реализующими схему MVC, сталкивалось с таким небольшим затруднением: кэширование фрагмента View.

Хорошие фреймворки предлагают инструменты для полного кэширования страниц, фрагментарного, или кэширования экшенов. Недавно я посмотрел 90 выпуск подкаста Railscasts, посвященный именно фрагментарному кэшированию в Ruby on Rails и уважаемый автор решал проблему, как мне показалось, неоптимально.

Опишу ситуацию.
Мы в шаблоне страницы и хотим закэшировать ее часть, например, список новых товаров. Пока все хорошо, мы пользуемся встроенными во фреймворк удобными средствами и в две-три строчки окружаем блок — ура, он кэшируется. Но — чу!, контроллер-то об этом ничего не знает и продолжает выполнять свою работу по подготовке данных для View. Естественно, ведь проверка наличия кэша осуществляется уже из шаблона, а контроллер к тому моменту отработал.

Автор подкаста показывает некрасивое решение — перенос кода для подготовки данных в шаблон и тут же, естественно, отметает его, как «ugly». Что он предлагает — перенести этот код в модель. То есть, в модели товара создается специальный метод, который выбирает новые товары, и этот метод вызывается из шаблона.
Это лучше, чем первый вариант, но все же недостаточно хорошо, так как в модели приходится реализовывать вещи, которые могут понадобиться в одном только месте, а при смене интерфейса сайта могут оказаться ненужными и скорее всего останутся болтаться в коде просто так.

Мое решение


Я работаю со своим фреймворком на PHP, и пример буду писать на PHP, но решение простое и реализуется на любом скриптовом язке.

view.php:
<code>
...
<? if !(cacher::start('Cache_Name')) { ?>
	<ul>
		<? foreach ($latest as $item) { ?>
			<li><?=$item->name();?>: <?=$item->price();?></li>
		<? } ?>
	</ul>
<? cacher::end(); } ?>
...
</code>


controller.php:
<code>
...
$latest = new model_collection('product');
$latest->load_by( $condition, $order, $limit );

$this->export('latest', $latest);
...
</code>


Метод load_by(...) выполняет один или несколько запросов к базе данных и формирует набор моделей класса Product. То есть, тратятся ресурсы на запрос, да еще и память на экземпляры модели.
Хорошо бы как-то запомнить, что мы хотим сделать, а делать это только если кэша нет.
Напишем это.

utils.php:
<code>
...
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;
		
		return call_user_func_array( array($prepared->obj, $prepared->method), $prepared->args );
	}

...
}
...
</code>

Метод run() упрощен, по подсказке davojan.


Использование



controller.php:
<code>
...
$latest = new model_collection('product');
// ничего не грузим сразу
// $latest->load_by( $condition, $order, $limit );
// запоминаем, что мы хотим сделать, в самой переменной для шаблона
$latest = utils::prepare( $latest, 'load_by', $condition, $order, $limit );

$this->export('latest', $latest);
...
</code>


view.php:
<code>
...
<? if !(cacher::start('Cache_Name')) { ?>
	<?
	// только здесь выполняем запланированное, при этом шаблону не нужно знать, что именно делается
	$latest = utils::run( $latest );
	?>
	<ul>
		<? foreach ($latest as $item) { ?>
			<li><?=$item->name();?>: <?=$item->price();?></li>
		<? } ?>
	</ul>

<? cacher::end(); } ?>
...
</code>


Предположим, в вашем фреймворке товары надо было бы грузить статическим методом. Пожалуйста, можно и так:

controller.php:
<code>
...
// ничего не грузим сразу
// $latest = Product::get_latest(...);
// запоминаем, что мы хотим сделать, в самой переменной для шаблона
$latest = utils::prepare( 'Product', 'get_latest', ... );

$this->export('latest', $latest);
...
</code>


В шаблоне же даже ничего не нужно менять.
Этот способ я использую во множестве мест и пока он меня не подводил. Недостаток: пока не удается готовить наборы операций, но в таких извращенных случаях уже можно и метод где-нибудь добавить.

Буду рад комментариям.


Апдейт


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

Страница со списком новостей, экшен 'index' контроллера 'news'.
<code>
...
$news = new model_collection('news'); // или как у вас
$news->load_by( $conditions, $order, $limit );

$this->export('news', $news);
...
</code>


Шаблон со списком новостей вкладывается в лэйаут, в котором присутствует еще куча компонент (новые товары, курсы валют и прочее). Компоненты кэшируются целиком, естественно. Но вот именно «основной» экшен же нам надо выполнить, мы страницу целиком закэшировать чаще всего не можем.
Тут-то и пригождается описанный подход — данные не доставать сразу, а только приготовиться. Можно, конечно, вынести непосредственно вывод новостей в еще один экшен, но таким путем мы почти удвоим количество экшенов, а это явно неудобно.

Так должно быть понятней.
Tags:
Hubs:
+17
Comments 66
Comments Comments 66

Articles