Что имеем
Начну с того, что расскажу немного о проекте, в котором работаю и как там все пишется. Может не у одних нас так…
Проект представляет из себя 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++…
Спасибо за потраченное время, надеюсь, что топик Вам понравился.