среда, 21 октября 2009 г.

Маршруты в RESTAS

Routes является ключевой концепцией RESTAS, собственно, с них и началась разработка системы. Первоначально, идею routes, в более упрощённом виде, я придумал самостоятельно, когда разрабатывал REST-службу для одного приложения. Это был мой первый опыт web-разработки и я ничего не знал о других системах (моё приложение изначально было реализовано на C++ как модуль для веб-сервера Apache, а в последующем переписано на Python под mod_python). По мере моего знакомства в веб-технологиями я узнал, что "придуманная" мной система диспетчеризации называется routes и, насколько я понимаю, впервые появилась в Ruby On Rails. Если вы не знакомы с данной концепцией, то могу порекомендовать следующее описание (Python-реализация): http://routes.groovie.org/manual.html. Другой, концептуально очень близкой системой, является URLConf в Django.

Когда я стал использовать Common Lisp для разработки web-приложений, то больше всего меня расстраивала система диспетчеризации запросов web-сервер Hunchentoot, точнее фактическое её отсутствие (а входящий в поставку define-easy-handler просто ужасен). Для решения данной проблемы я решил написать cl-routes: реализацию системы маршрутов для Common Lisp, но без привязки к модели MVC. Первоначально я планировал портировать Python-реализацию этой идеи на Common Lisp и принялся изучать её исходный код. Первое, что меня смутило - размер исходного кода, слишком много букв... А второе меня так просто добило: все маршруты транслируются в регулярные выражения и при обработке запроса идёт простой перебор всех выражений до первого совпадения. Брр... Фактически, системы маршрутизации RoR и Django работают точно так же, поиск ведётся прямым перебором. В документации к Django написано про производительность буквально следующее:
Each regular expression in a urlpatterns is compiled the first time it's accessed. This makes the system blazingly fast.
ага, как же, если количество маршрутов будет исчисляться сотнями или даже тысячами (что вполне реалистично для больших приложений), то такая реализация может легко стать главным тормозом всего приложения. И между прочим эта проблема достаточно хорошо известна в RoR, где предлагаются различные пути для увеличения производительности готовых приложений за счёт отказа от многих преимуществ маршрутов и увеличения нагрузки на разработчиков. В Django, в общем, есть кое-какие пути для увеличения производительности (за счёт задания префиксов на группы) (я правда не знаю, повышает ли это реально производительность или нет, но принципиально может), но это опять таки ведёт к увеличении нагрузки на программиста и не всегда может быть применено.

В общем, к существующим системам у меня есть две основные претензии:
  • Использование универсального и тяжеловесного механизма регулярных выражений, который просто не нужен для разбора url
  • Поиск в линейном списке (с некоторыми оговорками для Python-систем) - ведь структура сайта обычно естественным образом отображается в виде дерева, а значит возможен и более эффективен (чем больше сайт, тем более эффективен) поиск в дереве
cl-routes не используют регулярных выражений (вместо этого используется механизм унификации), и компилирует все маршруты в одно дерево, что предоставляет возможность более эффективного поиска. Описание первоначальной идеи реализации можно посмотреть в моём старом сообщении - она с тех пор не очень сильно изменилась. При задании маршрутов всегда возможны конфликты, когда один и тот же url соответствует нескольким шаблонам. В системах с последовательным поиском подходящего маршрута всегда выбирается первый, cl-routes пытается выбрать наиболее специальный, например тот, в котором больше переменных, или который имеет более длинную статическую часть. Точного описания алгоритма разрешения коллизий пока дать не могу, но у меня до сих не было с этим никаких проблем, поведение системы вполне разумно и по большей части соответствует интуитивным ожиданиям.

Кроме того, что в RoR, что в Django для внесения изменений в схему диспетчеризации необходимо внести изменения в несколько файлов, я же предпочитаю держать определение схемы url и её обработчик в одном месте, не размазывая логику по разным частям программы. В RESTAS задать новый маршрут можно так:
(restas:define-route article ("articles/:author/:item"
:method :get
:content-type "text/plain")
(format nil
"Author: ~A~Article: ~A"
author
item))
Т.е. в одном месте задаётся и шаблон url, и обработчик, при этом в теле обработчик сразу же доступны переменные, заданные в шаблоне url. Вложенный обработчик, по-умолчанию, может вернуть строку или "octets array", либо целое число (которое интерпретируется как код статуса ответа), либо pathname (в этом случае вызывается hunchentoot:handle-static-file, которая, в отличие от систем на базе Ruby или Python работает достаточно быстро и реальной необходимости в дополнительных серверах для "статики", таких как ngix, просто нет). Также, легко можно добавить поддержку и обработку любых типов возвращаемых объектов.

При определении маршрута через define-route необходимо указать символ (в приведённом примере - 'article), который будет являться его именем. Во-первых, это даёт возможность в любой момент переопределить маршрут (в том числе и шаблон url, лишь бы имя оставалось неизменным) простой отправкой кода в REPL (например, с помощью M-C-x в SLIME). Во-вторых, это позволяет использовать данный символ для генерации url:
(restas:genurl 'article
:author "archimag"
:item "introduction-in-routes")
или для перенаправления:
(restas:redirect 'article
:author "archimag"
:item "introduction-in-routes")
(возможны и другие использования)

В Django шаблон url задаётся регулярным выражением и возможности сопоставить одному и тому же регулярному выражению несколько различных обработчиков (например, для различных типов запроса: get, post и т.п.) нет, в cl-routes маршрут это объект класса (либо производный от него) routes:route, для которого можно переопределить метод route-check-conditions, что позволяет проводить диспетчеризацию не только на основе шаблона url. Это используется в RESTAS и позволяет одному и тому же шаблону url сопоставить несколько обработчиков, например, для разных типов (get, post и т.п.) запросов (что позволяет сократить количество if-лапши в коде). В ближайшее время (на днях, может даже сегодня) я добавлю в RESTAS возможность проводить произвольные проверки во время поиска подходящего обработчика: например сейчас у меня есть необходимость в разных обработчиках для одного и того же url в зависимости от роли пользователя в системе.

Вот несколько возможных шаблонов url:
/forum/:chapter/:topic/:message
/forum/archives/:year/:month/:day
/forum/:chapter/rss
Или с объединением строк:
/book/:(name)-:(chapter).html
/:(feed).:(format)
Также поддерживаются wildcard-параметры (с тем ограничением, что такой параметр может использоваться в шаблоне только один раз):
/mydoc/*items
/*path/rss

5 комментариев:

  1. А что если хочется дату ввиде YYYY-MM-DD или там чтобы :item был только цифровой. Руками в обработчике проверять?

    Кстати, когда url много, имхо все-таки удобнее, когда они расположены рядом. В django при желании можно сделать так чтобы url был рядом с обработчиком - погугли snippet @url decorator. А вот в restas получается что отделить routes от обработчиков нельзя.

    з.ы. Почему ни вкопировать нельзя ни стрелки не работают в поле комментария??? (firefox 3.0, ubuntu)

    ОтветитьУдалить
  2. > Руками в обработчике проверять?
    Конкретно этот момент (специальная поддержка для ограничений значений и типов переменных) будет реализован достаточно скоро, cl-routes это позволяет, я просто ещё не решил как лучше интегрировать это в define-route. Мало того, если будет заявленно, что переменная имеет тип integer, то помимо проверки, в код обработчика будет передаваться именно integer, а не просто строка.

    > А вот в restas получается что отделить routes
    > от обработчиков нельзя.
    Можно, простейший способ - в теле define-route просто вызывать фукнцию, которую можно расположить где угодно.

    Кроме того, cl-routes позволяет и более сложное использование, ибо маршрут - это объект класса route, от него можно унаследовать, переопределить generic-функции, ну много чего сделать интересного :) Просто это несколько более сложная тема, а простейший сейчас вариант - это использовать define-route

    > Почему ни вкопировать нельзя ни стрелки
    Не знаю, у меня на рабочей машине работает, а вот дома нет, я не разбирался...

    ОтветитьУдалить
  3. > Кроме того, что в RoR, что в Django для внесения изменений в схему диспетчеризации необходимо внести изменения в несколько файлов

    Ну ладно, может в RoR и так, но в джанго-то исправляем urls.py и всё. Какие несколько файлов?

    ОтветитьУдалить
  4. > но в джанго-то исправляем urls.py и всё
    Если просто шаблон url поправить, то и в RoR, конечно, можно в одном файле всё изменить. А я имею ввиду необходимость размещения обработчика в одном месте, а определения маршрута, который он будет обрабатывать, в другом.

    ОтветитьУдалить