Что имеем
Начну с того, что расскажу немного о проекте, в котором работаю и как там все пишется. Может не у одних нас так…
Проект представляет из себя CRM систему, разрабатываемую специально для клиентов одного сегмента бизнеса. Проекту лет 6 и команда разработчиков состоит из 10 человек. Язык: C++ и PL/SQL.
Наша система исползует Plain Old XML, так уж повелось. И на используемые XML нет схем, по большей части. Что тут говорить, если юнит тесты прививаются здесь только второй год и менеджер до сих пор упрекает за время, потраченное на их написание. Да ладно…
По ходу дела все улучшения появляются тогда, когда текущее положение дел порядком надоедает и становится невмоготу. Так же произошло и сейчас.
Как и многие, я думаю, мы многое делаем неоптимально и не лучшим образом. Главное делаем. Пример с XML не исключение.
Стандартная практика
Вот пример кода построения XML документа в функции:
::std::string request = clearCreateNode("book", createNode("author", "", "name='Freddy' surname='Smith'") + createNode("author", "", "name='Bill' surname='Joe'") + clearCreateNode("quote", "This is the best unknown book I've ever quoted!" + createNode("author", "", "name='Mr. Bob'")), "isbn='123456789' name='Some book' year='2011'");
Стандартная практика. Почему не
stream? — потому, что есть вложенность элементов. Хотя stream это первое, что приходит в голову и для императивного подхода вряд ли можно получить что-то отличное от «скармливания правого объекта левому».Ладно, привыкли. Можно потерпеть запятую, но терпеть для каждого тега повторение названия функции
clearNode или clearCreateNode — я уже не смог. И не важно, насколько коротким может быть название, хоть просто "T" — это маразм. Сродни тому, чтобы в диалоге с кем-то перед каждой репликой повторять: «я говорю», а перед каждым предложением говорить — «предложение:», а перед словом — «слово:» и так далее. Не маразм ли?В чём проблема
Кто-то скажет, что XML сам по себе избыточен — хорошо, что в коде хоть не приходится тег закрывать…
Да, это так, но это не повод вести себя также. Мне больше нравится общаться с человеком, че�� с идиотом. Когда мы оба понимаем контекст и семантику диалога. Когда можем использовать сленг и друг-друга понимаем. Или, когда можно только один раз объяснить правила конструкции и дальше общаться принимая их во внимание.
Например в Object Pascal есть такая конструкция:
with (Object) begin a := 1; b := "foobar"; end;
Приятно, однако, когда можно сказать: «Сейчас работаем с объектом Object, поэтому его свойство 'a' установим в 1, а свойство 'b' в 'foobar'».
«Ни капли жира!»
Возвращаясь к XML, вот как-то так выглядит XML документ на DSL, например в Lisp:
((book :isbn "123456789" :name "Some book" :year "2011") ((author :name "Freddy" :surname "Smith")) ((author :name "Bill" :surname "Joe")) (quote "This is the best unknown book I've ever quoted!" (author :name "Mr. Bob")))
Если не обращать внимание на скобки, то здесь нет «ни капли жира!» Если в XML не обращать внимание на символы <,>,/,= и опустить избыточность закрывающего тега, то получатся одинаковые сообщения.
<book isbn='123456789' name='Some book' year='2011'><author name='Freddy' surname='Smith'/><author name='Bill' surname='Joe'/><quote>This is the best unknown book I've ever quoted!<author name='Mr. Bob'/></quote></book>
Или с pretty print:
<book isbn='123456789' name='Some book' year='2011'> <author name='Freddy' surname='Smith'/> <author name='Bill' surname='Joe'/> <quote>This is the best unknown book I've ever quoted! <author name='Mr. Bob'/> </quote> </book>
Можно ли получить что-то похожее по смыслу в C++?
Результат
В общем, вот, что в итоге получилось:
const xml::Tag book ("book"); const xml::Tag author ("author"); const xml::Tag quote ("quote"); xml::Element request = xml::Element() (book ("isbn", "123456789") ("name", "Some book") ("year", "2011"), xml::Element() (author ("name", "Freddy") ("surname", "Smith")) (author ("name", "Bill") ("surname", "Joe")) (quote, xml::Element() ("This is the best unknown book I've ever quoted!") (author ("name", "Mr. Bob"))));
Или с выделением списка тэгов для переиспользования в проекте:
namespace library { namespace books { const xml::Tag book ("book"); const xml::Tag author ("author"); const xml::Tag quote ("quote"); } }
И тогда:
namespace bks = ::library::books; xml::Element request = xml::Element() (bks::book ("isbn", "123456789") ("name", "Some book") ("year", "2011"), xml::Element() (bks::author ("name", "Freddy") ("surname", "Smith")) (bks::author ("name", "Bill") ("surname", "Joe")) (bks::quote, xml::Element() ("This is the best unknown book I've ever quoted!") (bks::author ("name", "Mr. Bob"))));
Полностью избавиться от избыточности не удалось (всё-таки указывать
xml::Element() в точке вложения приходится). Но прогресс есть: уже не надо твердить для каждого тега, что это ТЕГ, и можно собирать аналитику для всего строящегося документа и делать на её основе например pretty print или ещё что-нибудь, так как имеем уже дело с объектом, точнее с деревом с общим корнем.Выводы
Можно выделить интерфейс такого DSL, состоящий из:
- семантических операторов класса
Element:
template <class T> void addContent(const T & content); void addEmptyTag(const Tag & tag); template <class T> void addTag(const Tag & tag, const T & content); void addTagElement(const Tag & tag, const Element & element);
- DSL операторов класса
Element:
template <class T> inline Element & operator () (const T & content) { addContent(content); return *this; } inline Element & operator () (const Tag & tag) { addEmptyTag(tag); return *this; } template <class T> inline Element & operator () (const Tag & tag, const T & content) { addTag(tag, content); return *this; } template <> inline Element & operator () (const Tag & tag, const Element & element) { addTagElement(tag, element); return *this; }
- интерфейса класса
Tag, который разделять не так интересно, ввиду отсутствия вложенности у тэгов имён и атрибутов.
И теперь остаётся только придумывать различные реализации для компиляции кода написанного на DSL в XML. Вот например лабораторный вариант:
template <class T> void addContent(const T & content) { ::std::ostringstream oss; oss << content; buf += http_encode(oss.str()); } void addEmptyTag(const Tag & tag) { buf += createNode(tag.name, "", tag.args); } template <class T> void addTag(const Tag & tag, const T & content) { ::std::ostringstream oss; oss << content; buf += createNode(tag.name, oss.str(), tag.args); } void addTagElement(const Tag & tag, const Element & element) { buf += clearCreateNode(tag.name, element.str(), tag.args); }
Для полноты картины, приведу код класса
Tag:class Tag { public: // DSL операторы: template <class K> Tag & operator () (const K & attr_name) { ::std::ostringstream oss; oss << " " << attr_name; args += oss.str(); return *this; } template <class K, class T> Tag & operator () (const K & attr_name, const T & value) { ::std::ostringstream oss; oss << " " << attr_name << "='" << value << "'"; args += oss.str(); return *this; } // Копирующие варианты DSL операторов: template <class K> Tag operator () (const K & attr_name) const { Tag copy(*this); return copy (attr_name); } template <class K, class T> Tag operator () (const K & attr_name, const T & value) const { Tag copy(*this); return copy (attr_name, value); } // Данные и инициализация: template <class T> Tag(const T & name) : name(name) {} Tag(const Tag & ref) : name(ref.name), args(ref.args) {} ::std::string name; ::std::string args; };
… и пример XSLT для перевода простого XML документа в код на этом DSL:
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:fn="http://www.w3.org/2005/xpath-functions"> <xsl:output method="text" version="1.0" encoding="windows-1251" indent="no"/> <xsl:template match="/"> <xsl:apply-templates/> ; </xsl:template> <xsl:template match="text()">"<xsl:value-of select="."/>"</xsl:template> <xsl:template match="attribute::*"> ("<xsl:value-of select="name()"/>", "<xsl:value-of select="."/>")</xsl:template> <xsl:template match="*"><xsl:if test="child::node()">xml::Element()</xsl:if> (<xsl:value-of select="name()"/><xsl:apply-templates select="attribute::*"/><xsl:if test="child::node()">, <xsl:apply-templates/></xsl:if>)</xsl:template> </xsl:stylesheet>
В итоге, можно сказать, что получилось-таки малой кровью написать DSL для С++. Теперь следующим пунктом будет включение SQL кода в C++…
Спасибо за потраченное время, надеюсь, что топик Вам понравился.