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

Автор: dmmd
Источник: habrahabr

Наверняка большинство программистов, работающих с современными веб-фрейворками, реализующими схему 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:


...

<!--f !(cacher::start(`Cache_Name`)) {-->

<!--	// только здесь выполняем запланированное, при этом шаблону не нужно знать, что именно делается

	$latest = utils::run( $latest );

-->
    <!--oreach ($latest as $item) {--> <? if !(cacher::start(`Cache_Name`)) { ?> <? // только здесь выполняем запланированное, при этом шаблону не нужно знать, что именно делается $latest = utils::run( $latest ); ?> <ul> <? foreach ($latest as $item) { ?> <li><?=$item->name();?>: <?=$item->price();?></li> <? } ?> </ul> <? cacher::end(); } ?> <!--acher::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);

...




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

Так должно быть понятней.


Опубликовал admin
31 Мар, Понедельник 2008г.



Программирование для чайников.