Итак, в своей первой статья я сделал несколько предположений об архитектуре предметной области в CMF/CMS системах. Тогда я сделал предположение об объектной модели я связанном с нею сервисе, который умел обрабатывать входящие запросы и выдавать нужный результат на основании внутренней структуры модели.
Сегодня, сейчас, я бы хотел коснуться вопроса непосредственного использования такой системы в существующем фреймворке Mozart, не так давно впущенном компанией ADV под лицензией GPLv3.
Основой API Mozart'а служат newt-контейнеры. Newt — это XML-контейнер (в XML-фрагменте), который специальным образом обрабатывается XML-парсером. Newt может быть как одиночным тэгом (закрытым тэгом <newt />), так и контейнером (содержит в себе другие контейнеры (newt'ы, инструкции)). Аналог newt'а — это функция, задачей которой является произвести какие-то действия (и вернуть результат, если требуется).
Парсер XML генерирует события по мере обработки данных XML, на эти события вешаются программные обработчики. Для нас важно 2 события: onStartTag и onEndTag. Все newt'ы обрабатываются на onEndTag. Это дает нам то, что сначала отрабатывают рекурсивно все вложенные newt'ы (вложенные инструкции), а потом уже их родители. Т.е. каждый вложенный newt генерирует какое-то содержимое для своего родительского newt'а, а родительский newt этот XML фрагмент может использовать по назначению (просто как контент или как свои инструкции (поскольку его тело уже полностью определено и закончено к моменту обработки)).
Newt-контейнер может содержать помимо других вложенных newt-контейнеров контейнеры-инструкции (специально оформленное XML-описание, указывающее newt'у, что делать). Инструкции обрамляются в специальный тэг (например, request или request-data), чтобы отличить их от простого XML-фрагмента, и могут быть произвольными согласно заложенному в программу функционалу: сохранение в БД, чтение из БД, выставление параметров и т.п. Для каждого контейнера так же определен контекст, которым можно управлять. Выполняется текущая инструкция в текущем контексте (который она получила). Выставляется контекст для нижестоящих инструкций.
Теперь применим эти знания на конкретном примере. В качестве такого возьмем выборки из БД, как наиболее основной и частый компонент любого проекта. За эту функцию в системе отвечает контейнер <newt:base>.
Особенностью работы с данными в Mozart можно считать то, что оперировать с ними в системе надо как с объектами. Представьте себе все таблицы Базы Данных, их связи как набор объектов терминологического поля, связанных между собой. Этим объектам можно дать «человеческие» названия и построить граф, как на рисунке ниже.
Жизненная ситуация по созданию системы для оператора мобильной связи может быть изображена как набор объектов реальной жизни, связанных между собой. На графе описывается ситуация с сотовой связью: оператор — регион — тариф — симкарта(номер) — контракт — телефон — брэнд — человек — где прописан.
На основании такой структуры мы можем строить произвольные запросы для получения данных. Возьмем для примера несколько ситуаций, в которых используются различные условия:
Каким брэндом пользуются люди, чей возраст от 20 до 30 лет:
Для ограничения возраста Человека мы использовали операторы min и max. Выставив их объекту user мы вложили внутрь выборку объекта brand. Вложенность связывает условие и выборку, Mozart строит связь между этими двумя объектами через связывающий их объект Телефон (посмотрите, как они связаны на графе выше).
Список людей, пользующихся услугами конкретного оператора:
Здесь мы использовали оператор равенства для выставления параметру name объекта operator конкретного значения. Вложенный объект user будет выбран с учетом условия по оператору. Если посмотреть на граф, то можно увидеть, что Оператор и Человек связаны двумя разными маршрутами. Mozart при построении связей выбирает кратчайший. Подробнее об этом можно почитать в статье "Архитектура предметной области в CMF/CMS системах".
Список людей, чья фамилия содержит заданную последовательность символов:
context-attr, используемый нами в этом примере, указывает подстроку, которая должна содержаться в параметре first_name объекта Человек. Это своего рода контекстный поиск.
Выше мы упомянули, как Mozart пытается построить связь между несмежными объектами. Углубимся в этот момент немного подробнее. Например, во втором примере, когда мы выбирали Человека по Оператору, мы предположили, что Mozart может строить связь по двум маршрутам (через Прописку или через Регион, как это видно на графе). Если, нам, например, требуется изменить алгоритм Mozart и указать ему, что короткий маршрут нас в конкретной ситуации не устраивает, мы можем использовать оператор path-exclude для инструкции get, указав список объектов, которые мы точно не хотим включать в связывающие (по аналогии есть оператор path-include). Ниже исключим объект Телефон:
Возможно, у кого-то может возникнуть вопрос об эффективности работы такой системы, которая пытается строить связи сразу по нескольким маршрутам, тратя на это процессорное время и память, а значит тормозя приложение. В этом смысле все решается очень просто: при проектировании схемы объектов нам следует избегать закольцованности. В нашем случае, вполне вероятно, объект Прописка следует пометить в конфигурации базы, как нетранзитный, т.е. такой объект, который является всегда конечным при построении связей.
Поговорим теперь о вложенности одних инструкций get и set в другие. Рассмотрим 2 примера:
и
Оба примера оперируют с 3-мя объектами: brand, phone и user, которые связаны.
Первый пример нам выведет всех пользователей, по каждому из них будет выведен список моделей телефона, которые этот человек использует. По каждому телефону будет выведен брэнд телефона. Получится структура:
Второй пример покажет нам список всех брэндов, по каждому выведет список всех телефонов этого брэнда. А уже внутри каждого телефоны мы получим список людей, который пользуются данной моделью телефона.
Если, например, вы захотите по каким-то причинам в первом примере для телефона выводить не его брэнд, а вообще весь список брэндов, для этого можно использовать инструкцию unset.
Т.е. мы сняли с объекта brand все выше выставленные на него условия, тем самым это просто выборка из объекта всех данных.
Если же мы хотим вывести список всех пользователей, всех моделей и марок телефонов, то просто избавимся от вложенности и напишем инструкции get последовательно:
Выше мы говорили о выборке объекта и инструкциях get. А что же происходит в ситуации, когда необходимо не выбрать последовательно несколько объектов, а выбрать всего один объект, при этом выставив ограничения по нескольким другим? Вернемся к нашему второму примеру (где выбирался Брэнд, его Телефон и использующий его Человек). Переделаем его в такой:
Мы заменили get объектов Брэнд и Телефон на set, т.е. на выставление ограничения по ним. Структура вложенности осталась прежняя. Тем самым мы указали накопить ограничения для самого последнего по вложенности get, который выбирает объект Человек с учетом выше установленных инструкций set, в которые он вложен.
В результате мы получим список пользователей, которые пользуются указанными телефонами:
Как мы увидели, API системы Mozart имеет очевидную структуру и обладает простотой, которая позволяет оперировать с выборкой данных и их классификацией неискушенным разработчикам. Результат получения данных представляет собой структурированный формат XML, структура которого полностью зависит от ваших запросов. Вы можете добавить в нее некоторую избыточность, можете нет. У вас есть возможность получать такие деревья XML, какие будут удобны не только для последующей обработки в XSLT, но и для обычного визуального восприятия, ведь именно простота разработки проекта является основополагающим при выборе системы разработки.
Терминологическое поле объектов помогает проще понять структуру проекта, связи между его данными. Избавивших от технических терминов, SQL запросов и прочих низкоуровневых тонкостей вы получаете среду, которая очень схожа с окружающим миром, оперируете в ней как в реальном мире.
Для затравки на следующую статью могу привести другие элементы API. Например, <newt:form> отвечает за обработку форм: вывод, получение и обработку данных. <newt:transform> — преобразует одни структуры данных в другие, с его помощью можно создавать инструкции для выше стоящих ньютов, а так же преобразовывать один xml в другой. <newt:action> предназначен для выполнения контроллеров, определенных в веб-приложении. По сути, это один из способов вызывать написанные разработчиком скрипты на каком-либо скриптовом языке. <newt:http-env> — удобный инструмент для работы с переменными окружения, будь то куки, сессия или пользовательский набор данных, называемых query.
Веб-сайт системы Mozart: mozartframework.ru
Сегодня, сейчас, я бы хотел коснуться вопроса непосредственного использования такой системы в существующем фреймворке Mozart, не так давно впущенном компанией ADV под лицензией GPLv3.
Основой API Mozart'а служат newt-контейнеры. Newt — это XML-контейнер (в XML-фрагменте), который специальным образом обрабатывается XML-парсером. Newt может быть как одиночным тэгом (закрытым тэгом <newt />), так и контейнером (содержит в себе другие контейнеры (newt'ы, инструкции)). Аналог newt'а — это функция, задачей которой является произвести какие-то действия (и вернуть результат, если требуется).
Парсер XML генерирует события по мере обработки данных XML, на эти события вешаются программные обработчики. Для нас важно 2 события: onStartTag и onEndTag. Все newt'ы обрабатываются на onEndTag. Это дает нам то, что сначала отрабатывают рекурсивно все вложенные newt'ы (вложенные инструкции), а потом уже их родители. Т.е. каждый вложенный newt генерирует какое-то содержимое для своего родительского newt'а, а родительский newt этот XML фрагмент может использовать по назначению (просто как контент или как свои инструкции (поскольку его тело уже полностью определено и закончено к моменту обработки)).
Newt-контейнер может содержать помимо других вложенных newt-контейнеров контейнеры-инструкции (специально оформленное XML-описание, указывающее newt'у, что делать). Инструкции обрамляются в специальный тэг (например, request или request-data), чтобы отличить их от простого XML-фрагмента, и могут быть произвольными согласно заложенному в программу функционалу: сохранение в БД, чтение из БД, выставление параметров и т.п. Для каждого контейнера так же определен контекст, которым можно управлять. Выполняется текущая инструкция в текущем контексте (который она получила). Выставляется контекст для нижестоящих инструкций.
Теперь применим эти знания на конкретном примере. В качестве такого возьмем выборки из БД, как наиболее основной и частый компонент любого проекта. За эту функцию в системе отвечает контейнер <newt:base>.
Особенностью работы с данными в Mozart можно считать то, что оперировать с ними в системе надо как с объектами. Представьте себе все таблицы Базы Данных, их связи как набор объектов терминологического поля, связанных между собой. Этим объектам можно дать «человеческие» названия и построить граф, как на рисунке ниже.
Жизненная ситуация по созданию системы для оператора мобильной связи может быть изображена как набор объектов реальной жизни, связанных между собой. На графе описывается ситуация с сотовой связью: оператор — регион — тариф — симкарта(номер) — контракт — телефон — брэнд — человек — где прописан.
На основании такой структуры мы можем строить произвольные запросы для получения данных. Возьмем для примера несколько ситуаций, в которых используются различные условия:
- каким брэндом пользуются люди, чей возраст от 20 до 30 лет;
- список людей, пользующихся услугами конкретного оператора;
- список людей, чья фамилия содержит заданную последовательность символов.
Каким брэндом пользуются люди, чей возраст от 20 до 30 лет:
Copy Source | Copy HTML<br/><newt:base><br/> <request><br/> <set object="user" attr="age" min="20" max="30"><br/> <get object="brand" /><br/> </set><br/> </request><br/></newt:base> <br/>
Для ограничения возраста Человека мы использовали операторы min и max. Выставив их объекту user мы вложили внутрь выборку объекта brand. Вложенность связывает условие и выборку, Mozart строит связь между этими двумя объектами через связывающий их объект Телефон (посмотрите, как они связаны на графе выше).
Список людей, пользующихся услугами конкретного оператора:
Copy Source | Copy HTML<br/><newt:base><br/> <request><br/> <set object="operator" attr="name" value="Example"><br/> <get object="user" /><br/> </set><br/> </request><br/></newt:base> <br/>
Здесь мы использовали оператор равенства для выставления параметру name объекта operator конкретного значения. Вложенный объект user будет выбран с учетом условия по оператору. Если посмотреть на граф, то можно увидеть, что Оператор и Человек связаны двумя разными маршрутами. Mozart при построении связей выбирает кратчайший. Подробнее об этом можно почитать в статье "Архитектура предметной области в CMF/CMS системах".
Список людей, чья фамилия содержит заданную последовательность символов:
Copy Source | Copy HTML<br/><newt:base><br/> <request><br/> <set object="user" attr="first_name" context-attr="Ива"><br/> <get object="user" /><br/> </set><br/> </request><br/></newt:base> <br/>
context-attr, используемый нами в этом примере, указывает подстроку, которая должна содержаться в параметре first_name объекта Человек. Это своего рода контекстный поиск.
Выше мы упомянули, как Mozart пытается построить связь между несмежными объектами. Углубимся в этот момент немного подробнее. Например, во втором примере, когда мы выбирали Человека по Оператору, мы предположили, что Mozart может строить связь по двум маршрутам (через Прописку или через Регион, как это видно на графе). Если, нам, например, требуется изменить алгоритм Mozart и указать ему, что короткий маршрут нас в конкретной ситуации не устраивает, мы можем использовать оператор path-exclude для инструкции get, указав список объектов, которые мы точно не хотим включать в связывающие (по аналогии есть оператор path-include). Ниже исключим объект Телефон:
Copy Source | Copy HTML<br/><newt:base><br/> <request><br/> <set object="operator" attr="name" value="Example"><br/> <get object="user" path-exclude="phone"/><br/> </set><br/> </request><br/></newt:base> <br/>
Возможно, у кого-то может возникнуть вопрос об эффективности работы такой системы, которая пытается строить связи сразу по нескольким маршрутам, тратя на это процессорное время и память, а значит тормозя приложение. В этом смысле все решается очень просто: при проектировании схемы объектов нам следует избегать закольцованности. В нашем случае, вполне вероятно, объект Прописка следует пометить в конфигурации базы, как нетранзитный, т.е. такой объект, который является всегда конечным при построении связей.
Поговорим теперь о вложенности одних инструкций get и set в другие. Рассмотрим 2 примера:
Copy Source | Copy HTML<br/><newt:base><br/> <request><br/> <get object="user" ><br/> <get object="phone"><br/> <get object="brand"/><br/> </get><br/> </get><br/> </request><br/></newt:base> <br/>
и
Copy Source | Copy HTML<br/><newt:base><br/> <request><br/> <get object="brand" ><br/> <get object="phone"><br/> <get object="user"/><br/> </get><br/> </get><br/> </request><br/></newt:base> <br/>
Оба примера оперируют с 3-мя объектами: brand, phone и user, которые связаны.
Первый пример нам выведет всех пользователей, по каждому из них будет выведен список моделей телефона, которые этот человек использует. По каждому телефону будет выведен брэнд телефона. Получится структура:
Copy Source | Copy HTML<br/><user name="1"><br/> <phone name="example123"><br/> <brand name="Super Brand" /><br/> </phone><br/> <phone name="345example"><br/> <brand name="Super Brand" /><br/> </phone><br/> <phone name="678example678"><br/> <brand name="Brand N 1" /><br/> </phone><br/></user><br/><user name="2"><br/> <phone name="example123"><br/> <brand name="Super Brand" /><br/> </phone><br/></user><br/><user name="3"><br/> <phone name="345example"><br/> <brand name="Super Brand" /><br/> </phone><br/></user> <br/>
Второй пример покажет нам список всех брэндов, по каждому выведет список всех телефонов этого брэнда. А уже внутри каждого телефоны мы получим список людей, который пользуются данной моделью телефона.
Copy Source | Copy HTML<br/><brand name="Super Brand"><br/> <phone name="example123"><br/> <user name="1"/><br/> <user name="2"/><br/> </phone><br/> <phone name="345example"><br/> <user name="1"/><br/> <user name="3"/><br/> </phone><br/></brand><br/><brand name="Brand N 1"><br/> <phone name="678example67"><br/> <user name="1"/><br/> </phone><br/></brand> <br/>
Если, например, вы захотите по каким-то причинам в первом примере для телефона выводить не его брэнд, а вообще весь список брэндов, для этого можно использовать инструкцию unset.
Copy Source | Copy HTML<br/><newt:base><br/> <request><br/> <get object="user" ><br/> <get object="phone"><br/> <unset object="brand"><br/> <get object="brand"/><br/> </unset><br/> </get><br/> </get><br/> </request><br/></newt:base> <br/>
Т.е. мы сняли с объекта brand все выше выставленные на него условия, тем самым это просто выборка из объекта всех данных.
Если же мы хотим вывести список всех пользователей, всех моделей и марок телефонов, то просто избавимся от вложенности и напишем инструкции get последовательно:
Copy Source | Copy HTML<br/><newt:base><br/> <request><br/> <get object="brand" /><br/> <get object="phone" /><br/> <get object="user" /><br/> </request><br/></newt:base> <br/>
Выше мы говорили о выборке объекта и инструкциях get. А что же происходит в ситуации, когда необходимо не выбрать последовательно несколько объектов, а выбрать всего один объект, при этом выставив ограничения по нескольким другим? Вернемся к нашему второму примеру (где выбирался Брэнд, его Телефон и использующий его Человек). Переделаем его в такой:
Copy Source | Copy HTML<br/><newt:base><br/> <request><br/> <set object="brand" attr="name" value="Brand N 1"><br/> <set object="phone" attr="name" value="678example67"><br/> <get object="user"/><br/> </set><br/> </set><br/> </request><br/></newt:base> <br/>
Мы заменили get объектов Брэнд и Телефон на set, т.е. на выставление ограничения по ним. Структура вложенности осталась прежняя. Тем самым мы указали накопить ограничения для самого последнего по вложенности get, который выбирает объект Человек с учетом выше установленных инструкций set, в которые он вложен.
В результате мы получим список пользователей, которые пользуются указанными телефонами:
Copy Source | Copy HTML<br/><user name="1"/><br/><user name="10"/><br/><user name="11"/> <br/>
Выводы
Как мы увидели, API системы Mozart имеет очевидную структуру и обладает простотой, которая позволяет оперировать с выборкой данных и их классификацией неискушенным разработчикам. Результат получения данных представляет собой структурированный формат XML, структура которого полностью зависит от ваших запросов. Вы можете добавить в нее некоторую избыточность, можете нет. У вас есть возможность получать такие деревья XML, какие будут удобны не только для последующей обработки в XSLT, но и для обычного визуального восприятия, ведь именно простота разработки проекта является основополагающим при выборе системы разработки.
Терминологическое поле объектов помогает проще понять структуру проекта, связи между его данными. Избавивших от технических терминов, SQL запросов и прочих низкоуровневых тонкостей вы получаете среду, которая очень схожа с окружающим миром, оперируете в ней как в реальном мире.
Для затравки на следующую статью могу привести другие элементы API. Например, <newt:form> отвечает за обработку форм: вывод, получение и обработку данных. <newt:transform> — преобразует одни структуры данных в другие, с его помощью можно создавать инструкции для выше стоящих ньютов, а так же преобразовывать один xml в другой. <newt:action> предназначен для выполнения контроллеров, определенных в веб-приложении. По сути, это один из способов вызывать написанные разработчиком скрипты на каком-либо скриптовом языке. <newt:http-env> — удобный инструмент для работы с переменными окружения, будь то куки, сессия или пользовательский набор данных, называемых query.
Веб-сайт системы Mozart: mozartframework.ru