Наверняка большинство программистов, работающих с современными веб-фрейворками, реализующими схему MVC, сталкивалось с таким небольшим затруднением: кэширование фрагмента View.
Хорошие фреймворки предлагают инструменты для полного кэширования страниц, фрагментарного, или кэширования экшенов. Недавно я посмотрел 90 выпуск подкаста Railscasts, посвященный именно фрагментарному кэшированию в Ruby on Rails и уважаемый автор решал проблему, как мне показалось, неоптимально.
Опишу ситуацию.
Мы в шаблоне страницы и хотим закэшировать ее часть, например, список новых товаров. Пока все хорошо, мы пользуемся встроенными во фреймворк удобными средствами и в две-три строчки окружаем блок — ура, он кэшируется. Но — чу!, контроллер-то об этом ничего не знает и продолжает выполнять свою работу по подготовке данных для View. Естественно, ведь проверка наличия кэша осуществляется уже из шаблона, а контроллер к тому моменту отработал.
Автор подкаста показывает некрасивое решение — перенос кода для подготовки данных в шаблон и тут же, естественно, отметает его, как «ugly». Что он предлагает — перенести этот код в модель. То есть, в модели товара создается специальный метод, который выбирает новые товары, и этот метод вызывается из шаблона.
Это лучше, чем первый вариант, но все же недостаточно хорошо, так как в модели приходится реализовывать вещи, которые могут понадобиться в одном только месте, а при смене интерфейса сайта могут оказаться ненужными и скорее всего останутся болтаться в коде просто так.
Я работаю со своим фреймворком на PHP, и пример буду писать на PHP, но решение простое и реализуется на любом скриптовом язке.
view.php:
controller.php:
Метод load_by(...) выполняет один или несколько запросов к базе данных и формирует набор моделей класса Product. То есть, тратятся ресурсы на запрос, да еще и память на экземпляры модели.
Хорошо бы как-то запомнить, что мы хотим сделать, а делать это только если кэша нет.
Напишем это.
utils.php:
Метод
controller.php:
view.php:
Предположим, в вашем фреймворке товары надо было бы грузить статическим методом. Пожалуйста, можно и так:
controller.php:
В шаблоне же даже ничего не нужно менять.
Этот способ я использую во множестве мест и пока он меня не подводил. Недостаток: пока не удается готовить наборы операций, но в таких извращенных случаях уже можно и метод где-нибудь добавить.
Буду рад комментариям.
В комментариях мне указывают на наличие компонентов и возможности кэшировать их целиком. Я вынужден пояснить — заметка не об этом. Приведу другой пример, из реальной жизни.
Страница со списком новостей, экшен 'index' контроллера 'news'.
Шаблон со списком новостей вкладывается в лэйаут, в котором присутствует еще куча компонент (новые товары, курсы валют и прочее). Компоненты кэшируются целиком, естественно. Но вот именно «основной» экшен же нам надо выполнить, мы страницу целиком закэшировать чаще всего не можем.
Тут-то и пригождается описанный подход — данные не доставать сразу, а только приготовиться. Можно, конечно, вынести непосредственно вывод новостей в еще один экшен, но таким путем мы почти удвоим количество экшенов, а это явно неудобно.
Так должно быть понятней.
Хорошие фреймворки предлагают инструменты для полного кэширования страниц, фрагментарного, или кэширования экшенов. Недавно я посмотрел 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>
Шаблон со списком новостей вкладывается в лэйаут, в котором присутствует еще куча компонент (новые товары, курсы валют и прочее). Компоненты кэшируются целиком, естественно. Но вот именно «основной» экшен же нам надо выполнить, мы страницу целиком закэшировать чаще всего не можем.
Тут-то и пригождается описанный подход — данные не доставать сразу, а только приготовиться. Можно, конечно, вынести непосредственно вывод новостей в еще один экшен, но таким путем мы почти удвоим количество экшенов, а это явно неудобно.
Так должно быть понятней.