среда, 2 марта 2011 г.

Про разделение логики и представления

Каждому приходилось однажды решать, что вынести в контроллер, а что оставить в представлении. Как ни крути, но в паттерне MVC звено View совсем избавить от логики довольно сложно. Давно ушли в прошлое те времена, когда html-код генерился прямо из perl/java/php-кода. Теперь суровая необходимость дня – это системы шаблонов. Подход понятен. В идеале, дизайнер вообще не должен ничего знать о бизнес-логике приложения или языке реализации, он просто должен сверстать html-страницу. Программист, соответственно, вообще не должен заботиться о том, какую ему подсовывают html-страницу для интерфейса.

Ну и как? У нас уже есть все это?

Взять, хотя бы технологию Java Server Faces. Во-первых, html-ом там и не пахнет. Страницы пишутся на своем собственном специфичном языке, реализованном поверх xml. Во-вторых, как убрать оттуда логику представления? Ну разве можно отказаться, например, от тегов обработки условий или повторителей? Т. е. здесь считается идиоматичным писать интерфейс пользователя на своем собственном языке, не всегда знакомом дизайнерам, который к тому же содержит фрагменты бизнес-логики.Впрочем, в ASP.NET ситуация ничуть не лучше.

На шаг впереди стоят Python и Ruby. Здесь, по крайней мере, в шаблонах принято использовать html, хотя и он будет разбавлен своими собственными тегами. Бизнес-логику и в этих технологиях не смогли полностью убрать из представления.

На мой взгляд, ближе всех к реализации идеи разделения логики и представления подошел проект Enlive, выполненный Christophe Grand на языке clojure. Представьте себе, здесь код и html-шаблон действительно никак не пересекаются. Единственная точка соприкосновения – подразумеваемая структура документа или id-шники элементов страницы. Если вкратце, то Enlive парсит html-шаблон и строит по нему дерево элементов. Программист имеет возможность задать правила преобразования этой html-страницы в другую html-страницу, заменив попутно данные или атрибуты в нужных тегах. Селекторы доступа к элементам html-страницы здорово похожи на те, что используются в CSS.

Рассмотрим пример.

Создадим проект (lein new www-test1), в файле project.clj пропишем:
(defproject www-test1 "1.0.0-SNAPSHOT"
  :description "FIXME: write"
  :dependencies [[org.clojure/clojure "1.2.0"]
                 [org.clojure/clojure-contrib "1.2.0"]
   [compojure "0.6.0"]
   [enlive "1.0.0-SNAPSHOT"]]
  :dev-dependencies [[lein-ring "0.3.2"]]
  :ring {:handler www-test1.core/app})
Не забываем выполнить lein deps, затем в файле core.clj добавим следующее.
(ns www-test1.core
  (:use compojure.core)
  (:require [compojure.route :as route]
            [compojure.handler :as handler]
     [net.cgrand.enlive-html :as html]))

(html/deftemplate index "www_test1/page.html"
  [ctxt]
  [:#test1] (html/content (:test1 ctxt))
  [:#test2] (html/set-attr "style" "display:none")
  [:#test3] (html/set-attr "style" "display:inherit")
  [:div :ul :li] (html/clone-for [item ["a" "b" "c" "z"]]
     (html/content item)))

(defroutes main-routes
  (GET "/page/:param1" [param1] (index {:test1 param1}))
  (route/resources "/")
  (route/not-found "Page not found"))

(def app
  (handler/site main-routes))

Html-шаблон page.html сохраняем в той же директории, что и core.clj. Выглядит он так:
<html>
<head>My header</head>
<body>

<h3>This is a sample element.</h3>
<div id="test1">If you see this then the app doesn't work!</div><br/>

<div id="test2">
You shouldn't see this.
</div>

<div id="test3" style="display:none">
You should see this.
</div>

<br/>
<div id="test4">
      <ul>
        <li>an item</li>
      </ul>
    </div>

</body>
</html>

Теперь, когда все перед глазами, рассмотрим код core.clj немного подробнее. Макрос deftemplate создает функцию index, которая принимает параметр ctxt, загружает html-шаблон page.html и преобразует шаблон в соответствии с заданными правилами. А правила такие. Для тэга с айдишником test1 установить текст, который получим как результат функции (:test1 ctxt) – подразумевается, что в index мы передадим хэш. Область с айдишником test2 скроем, установив ей атрибут стиля display:none. Область test3 – наоборот, покажем. А в div-е, содержащем список, клонируем единственный тестовый элемент, попутно установив ему текстовые данные из списка a, b, c, z – получим, соответственно, html-список из этих элементов. Таким образом, все, что мы знаем о шаблоне, – это айдишники элементов с данными и список.

Запустить приложение можно командой lein ring server. Тогда, перейдя по ссылке localhost:3000/page/Hello+world, получим ожидаемый результат: Enlive преобразует наш пустой шаблон в html-страницу, содержащую данные.

Вообще, подход Enlive к разделению кода и представления, достаточно необычный. Ну где еще можно встретить что-то подобное? Поэтому большая часть программистов на clojure все еще старается придерживаться устаревшего подхода и либо генерирует html из собственного dsl (например, clj-html), или внедряет clojure прямо в html (например, fleet). Но Enlive постепенно находит и свою аудиторию. С неделю назад в mail-листе Enlive у Кристофа Гранда поинтересовались, как развивается библиотека. Оказалось, что Enlive уже достаточно стабилен, и после нескольких минорных багфиксов ожидается его релиз 1.0. Кроме того, у Кристофа Гранда большие планы по развитию Enlive. В среднесрочных планах ожидается модернизация кода (адаптация под новые возможности clojure 1.2), добавление поддержки xml, «параллелизация» выполнения, добавление Enlive в clojure.contrib. А в долгосрочных планах – повышение уровня абстракции Enlive для работы с любыми типами шаблонов, а также реализация рациональной парадигмы convention over configuration, подразумевающей изначально достаточно хорошую настройку библиотеки.

Одним словом, если раньше самые современные идеи в первую очередь реализовывали в Ruby on Rails (а оттуда уже расползались по всяким Python/Django, PHP, J2EE6 и т. д.), то теперь, похоже, качественный рывок в индустрии совершит clojure со своим новым движком Enlive.

Есть у Enlive и свои проблемы. Поскольку ее разрабатывает всего один человек, она все еще довольно маленькая и не очень хорошо документирована. К счастью, проект opensource-ный, и каждый может в нем поучаствовать. Спросите себя, сколько вы используете бесплатного свободного софта, и как часто что-либо отдаете взамен? Как раз сейчас у вас есть отличная возможность что-то сделать и для OpenSource. Если вам понравился Enlive – напишите про него в своем блоге. Напишите tutorial, где подробно, для новичков, расскажете, как пользоваться Enlive. Дополните документацию на сайте проекта или предложите новую функциональность. Сейчас проект все еще очень мал, и все, что вы для него сделаете, будет очень заметным.

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

  1. Pure тоже разделяет, на жаваскрипт.
    http://beebole.com/pure/

    ОтветитьУдалить
  2. Ну так то ж клиентская технология, а я про server-side.

    ОтветитьУдалить
  3. Очень напоминает Lift, в нём и селекторы появились некоторе время назад

    ОтветитьУдалить
  4. Да, действительно, похоже. Но и отличия тоже есть. В Enlive мы просто задаем декларативный набор правил преобразования одного документа в другой. Что до Lift-а, то там, судя по примерам, используется несколько иная техника, ближе к императивной. Хотя, судя по всему, тоже классный фреймворк.

    ОтветитьУдалить
  5. Ну отличия-то ясное дело, что есть.
    Но по сути же вьюшки это функциии NodeSeq -> NodeSeq, не знаю что в этом императивного.
    А фреймворк интересный, но больно много туда "магии" напихали.

    ОтветитьУдалить
  6. Т.е. по сути мы имеем очередную реинкарнацию вывернутого наизнанку XML/XSLT. Прелестно, прелестно.

    ОтветитьУдалить
  7. Ну вообще Enlive проще использовать, чем XSLT, да и писать на Clojure намного приятнее, чем на xml-подобном DSL.

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