← Blog of oldlisper

Использование policy-based design в RESTAS

· 21.12.2012 00:00
· original author: archimag

Использование policy-based design в RESTAS

В настоящее время в веб-разработке доминирует модель MVC, которая мне никогда особо не нравилась. Во-первых, реальная практика применения данного подхода в веб привела к значительной его дискредитации (Fat Stupid Ugly Controllers), а во-вторых, я считаю модель MVC совершенно недостаточной для разработки современных приложений. В моём понимании, MVC (как и все вариации на эту тему) это такой частный случай паттерна Strategy (также известного как политика), описанного в Design Patterns.

Тут необходимо некоторое лирическое отступление.

Сама книга Design Patterns мне никогда особо не нравилась, ибо написана она плохо, а описанные в ней паттерны делятся на две группы: тривиальные и ужасные. Однако она имеет для меня важное значение в связке с другой книгой - Современное проектирование на С++.

До Александреску считалось, что паттерны GoF не могу быть выражены непосредственно в программном коде и должны использоваться в качестве руководства при принятии конкретных архитектурных решений, а реализация должна каждый раз писаться заново под конкретную ситуацию. Александреску же показал возможность реализации большинства паттернов в виде библиотечного кода, пригодного для повторного использования.

Эта книга (Современное проектирование на С++) в итоге привела меня к Common Lisp - в то время (лет 5-6 назад) в тематических форумах при обсуждении Современное проектирование на С++ упор делался на возможностях обобщённого программирования в C++ и порой появлялись утверждения, что в Common Lisp такие возможности намного круче. Честно говоря, тогда от меня ускользнула суть использованного Александреску метода, иначе бы я понял, что макросы в данном случае совершенно не при чём.

На самом деле Александреску показал, что большинство паттернов GoF могут быть представлены в виде библиотечного кода с помощью упомянутого выше паттерна Strategy (который используется в несколько модифицированном виде: вместо динамического связывания используются статическое, и называется policy), а в качестве технического механизма реализации policy использовал шаблоны C++. Сопутствующее всему этому мета-программирование является вторичным по отношению к главной идиоме - policy-based design.

Вот тут лирическое отступление заканчивается и я могу сформулировать свою главную мысль: я считаю policy-based design мощнейшей техникой проектирования и главная преследуемая мной цель при разработке RESTAS это возможность создания повторно используемых компонентов веб-приложений на базе этой идиомы. Сейчас повторно-используемые компоненты в веб используются в основном в различных CMS и гигантских фреймворках - прекрасное описание проблем данного подхода есть в первой главе Современное проектирование на С++, я не буду его повторять (но рекомендую её прочитать). Кстати, упомянутая популярная модель MVC тривиально выражается в рамках подхода policy-based design, но при этом policy-based design предлагает намного больше.

Для практической реализации поддержки policy-based design в виде кода нужно определить способ технической реализации policy. Александреску использовал для этого шаблоны C++ и в основном посвятил свой труд методам мета-программирования и разработке на их базе вспомогательных инструментов (см., например, списки типов).

Вообще, при первом взгляде кажется, что паттер Strategy настолько прост, что никакая особая техническая поддержка ему не нужна. Скажем, В Python можно просто использовать duck typing. Однако, попытка последовательного следования идиоме policy-based design (освобождённой от специфичных для C++ вещей) приводит к довольно значительным интеллектуальным издержкам (всё надо продумать). Возможно по этой причине подход policy-based design получил распространение в основном в мире C++, где издержки на проектирование полностью окупаются уменьшением издержек на реализацию.

Common Lisp, как и Python, поддерживает duck typing, но кроме того, имеет уникальную (и одну из моих любимых) возможность - динамические переменные (см. статью Переменные в Common Lisp). И именно на динамических переменных основана поддержка policy и policy-based design в RESTAS (см. Модули ).

Например, если рассмотреть pastebin-сервис, доступный по адресу http://lisper.ru/apps/format/, то там сделано примерно так:

(defvar *storage* nil
   "Переменная, через которую будет осуществляться доступ к хранилищу записей"
)


(defgeneric storage-count-notes (storage)
  :documentation "Количество записей"
)


(defgeneric storage-list-notes (storage offset limit)
  :documentation "Список последних записей"
)


(defgeneric storage-get-note (storage id)
  :documentation "Возвращает запись по индефикатору"
)


(defgeneric storage-add-note (storage note)
  :documentation "Добавляет новую запись"
)


(defgeneric storage-remove-note (storage id)
  :documentation "Удаляет запись"
)

Теперь в коде модуля больше нет необходимости знать о том, как устроенна модель и где именно хранятся записи. Пользователь модуля может сам определить где и как должны храниться данные (собственно, так и сделано на lisper.ru).

У данного подхода, однако, есть несколько "неприятных" моментов:

  • Необходимо экспортировать из модуля все generic-методы, описывающие интерфейс *storage*.

  • А ещё лучше поместить их в отдельный пакет, что бы можно было использовать его в секции :use при определении своего пакета.

  • Писать всюду вызовы в стиле

    (storage-count-notes *storage*)

    т.е. всюду писать полное имя метода и указывать *storage* неудобно и раздувает код. Может быть удобно определить алиас

    (defun count-notes ()
      (storage-count-notes *storage*)
    )

    и так для каждого generic-метода.

  • В некоторых случаях и эти алиасы целесообразно разместить в отдельном пакете, что бы ими было удобно пользоваться из нескольких модулей.

Если при разработке модуля будет использоваться несколько политик, определённых подобным образом, то размер вспомогательного, не выполняющего никакой действительно полезной работы, кода может даже превысить размер кода, описывающего логику приложения. Во всяком случае, это утомляет и провоцирует писать "просто".

Для исправления этой проблемы я добавил в RESTAS макрос define-policy, который устраняет необходимость ручного выполнения всей этой работы. Например, в коде движка этого блога определяется следующая политика:

(restas:define-policy datastore
  (:interface-package #:arblog.policy.datastore)
  (:interface-method-template "DATASTORE-~A")
  (:internal-package #:arblog.internal.datastore)
  (:internal-function-template "DS.~A")

  (define-method count-posts (&optional tag)
    "Return a count of the posts that are published"
)


  (define-method list-recent-posts (skip limit &key tag fields)
    "Retrieve the recent posts."
)


  (define-method find-single-post (year month day title)
    "Retrieve a single post, based on date and post title"
)


  (define-method get-single-post (id &key fields)
    "Retrieve a single post, based  on post ID"
)


  (define-method list-archive-posts (min max &optional fields)
    "Retrieve archive posts"
)


  (define-method all-tags ()
    "Retrieve an array of tags"
)


  (define-method insert-post (title tags content &key content-rst published updated)
    "Insert post in the datastore and return the post ID of the created post"
)


  (define-method update-post (id title tags content &key content-rst)
    "Update post in the datastore"
)


  (define-method set-admin (name password)
    "Set administrator name and password"
)


  (define-method check-admin (name password)
    "Check for administrator rights"
)
)

Здесь происходит следующее:

  • Создаются два пакета: #:arblog.policy.datastore и #:arblog.internal.datastore

  • В пакете #:arblog.internal.datastore создаётся переменная *DATASTORE*

  • В пакете #:arblog.policy.datastore создаются generic-методы, описанные с помощью define-method. Использованное в define-method имя преобразуется с помощью вызова format с управляющей строкой, описанной в :interface-method-template. Например, для count-posts будет получено такое имя:

    (format nil "DATASTORE-~A" (string 'count-post)) => "DATASTORE-COUNT-POST"

    которое будет использоваться для создания generic-метода

    (defgeneric arblog.policy.datastore:datastore-count-post (datastore)
      (:documentation "Return a count of the posts that are published")
    )

  • В пакете #:arblog.internal.datastore создаются алиасы для созданных generic-методов, а имя преобразуется с помощью строки, описанной в :internal-function-template

    (format nil "DS.-~A" (string 'count-post)) => "DS.COUNT-POST"

    (defun arblog.internal.datastore:ds.count-post ()
       (arblog.policy.datastore:datastore-count-post arblog.internal.datastore:*datastore*)
    )

Если не указывать опцию :interface-package или :internal-package, то соответствующие пакеты создаваться не будут, а будет использоваться текущий пакет.

Макрос restas:define-policy не зависит от RESTAS, но надо же было его куда-то поместить, плюс он очень полезен именно при создании приложений и компонентов на базе RESTAS.

В качестве примера, для arblog я пока определил следующие политики:

  • datastore

  • theme

  • markup

В планах выделить ещё по крайней мере две:

  • Используемую систему комментариев (сейчас в код зашито использование DISQUS)

  • Способ авторизации владельца блога (сейчас используется HTTP-авторизация)