пятница, 26 марта 2010 г.

Небольшое разделение логики и представления в RESTAS

Моё основное приложение состоит из нескольких модулей и один из них полностью посвящён API, доступному из JavaScript, а в качестве формата данных используется JSON (ране я использовал XML, но сейчас полностью от него отказываюсь). Я заметил, что большая часть маршрутов в этом модуле описывается примерно следующим образом:
(define-route api-method ("/path/to/method/:(param1)"
:content-type "application/json")
(json:encode-json
(list :field1 (sql-query "select ...")
:field2 (sql-query "select ..."))))
И так по всему модулю, в обработчике делается один несколько SQL-запросов, результаты которых представленны в виде списков (которые могу быть древовидными) свойств (plist), которые обычно тривиальным образом обрабатываются и полученный большой plist отдаётся клиенту в формате JSON. Ещё я заметил, что те же самые запросы нужно так же выполнять и в другом месте, где происходит формирование html и отдача его клиенту. Фактически, это было оформлено так, что были функции, возвращающие чистые данные в формате s-выражений, которые вызывались для генерации json в одном месте, а для генерации html в другом. При этом, маршруты, ответственные за генерацию JSON вырождались в "неприлично тупое":
(define-route api-method-route ("/path/to/method"
:content-type "application/json")
(json:encode-json (api-method ...)))
Здесь можно было бы написать простой макрос, который бы определял функцию, возвращающую данные, и сразу же определял маршрут, в котором бы результат вызова данной функции конвертировался бы в формат JSON. Но я решил, что можно сделать лучше.

В RESTAS при определении маршрута с помощью define-route создаётся функция, имя которой совпадает с именем маршрута и которая может принимать keyword-параметры, соответствующие параметрам, указанным в шаблоне url. Эту функцию можно вызывать непосредственно из REPL или из других функций, но такой возможностью я пользовался не часто, ибо результатом её работы ранее обычно был HTML (ну или JSON в данном случае) и читать его в REPL как-то не очень приятно. Так вот, мне пришла в голову и я реализовал следующую идею:
(define-route api-method ("/path/to/method/:(param1)/:(param2)"
:content-type "application/json"
:render-method #'json:encode-json)
(list :field1 (sql-query "select ...")
:field2 (sql-query "select ..."))))
По сравнению с первым листингом здесь видно некоторое разделение: в теле маршрута только логика в виде генерации s-выражения с данными, а в свойствах маршрута указано, что перед отдачей этих данных клиенту их необходимо конвертировать в формат JSON с помощью функции #'json:encode-json. Таким образом, фактически, определяется метод, который доступен как внутри приложения, так и для JavaScript-кода.

Но пример с JSON это только частный случай, вообще такая техника может иметь очень разные применения, например, легко себе представить модуль, все маршруты которого возвращают некие объекты из предметной области, я для генерации разметки во всех случаях используется одна и та же generic-функция. Что бы упростить программирования подобного случая (как и моего пример с JSON) и не писать во всех маршрутах одно и то же, я также ввёл переменную модуля {module-name}:*default-render-method*, которая содержит метод отображения по-умолчанию, и равная по-умолчанию же просто #'identity. Для настройки значения этой переменной при определении модуля можно использовать ключевой параметр :default-render-method, например:
(restas:define-module mymodule
(:use #:cl)
(:defaul-render-method #'json:encode-json))
Кстати, в некоторых случаях это даёт возможность настраивать способ отображения при определении submodule с помощью restas:define-submodule и стандартного механизма настройки значений динамических переменных.

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

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