IT Notes

Паттерн Строитель и XML

Введение

Паттерн Строитель представляет собой проектное решение для задач компоновки составных структур и объектов. XML же является языком разметки структурированных данных, который в настоящее время довольно широко используется и стал практически стандартом при обмене информацией. В Qt предусмотрен набор классов, обеспечивающих возможность формирования XML-документов, однако ими не так легко и удобно пользоваться, как хотелось бы. В этой заметке мы создадим класс-Строителя на основе инструментальных средств Qt, который позволит существенно упростить процесс компоновки XML.

Коротко об XML

Проще всего разобраться с форматом XML на примере:

<?xml version="1.0" encoding="utf-8"?>
<user_groups>
    <group name="http">
        <user>aleksey</user>
        <user>sergey</user>
    </group>
    <group name="ftp"/>
    <group name="wheel">
        <user>root</user>
        <user>mikhail</user>
    </group>
</user_groups>

Здесь использован не полный перечень элементов разметки XML, однако уже этого вполне достаточно в большинстве случаев. Разберемся со структурой формата поподробнее. Если вы знакомы с HTML, то наверняка заметили сходство. HTML и правда очень похож по виду на XML. Существует даже разновидность разметки HTML, которая полностью основана на XML (называется XHTML). Однако учитывайте, что (X)HTML - язык гипертекстовой разметки, то есть имеет конкретное назначение, в то время как XML - расширяемый язык разметки и может использоваться для описания произвольных данных. Таким образом, XHTML и XML находятся в отношении частное-общее.

В первой строке примера мы определили инструкции обработки с указанием версии XML и кодировки. Далее мы открываем корневой элемент, который соответствует тегу user_groups. Дочерними элементами по отношению к user_groups являются группы, для которых мы выбрали тег group. Обратите внимание, что для тега group мы предусмотрели атрибут name, в котором указывается имя группы. В группах имеются вложенные теги, соответствующие пользователям, которые в них входят. Имя пользователя указывается в текстовом формате в теге user. Если внутри тега ничего размещать не требуется, то он записывается в формате <[имя тега] [аттрибуты]/>. Например, подобным образом мы определили группу ftp.

Следует заметить, что имена тегов и атрибутов, а также структуру XML-документа в представленном примере мы определили сами. Если вас заинтересовал формат XML и вы хотите узнать о нем побольше, то найти его формальное описание в интернете не составит труда.

Паттерн Строитель

Идея паттерна Строитель довольно проста. Она заключается в том, что вы разбиваете некую составную задачу компоновки сложного объекта на ряд более мелких. Это позволяет упростить логику создания объекта, ведь не приходится передавать сразу большой набор параметров за один заход, а иногда это и невозможно вовсе. С помощью Строителя процесс формирования проходит поэтапно.

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

Я думаю, вы уже догадываетесь, каким образом согласуются XML и паттерн Строитель. Поэтому мы приступаем.

Строим XML с помощью Qt

Начнем мы разработку не с самого класса-Строителя, а с пользовательского кода, который будет использовать этот класс. В какой-то мере такая методика напоминает TDD (см. Разработка через тестирование в Qt). Мне кажется, что код формирования XML-документа должен выглядеть следующим образом:

int main() {
    QString xml =
            XMLBuilder().
            begin( "user_groups" ).
                begin( "group" ).attr( "name", "http" ).
                    add( "user", "aleksey" ).
                    add( "user", "sergey" ).
                end().
                begin( "group" ).attr( "name", "ftp" ).
                end().
                begin( "group" ).attr( "name", "wheel" ).
                    add( "user", "root" ).
                    add( "user", "mikhail" ).
                end().
            end().getXML();

    std::cout << xml.toStdString() << std::endl;

    return 0;
}

В представленном коде мы компонуем именно тот документ с информацией о группах пользователей, который рассматривали, когда говорили о формате XML. Из этого фрагмента следует, что класс-Строитель называется XMLBuilder. Особенности его использования понять довольно легко, если выполнить сопоставление с XML-документом, которому он соответствует. Наблюдается четкая взаимосвязь даже в форме записи. Класс XMLBuilder содержит всего 5 функций: begin(), end(), add(), attr() и getXML(). Функция begin() открывает тег, принимая его имя в качестве аргумента. Каждому вызову begin() должен соответствовать вызов end(), который определяет место закрывающего тега. Для добавления текстовых элементов используется функция add(), которая принимает имя тега и его текстовое содержимое. Чтобы определить атрибуты тега, используется функция attr(), которая принимает имя атрибута и его значение. Ожидается, что вызов attr() привязывается к тегу, который был добавлен последним. Сформированный XML-документ в текстовом виде возвращается с помощью getXML().

Подобное выстраивание в цепочки - довольно интересный прием. Например, на нем во многом основан код библиотеки JQuery. Он достигается за счет того, что каждая из функций (кроме getXML() в нашем случае) возвращает ссылку на сам объект, для которого был произведен вызов.

Если вы посмотрите на приведенную структуру повнимательнее, то становится понятно, что реализовать такой класс для генерации XML можно без особых сложностей с нуля на чистом C++. Однако мы воспользуемся тем, что готов предложить Qt. Посмотрим на соответствующий код:

#include <QDomDocument>
#include <QDomElement>
#include <QStack>

class XMLBUILDERSHARED_EXPORT XMLBuilder {
public:
    enum ErrorCode {
        EMPTY_TAG_NAME,
        EMPTY_ATTR_NAME,
        COULD_NOT_ADD_ATTR,
        TAG_NOT_STARTED
    };

    class XMLException : public std::exception {
    public:
        explicit XMLException( ErrorCode code ) : m_code( code ) { }
        ~XMLException() throw() { }

        const char* what() const throw() {
            switch( m_code ) {
            case EMPTY_TAG_NAME: return "Empty tag name!";
            case EMPTY_ATTR_NAME: return "Empty attribute name!";
            case COULD_NOT_ADD_ATTR: return "Could not add attribute!";
            case TAG_NOT_STARTED: return "There are no started tags!";
            default: return "Unknown error!";
            }
        }

    private:
        ErrorCode m_code;
    };

public:
    explicit XMLBuilder( const QString& docName = "" );

    XMLBuilder& begin( const QString& tagName ) throw( XMLException );
    XMLBuilder& add( const QString& tagName, const QString& content ) throw( XMLException );
    XMLBuilder& attr( const QString& attrName, const QString& content ) throw( XMLException );
    XMLBuilder& end() throw( XMLException );

    QString getXML() const;

private:
    void appendElement( const QDomElement& element );

private:
    QDomDocument m_doc;
    QStack< QDomElement > m_elementsStack;
    QDomElement m_currentElement;
};

Здесь мы объявили класс XMLBuilder в соответствии с нашей задумкой. Кроме того, следует заметить, что во время формирования XML могут возникнуть ошибки. Для обработки этих ошибок мы предусмотрели класс исключения, а также перечисление ErrorCode с их кодами. Более подробно о применении исключений вы можете почитать в моей заметке Обработка ошибок: Исключения. Помимо тех пяти функций-членов, которые мы уже обсуждали, добавилась еще одна закрытая функция appendElement(). Скоро мы посмотрим на ее реализацию. К тому же, еще раз обращаю ваше внимание на то, что для построения цепочек все функции компоновки возвращают ссылку на объект, для которого был произведен вызов.

Теперь коротко пройдемся по полям класса XMLBuilder. Компоновка XML-документа будет проходить на основе QDomDocument, который в Qt сам представляет некую разновидность Строителя. Чтобы учесть рекурсивную структуру с вложенными тегами, мы используем стек m_elementsStack для фиксации корневых элементов на каждом уровне. Про стеки и многие другие структуры данных я писал в заметке Основные структуры данных, поэтому здесь мы не будем углубляться в подробности, однако замечу, что в представленном случае мы имеем дело с типичным вариантом его использования. Элемент разметки, который соответствует текущему тегу, нам тоже пригодится. Поэтому и он входит в перечень полей класса (m_currentElement). Потребность в нем возникнет при добавлении атрибутов.

Вот и пришло время заняться реализацией. Она получилась довольно простой:

static const QString XML_INSTRUCTION_CONTENT = "version=\"1.0\" encoding=\"utf-8\"";

XMLBuilder::XMLBuilder( const QString& docName ) : m_doc( docName ) {
    QDomProcessingInstruction instructions =
        m_doc.createProcessingInstruction( "xml", XML_INSTRUCTION_CONTENT );
    m_doc.appendChild( instructions );
}

XMLBuilder& XMLBuilder::begin( const QString& tagName ) throw( XMLException ) {
    if( tagName.isEmpty() ) {
        throw XMLException( EMPTY_TAG_NAME );
    }

    m_currentElement = m_doc.createElement( tagName );
    appendElement( m_currentElement );

    m_elementsStack.push( m_currentElement );

    return *this;
}

XMLBuilder& XMLBuilder::end() throw( XMLException ) {
    if( m_elementsStack.isEmpty() ) {
        throw XMLException( TAG_NOT_STARTED );
    }

    m_currentElement = m_elementsStack.pop();

    return *this;
}

XMLBuilder& XMLBuilder::attr( const QString& attrName, const QString& content ) throw( XMLException ) {
    if( attrName.isEmpty() ) {
        throw XMLException( EMPTY_ATTR_NAME );
    }

    if( m_currentElement.isNull() ) {
        throw XMLException( COULD_NOT_ADD_ATTR );
    }

    m_currentElement.setAttribute( attrName, content );

    return *this;
}

XMLBuilder& XMLBuilder::add( const QString& tagName, const QString& content ) throw( XMLException ) {
    if( tagName.isEmpty() ) {
        throw XMLException( EMPTY_TAG_NAME );
    }

    m_currentElement = m_doc.createElement( tagName );
    appendElement( m_currentElement );

    QDomText textNode = m_doc.createTextNode( content );
    m_currentElement.appendChild( textNode );

    return *this;
}

QString XMLBuilder::getXML() const {
    return m_doc.toString();
}

void XMLBuilder::appendElement( const QDomElement& element ) {
    if( m_elementsStack.isEmpty() ) {
        m_doc.appendChild( element );
    } else {
        QDomElement currentRoot = m_elementsStack.top();
        currentRoot.appendChild( element );
    }
}

Не вижу смысла разбирать приведенный пример построчно. Функции довольно короткие, а их назначение мы уже разбирали. Давайте просто пройдемся по общим принципам использования инструментов Qt для создания XML-документов. Во-первый не забудьте, что для успешной компиляции приведенного проекта в pro-файле нужно подключить соответствующий модуль:

QT += xml

Инициализация документа происходит в конструкторе XMLBuilder(). Объекту m_doc передается имя документа, а затем к нему прикрепляются инструкции обработки с указанием версии XML и кодировки. Сейчас эта функциональность довольно смазана. Более того, все зашито в коде в виде констант. Однако это не так критично в рамках примера. Но лучше было бы предоставить дополнительные возможности настройки этих (а может и других) параметров с помощью дополнительных аргументов конструктора.

Создание XML-элементов в соответствии с именем тега осуществляется с помощью функции createElement() класса документа QDomDocument. А для формирования текстового элемента QDomText используется функция-член createTextNode() все того же класса документа. Прикрепление новых вложенных элементов мы осуществляем в нашей закрытой функции appendElement(). Если элемент оказался первым, то мы закрепляем его в качестве корневого. Иначе мы берем уже существующий корневой элемент из вершины стека, который соответствует текущему уровню вложенности, и делаем новый элемент его потомком. Атрибуты прикрепляются к последнему добавленному элементу с помощью функции setAttribute(). В остальном же весь код основывается на комбинировании указанных функций класса QDomDocument и использовании стека для формирования вложенных структур. Когда происходит вызов begin() - новый уровень открывается и созданный элемент помещается стек, оказываясь в его вершине, то есть становится рабочим корневым элементом, к которому будут прикрепляться новые элементы. Как только происходит вызов end(), вершина стека выталкивается и мы откатываемся на предыдущий более высокий уровень иерархии.

Если не уверены, что до конца поняли, как все это работает, то советую взять пример, где мы формировали список пользовательских групп, и попытаться сопоставить сформированный XML-документ с тем, что происходит внутри нашего строителя. Я думаю, что тогда все встанет на свои места.

Вообще говоря, реализацию нашего Строителя нельзя считать полной, поскольку в ней не предусмотрены некоторые элементы, доступные в XML и предусмотренные в используемом модуле Qt. Например, сейчас мы не реализовали возможность добавления комментариев. Однако структура нашего Строителя такова, что поддержку новых элементов в него добавить довольно легко. Поэтому если у вас возникнет желание или необходимость, то вы без проблем сможете сделать это сами.

Повышаем планку

Код примера с пользовательскими группами выглядел здорово за счет использования цепочек, однако стоит признать, что он не слишком функционален. Вам вряд ли придется формировать XML-документы с заранее известным содержимым. Скорее всего, на практике вы захотите преобразовать уже имеющуюся у вас информацию в формат XML. Поэтому давайте реализуем более реалистичный пример. Вновь сформируем XML-документ с пользовательскими группами, но на этот раз возьмем за основу содержимое файла /etc/group. Если вы пользуетесь Windows, то просто можете найти пример этого файла в интернете и заменить путь на тот, куда его положите.

Поскольку мы займемся обработкой этого файла, то коротко напомню его структуру. Она предельно проста. Файл /etc/group состоит из набора строк. Каждая строка определена в следующем формате:

group_name:password:group_id:users

Нам из этого набора понадобится только первая и последняя часть, то есть group_name и users. Причем, пользователи перечисляются через запятую без лишних пробелов. Поэтому всю необходимую информацию мы легко сможем извлечь воспользовавшись функцией QString::split(). Вот что у меня получилось:

#include <QFile>
#include <QStringList>

int main() {
    QFile file( "/etc/group" );
    file.open( QIODevice::ReadOnly );

    XMLBuilder builder;
    builder.begin( "user_groups" );
    while( !file.atEnd() ) {
        const QString line = file.readLine();
        const QStringList lineSplit = line.split( ":" );
        if( lineSplit.size() < 4 ) {
            continue;
        }
        builder.begin( "group" ).attr( "name", lineSplit.first() );
        foreach( const QString& user, lineSplit.last().split( "," ) ) {
            const QString userTrimmed = user.trimmed();
            if( !userTrimmed.isEmpty() ) {
                builder.add( "user", userTrimmed );
            }
        }
        builder.end();
    }
    builder.end();
    std::cout << builder.getXML().toStdString() << std::endl;

    return 0;
}

Этот пример уже выглядит чуть посложнее. В первую очередь это объясняется необходимостью разбора содержимого файла /etc/group. Однако сам код построения XML получился ничуть не хуже, хоть нам и пришлось отказаться от использования цепочек в тех масштабах, как это было раньше.

Заключение

Вот мы и разобрались с еще одним интересным паттерном ООП, который называется Строитель. С его помощью вы можете разбить задачу компоновки сложных структур на ряд мелких этапов, которые значительно проще понять и реализовать. Подтвердить эту мысль мы смогли на примере задачи компоновки XML-документа. В результате у нас получился класс-Строитель, который вполне естественно и интуитивно позволяет формировать структуру XML-документа шаг за шагом.

Похожие публикации

Комментарии

у меня вопрос по следующим строкам кода в интерфейсе класса XMLBuilder:

XMLBuilder& begin( const QString& tagName ) throw( XMLException );

XMLBuilder& add( const QString& tagName, const QString& content ) throw( XMLException );

XMLBuilder& attr( const QString& attrName, const QString& content ) throw( XMLException );

XMLBuilder& end() throw( XMLException );

что означает throw (XMLException)? и еще в другой статье видел добавление "=0", также не понял

Klaster:

что означает throw (XMLException)?

Эта конструкция является аналогом нотации throws из языка программирования Java. Она указывает, что функция-член может выбросить исключение типа XMLException (или его наследников). Но, в отличие от Java, в C++ ее использование не является обязательным.

Klaster:

и еще в другой статье видел добавление "=0", также не понял

Под "= 0", вероятно, имеется в виду случай чисто виртуальной функции абстрактного класса. Это означает, что функция не определена и должна быть реализована в наследниках класса.

Спасибо

Пожалуйста =)