понедельник, 1 марта 2010 г.

Модули в RESTAS

В последние дни провёл большую чистку RESTAS (а также и rulisp), по удалял устаревший код и переработал архитектуру модулей. На последнем моменте хотел бы остановиться подробней.

Изначально, в RESTAS было понятие site и понятие plugin, которые определялись через defsite и define-plugin соответственно. site можно было запустить как web-сайт, а plugin являлся элементом повторного использования, который (например, wiki или форум) можно было использовать повторно на различных сайтах. В результате проведённой переработки я избавился от обоих этих понятий, а вместо них ввёл единственное - module. Предыдущая схема была двухуровневой, текущая - иерархической.

Modules

С точки зрения Common Lisp module это просто package. С точки зрения RESTAS module это набор маршрутов (routes, подробнее о них можно почитать здесь), определяющих структуру web-приложения. module создаётся с помощью макроса restas:define-module, что приводит к созданию пакета с соответствующим именем, плюс проводится некоторая дополнительная инициализация этого пакета. Пример:
(restas:define-module #:hello-world
(:use :cl))
Теперь можно создать в этом модуле несколько маршрутов:
(in-package #:hello-world)

(define-route main ("hello")
"<h1>Hello world!</h1>")
Обращаю внимание на принципиальную важность размещения макроса define-route в пакете (после in-package), связанном с определённым модулем.

И наконец, полученный модуль можно запустить как web-сайт:
(restas:start '#:hello-world :port 8080)
В функцию restas:start (в предыдущих версиях данная функция называлась start-site) можно также передать ключевой параметр :hostname, что позволяет обслуживать несколько виртуальных хостов в рамках одного процесса.

Таким образом, module, разместив в нём несколько маршрутов, можно использовать для запуска web-приложения. Но у модулей есть ещё и другой вариант использования.

Submodules

Меня всегда волновала проблема повторного использования кода и занявшись web я заинтересовался как я могу повторно использовать код для, скажем, форума или вики, на нескольких сайтах, или даже несколько раз в рамках одного сайта? Отличие подобного web-приложения от простой библиотеки, содержащей функции, макросы, классы и т.п в том, что web-компонент также должен содержать информацию об обслуживаемых им url и эту информацию надо использовать в механизме диспетчеризации запросов, правильно определяя код, ответственный за обработку поступившего запроса. В терминах routes (маршрутов) любой повторно используемый web-компонент является просто списком обрабатываемых им маршрутов, а это как раз и есть то, чем являются modules с точки зрения RESTAS. Для повторного использования модуля в рамках другого модуля используется макрос define-submodule. Пример (зависит от кода выше):
(restas:define-module #:test
(:use :cl))

(in-package #:test)

(restas:define-submodule test-hello-world (#:hello-world))

(restas:start '#:test :port 8080)
В данном примере определяется новый модуль test, к нему присоединяется определённый выше модуль hello-world (а получившийся submodule ассоциируется с символом test-hello-world) и модуль test запускается на порту 8080. Хотя в самом модуле test не определён ни один маршрут, но в итоге он способен обрабатывать запросы, поступающие на /hello благодаря включению в себя модуля hello-world.

В таком виде данный функционал не выглядит очень полезным и вот почему. Для успешного повторного использования любого компонента надо уметь его конфигурировать, настраивать его параметры - без этой возможности повторное использование сведётся к технике copy/paste с последующим редактированием кода, что выглядит удручающее само по себе, а в контексте CL ещё имеет и множество технических ограничений (что связано с тем, что понятие package никак не связано с физическим размещением кода на файловой системе). В ООП традиционным способом решения проблем конфигурации является использование классов, но, хотя Common Lisp и имеет сверхмощную поддержку ООП (CLOS + MOP), я решил всё таки отказаться от подобного подхода: возникающие проблемы дизайна, проектирования, бесконечные интерфейсы и наследование всегда существенным образом повышают уровень сложности системы, что кажется мне совершенно излишним для такой простой области, как разработка web-приложений. Для решения этой проблемы в Common Lisp есть ещё один потрясающий механизм: динамические переменные. Сразу реальный пример кода, используемый на lisper.ru для публикации статических файлов:
(restas:define-submodule rulisp-static (#:restas.directory-publisher)
(restas.directory-publisher:*directory* (merge-pathnames "static/" *resources-dir*))
(restas.directory-publisher:*autoindex* nil))
В данном примере используется модуль restas-directory-publisher, о котором я уже писал ранее.

В модуле restas.directory-publisher определены (с помощью defparameter или defvar) несколько глобальных динамических переменных, которые можно использоваться для настройки его работы. В макросе restas:define-submodule некоторые из этих переменных связываются c новыми значения, но эти связывания не применяются непосредственно, а сохраняются в виде контекста для использования в будущем. При обработке запроса диспетчер находит маршрут, определяет связанный с ним submodule, настраивает окружение на основе сохранённого контекста и производит дальнейшую обработку запроса в рамках этого окружения (для этого используется вызов progv). Данный механизм напоминает Buffer-Local Variables в Emacs, я описывал вариант его реализации здесь.

При определении нового модуля с помощью restas:define-module в него (в пакет) добавляется переменная *baseurl*, которая должна быть списком строк и определяет базовый url, по которому будет активизирован данный модуль, по-умолчанию она установленная в nil. Данную переменную можно использовать в restas:define-submodule для задания url, по которому будет включён submodule. Вот более сложный пример использования модуля restas-directory-publisher на сайте lisper.ru (посмотреть этот код в работе можно по адресу http://lisper.ru/files/):
(restas:define-submodule rulisp-files (#:restas.directory-publisher)
(restas.directory-publisher:*baseurl* '("files"))
(restas.directory-publisher:*directory* (merge-pathnames "files/" *vardir*))
(restas.directory-publisher:*autoindex-template*
(lambda (data)
(rulisp-finalize-page :title (getf data :title)
:css '("style.css" "autoindex.css")
:content (restas.directory-publisher.view:autoindex-content data)))))
Кстати, вкупе с предыдущим, данный пример демонстрирует двукратное использование одного модуля с различными режимами работы в рамках одного и того же сайта, без каких-либо конфликтов между собой.

Внутренняя инициализация

Макрос restas:define-submodule позволяет контролировать настройку модуля "снаружи", но порой надо иметь возможность влиять на создаваемый контекст изнутри модуля. Например, модуль restas-planet, который используется для организации Russian Lisp Planet нуждается в механизме для сохранения объекта-робота (он по заданному расписанию считывает ленты и объединяет их в одну), который должен быть вычислен на основе переменных, которые могут быть помещены в контекст submodule. Для такого случая предусмотрен макрос restas:define-initialization, вот реальный код из planet.lisp:
(restas:define-initialization (context)
(restas:with-context context
(when *feeds*
(restas:context-add-variable context
'*spider*
(make-instance 'spider
:feeds *feeds*
:schedule *schedule*
:cache-dir (if *cache-dir*
(ensure-directories-exist (merge-pathnames "spider/"
*cache-dir*))))))))
Здесь производится вычисления объекта spider, который ассоциируется с переменной *spider* и помещается в контекст создаваемого submodule. Данный код будет вычислен при вычислении формы restas:define-submodule. Поскольку создание объекта spider приводит к запуску планировщика (в частности, создаётся таймер), то также надо уметь останавливать их при повторном вычислении формы restas:define-submodule, для этого предусмотрен макрос restas:define-finalization:
(restas:define-finalization (context)
(let ((spider (restas:context-symbol-value context '*spider*)))
(when spider
(spider-stop-scheduler spider))))

Дуализм

Описанная схема подразумевает дуализм: модуль как standalone-приложение, и он же как компонент повторного использования, что позволяет разрабатывать модуль без какого-либо учёта возможности повторного использования, а потом минимальной ценой (просто приводя его к "правильному" дизайну) превращать в многократно используемый компонент. Подобный подход, как мне кажется, позволяет в значительной степени избежать проблем, свойственных традиционному ООП-дизайну.

В качестве демонстрации, вот код для запуска restas-directory-publisher, который я использовал выше как повторно используемый компонент, в виде standalone-приложения:
(restas:start '#:restas.directory-publisher 
:port 8080
:context (restas:make-context (restas.directory-publisher:*baseurl* '("tmp"))
(restas.directory-publisher:*directory* #P"/tmp/")
(restas.directory-publisher:*autoindex* t)))
Теперь открыв в браузере страницу http://localhost:8080/tmp/ можно будет наблюдать содержимое директории #P"/tmp/".


P.S. Описанная архитектура доступна в git версии RESTAS и если вы захотите её опробовать, но ранее уже запускали более старые версии RESTAS, то настоятельно рекомендую удалить .fasl файлы, имеющие к нему какое-либо отношение.

Комментариев нет:

Отправить комментарий