The Russian Electronic Developer Magazine | |
Русский электронный журнал разработчика | |
Всем известны самые примитивные средства описания в виде пар "параметр=значение", столь любимые программистами в среде различных *ix (автор, конечно же, не подымет руку на охаивание синтаксиса правил разбора в sendmail.cf - сами знаете за что), от которых не слишком далеко ушли структуры (синтаксис) файлов config.sys и autoexec.bat, возможное содержание которых достаточно хорошо всем знакомо. Ниже, в качестве примера, приведен кусок моего файла config.sys:
SET EPMPATH=C:\OS2\APPS;C:\opendoc\BIN; PROTECTONLY=NO SHELL=C:\OS2\MDOS\COMMAND.COM C:\OS2\MDOS FCBS=16,8 RMSIZE=640 DEVICE=C:\OS2\MDOS\VEMM.SYS DOS=LOW,NOUMB DEVICE=C:\OS2\MDOS\VXMS.SYS /UMB DEVICE=C:\OS2\MDOS\VDPMI.SYS DEVICE=C:\OS2\MDOS\VDPX.SYS DEVICE=C:\OS2\MDOS\VWIN.SYS DEVICE=C:\OS2\MDOS\VW32S.SYSКритиковать такой синтаксис несложно - он не позволяет синтаксически отделять описания разных объектов в одном файле. Зато его использование примитивно реализуется программно и во многих случаях оправдано. Но не во всех случаях возможно.
Незначительное расширение такой синтаксис получил в ini-файлах от Windows X.XX (например, 3.11) за счет введения секций. Именованные секции позволяют структурировать описания и формулировать запросы на элементы описания уже по двум параметрам "имя секции"-"имя параметра", что гораздо удобнее. Вот пример из файла SYSTEM.INI для WIN-OS2 сессии:
[keyboard] subtype= type=4 keyboard.dll=kbdru.dll oemansi.bin=xlat866.bin typeofswitch=2 secondkeyb.dll=kbdusx.dll [boot.description] keyboard.typ=Enhanced 101 or 102 key US and Non US keyboards mouse.drv=Microsoft, or IBM PS/2 network.drv=No Network Installed language.dll=Russian system.drv=MS-DOS System codepage=866 woafont.fon=Russian (866)К недостаткам этого метода можно отнести синтаксическую неопределенность при работе с переменным числом записей в рамках одной секции и отсутствие вложенности секций, что иногда полезно, т.к. не все информационные объекты линейны. Конечно, все это можно так или иначе реализовать программно, но эта реализация будет не естественным следствием идеи, заложенной в синтаксис, а лишь свидетельством изворотливости программиста.
Здесь следует отметить, что критика и далее будет бить по одной-двум позициям, намечающим путь к совершенству, не более того.
Следующим шагом в развитии идеи скриптов-описателей стали registry от Windows9X и далее, а также ini-файлы в OS/2. Привести их примеры не представляется возможным в силу их непечатности. Отметим следующее: они подразумевают древовидную структуру элементов (развитие идеи вложенности) и позволяют хранить объекты любого типа за счет введения бинарных объектов, трактуемых теми, кто их использует.
Для критики существующих реализаций подобного ресурса (в OS/2 он реализован в WINPRF.DLL) необходимо уточнить требования к синтаксису файлов конфигурации вообще.
Как отмечалось ранее, взгляды, здесь излагаемые, стали результатом работы над подсистемой сопровождения сценариев имитатора-тренажера для буровиков. Сценарий для подобного тренажера содержит в себе все параметры, конфигурирующие начальное состояние имитатора буровой установки и скважины, т.е. не многим отличается от файлов конфигурации. Он содержит внутри себя описание множества совершенно независимых объектов - описателей различного оборудования, описателей пород и их сочетания в геологический разрез, описания различных аномалий, раскиданных по стволу скважины (чтобы обучаемому было не скучно) и т.д. и т.п.
Подобные сценарии должны были создаваться самими пользователями продукта. Отсюда возникало требование интуитивной понятности для непрограммиста самих описаний и средств их создающих. Поскольку описания одного и того же оборудования могли встречаться во многих сценариях, они должны были описываться отдельно от самого сценария и входить в него в виде ссылки. Хорошо продуманный сценарий становится методическим материалом, поэтому должны быть предусмотренны средства его печати в удобном и красивом виде.
Эти требования повлекли за собой создание цепочки информационно связанных между собой программных средств:
Вышеописанные проблемы со сценарием и являются, по мнению авторов, вполне содержательной критикой использования Profile API для сопровождения сложных конфигураций.
Все это подвигло нас на формулировку следующих требований к концепции конфигурирования:
Предлагается синтаксис описания подобных классов, который оперирует ключевыми словами, литералами и комментариями.
Литералы используются такие же, как в C++ : \xx, \\, \n, \t, etc, а также специальные литералы %.
Комментарии обозначаются сочетанием символов //, /* */, правила их использования аналогичны С++. Версионность поддерживается вхождением (необязательным) версии в имя класса -<classname>[<version>]
Система оперирует следующими объектами:
классификаторы | transient | берется из класса как значение по умолчанию; может быть изменен в каждом представителе |
---|---|---|
базовые типы данных | int | целое (4 байта) |
double | вещественное (8 байт) | |
string | строка символов (не обязательно ограничивается \0) | |
group | массив произвольных объектов - позволяет доступ по номеру элемента | |
namedgroup | основан на базе group, используется длп описания диалога и печати | |
базовые типы данных для визуализации и печати | IntField | класс редактируемого и печатаемого int |
DoubleField | класс редактируемого и печатаемого double | |
StringField | класс редактируемого и печатаемого string | |
TextField | класс редактируемого и печатаемого текстового окна (textarea) | |
RadioToggleField | класс выбора в диалоге с помощью RadioButton | |
ComboTogleField | класс выбора из списка в диалоге с помощью ComboBox | |
ReferenceField | класс-ссылка - в диалоге редактируется с помощью drag&drop |
Далее приведены их описания в предлагаемом синтакисе :
IntField = { string title = "" // название, выводимое при печати и редактировании в диалоге int value = 0 // значение поля int min = 0 // контролируемое в диалоге минимальное значение целого value int max = 0 // контролируемое в диалоге максимальное значение целого value int delta = 0 // шаг автоизменения при редактировании в диалоге } DoubleField = { string title = "" // название, выводимое при печати и редактировании в диалоге double value = 0 // значение поля double min = 0 // контролируемое в диалоге минимальное значение вещественного value double max = 0 // контролируемое в диалоге максимальное значение вещественного value double coef = 0 // коэффициент перевода (для случая редактирования в одних единицах // измерения, а использования в других) double delta = 0 // шаг автоизменения при редактировании в диалоге int decimals = 0 // количество выводимых при печати и редактировании знаков // после десятичной точки } StringField = { int max = 0 // максимальное количество символов в строке string title = "" // название, выводимое при печати и редактировании в диалоге string value = "" // если строка ограничена одинарными кавычками '...' ограничитель строки // (\0 не ставится) } TextField = { // аналогичен StringField, но представляет собой textarea (MLE) int max = 0 string title = "" string value = "" } RadioToggleField = { // название говорит само за себя - реализация диалогового элемента string title = "" int value = 0 // содержит номер выбранной строки group elements = { } // набор строк типа string } ComboToggleField = { // аналогично предыдущему для объекта ComboBox string title = "" int value = 0 group elements = { } } ReferenceField = { // Объект-ссылка на другой объект. В диалоге реализует drag&drop string title = "" string class = "void" // имя класса, объекты которого могут заполнять данное поля // (их может быть много - class0|ckass1|...|classN) string value = "" // полное имя файла, содержащего объект int value2 = 0 // WPS-objectid }Класс ReferenceField позволяет использовать множество настроек в одной, если, например, программный продукт состоит из множества задач, каждая из которых имеет свою настройку с индивидуальной и общей частью, и наоборот, если настройки одного объекта используютсп во множестве других.
В качестве чисто практического (для описания изображения пород), реализован класс масски заливки:
ColorMaskField = { string title = "" int fcolor = 0 // цвет переднего плана int bcolor = 0 // цвет фона string value = '' // маска заливки 8*8 }Также можно заметить, что существуют и предустановленные данные:
DEngine drill={ string title="Забойный двигатель" DoubleField diam_zd ={ title="Диаметр заб.двигателя" coef=0.001 decimals=1 delta=0.1 min=30 max=400} DoubleField leng_zd ={ title="Длина заб.двигателя" coef=1 decimals=2 delta=0.01 max=50} DoubleField q_zd ={ title="Расход заб.двигателя" coef=1000 decimals=1 delta=0.1 min=30 max=400} DoubleField dens_zd ={ title="Плотность жидкости" coef=1000 decimals=2 delta=0.01 min=0.8 max=2.5} DoubleField loss_px_zd ={ title="Потери давления на холостом ходу" coef=98100 min=1 max=200} DoubleField freq_nx_zd ={ title="Частота вращения на холостом ходу" coef=0.01666 min=10 max=999} DoubleField loss_pt_zd ={ title="Потери давления при торможении" coef=98100 min=1 max=200} DoubleField moment_t_zd={ title="Тормозной момент" coef=10 min=1 max=1500} ComboToggleField drop ={ string title ="Тип двигателя" group elements = { string = "турбобур с постоянной линией" string = "турбобур с падающей линией" string = "объемный двигатель" } } }А так выглядит описание конкретного забойного двигателя (/* комментарий */ в первой строке необходим для интерпретации текста как скрипта):
/* XObject */ DEngine = { diam_zd = {value = 30.0000} leng_zd = {value = 0.0000} q_zd = {value = 30.0000} dens_zd = {value = 0.8000} loss_px_zd = {value = 1.0000} freq_nx_zd = {value = 10.0000} loss_pt_zd = {value = 1.0000} moment_t_zd = {value = 1.0000} drop = {value = 0} }Данный синтаксис показывает возможность изменения в каждом конкретном объекте и вспомогательных полей: min, max и прочих.
Как восклицала Алиса Кэррола - "Кому нужна книжка без картинок и диалогов!" Обратимся к заявленной самодиалоговости и документированности, т.е визуализации и печати.
Создан, как говорилось ранее, WPS-объект, интерпретирующий специально организованные части описания классов как страницы своих свойств (property). Для этого в описание класса вносятся специальные описания - group с именами pages (для диалога) и sections (для печати).
Вот как выглядит описывающая диалог часть класса:
group pages = { namedgroup = { // описание страницы диалога title = "%MajorTab" // символ % означает MajorTab elements = { string = "имя 1" // элемент класса string = "имя 2" ... string = "имя N" } } namedgroup = { // вторая страница диалога title = "MinorTab страницы" elements = { string = "имя 1" string = "имя 2" ... string = "имя N" } } ... // другие страницы диалога }Вот как выглядит (и что порождает) часть описания класса забойного двигателя (DEngine), относящаяся к диалогу:
DEngine drill={ ... group pages = { namedgroup = { string title = "%Забойный двигатель" group elements = { string string = "drop" string string = "diam_zd" string string = "leng_zd" string string = "q_zd" string string = "dens_zd" string string = "loss_px_zd" string string = "freq_nx_zd" string string = "loss_pt_zd" string string = "moment_t_zd" } } } }
Обратите внимание на вторую закладку - Тип. Она создается всегда и дает возможность явно задать класс объекта и выбрать способ сохранения (текстовый/двоичный). Последнее позволяет легко подправить руками любое поле объекта с помощью текстового редактора. И наоборот, любой объект может быть создан в текстовм редакторе, а затем сохранен в двоичном виде.
Для описания печати объекта используется аналогично организованная group sections; и вот, что получается при печати:
DEngine drill={ ... group sections={ namedgroup={ title = "Забойный двигатель" elements={ string ="drop" string ="diam_zd" string ="leng_zd" string ="q_zd" string ="dens_zd" string ="loss_px_zd" string ="freq_nx_zd" string ="loss_pt_zd" string ="moment_t_zd" } } } }
Печать объекта производится либо из его меню, либо простым натаскиванием его иконы на икону (фолдер) принтера. Выше приведен пример печати одного объекта. Тот же объект, как ReferenceField внутри сценария, при печати имеет следующий вид:
/* AMT */ group Config = { string customer = "ЗАО АМТ" int loglevel = 0 // -1 полный отладочный вывод в файл \xobject.log // 0 выводить только сообщения обошибках // 1 выводить предупреждения исообщения об ошибках group classes = { string = "classes.ini" // описание общих классов (обпзательно) string = "common.ini" // описание другой группы общих классов string = "DSTclass.ini" // описание классов бурового тренажера string = "KRSclass.ini" // описание классов тренажера для капитального ремонта скважин } }Этот файл используется WPS-объектом AMTWPSXObject, реализующем диалоги и печать объектов конфигурации. Этот объект ассоциирован с расширением AMT. При программировании следует самому прочитать описания используемых классов - например, весь config.ini и все перечисленные в нем файлы или только нужные задаче.
Следующий текст дает пример считывания всех библиотек классов, описанных в config.ini:
#include "common.h" XObject *o_main, *o_sub, *o_ref, *o_target, *o_config; // для сценария // функция инициализирует подсистему предустановленных классов XObject // и возвращает кол-во классов в библиотеках int InitXClasses(void) { char buf[256]="", *str; char *amtPath = getenv(AMT_PATH); int num=0; XOBJECT_INITIALIZE // загрузка библиотек классов buf[0] = 0; if (amtPath) strcat(buf, amtPath); strcat(buf, "\\"); strcat(buf, AMT_CONFIG); o_config = XObject::loadObject(buf); if (!o_config) return 1; o_config = o_config->getGroupElement("classes"); for (int i = 0; iСледующие строки дают представление о получении значений некоторых элементов класса сценария для тренажера:getGroupSize(); i++ ) { str = o_config->getGroupElement(i)->getStringValue(); if (str) { buf[0] = 0; if (amtPath) strcat(buf, amtPath); strcat(buf, "\\"); strcat(buf, str); num += XObject::loadClasses(buf); } } return num; }
{ .... o_main=XObject::loadObject(file_name); // загрузка файла конкретного сценария if (!o_main) return 1; // ошибка чтения сценария - переход к тестовому режиму ... o_sub = o_main->getGroupElement("title"); st->Scen->Name=strdup(o_sub->getStringValue()); o_sub = o_main->getGroupElement("model_type"); st->Scen->model_type=o_sub->getIntValue(); // тип (номер) модели данного тренажера o_sub = o_main->getGroupElement2("mode|value"); // здесь используется составное имя st->Scen->mode=o_sub->getIntValue(); // тип (номер) модели данного тренажера ... // если элемент является ссылкой, можно воспользоваться методом GetValue(name), // который сам и загрузит скрипт ссылки o_ref=o_main->getValue("derrik") // загрузить описание буровой вышки // вычислить значение параметра g_kv в системе СИ st->Scen->g_kv=o_ref->getGroupElement2("g_kv|value")->getDoubleValue() * o_ref->getGroupElement2("g_kv|coef")->getDoubleValue(); }Как видно из примеров, доступ к элементам конфигурации (параметрам) может производится по именам полей класса (name), по составным именам ("name|subname"), а также по их порядковым номерам в group. В приведенном выше описании забойного двигателя величина параметра <diam_zd|value> может браться по имени <%1|value>, т.к. diam_zd имеет порядковый номер один в классе.
Это был пример чтения конфигурации. Исходя из удобства их сопровождения - визуализации и печати - имеет смысл (в некоторых случаях) и сохранять в них программно полученную информацию. Так, например, в буровом тренажере журнал занятий реализован своим набором классов и в него записываются результаты всех занятий. Вот как это реализуется:
XObject *les, *student, *acc; // для студента int InitStudentRecords(DST_STATE *st, int alarmtime, int alarmactive) { int res; student=XObject::loadObject(st->StudentName); // загружаем файл объекта класса студент if (!student) return 1; les=XObject::getInstance("Lesson", CLASS_NAMEDGROUP_NAME); // создаем объект класса занятие acc=XObject::getInstance("Accid", CLASS_NAMEDGROUP_NAME); // создаем объект класса авария // заполняем описание занятия les->getGroupElement("sim")->setStringValue(st->SIM); les->getGroupElement("mode")->setIntValue(st->Scen->mode); les->getGroupElement("date")->setIntValue((int)(st->BeginTime)); les->getGroupElement("scen")->setStringValue(st->Scen->Name); les->getGroupElement("ft")->setIntValue(alarmtime); les->getGroupElement("hl")->setIntValue(st->TimeFromBeginReal); if (alarmactive) res=2; else res=1; les->getGroupElement("res")->setIntValue(res); return 0; } int AddAccidRecord(int num, int begintime, int howlong, int source) { acc->getGroupElement("num")->setIntValue(num); acc->getGroupElement("bt")->setIntValue(begintime); acc->getGroupElement("hl")->setIntValue(howlong); acc->getGroupElement("src")->setIntValue(source); les->getGroupElement("accids")->addGroupElement(acc->clone()); // клонирование класса return 0; } int MakeStudentRecord(DST_STATE *st) { student->getGroupElement("lessons")->addGroupElement(les); XObject::saveObject(st->StudentName,student); delete acc; delete student; return 0; } void SaveAlarm(void) { AMessage *AMt; if (!STATUS->StudentName) return; if (InitStudentRecords(STATUS, AlarmTime, AlarmActive)) return; AMt=AccBeginList; while (AMt) { AddAccidRecord(AMt->Anum, AMt->beginTime, AMt->howLong, AMt->source); AMt=AMt->next; } MakeStudentRecord(STATUS); }Для заинтересовавшихся приведем описание public объектов класса XObject:
class _Export XObject { public: static void setLogLevel(int); static int log(char*, ...); static int log(int, char*, ...); static int logObject(XObject*, char* = NULL); static void logClasses(); static void initClass(); static void unInitClass(); static int loadFile(char*, char**, int*); static int saveFile(char*, char*, int); static XObject* getInstance(char*, char*, int = FLAGS_NORMAL, int =-1); static XObject* getInstanceText(char*, int*, int =-1); static XObject* getInstanceBin(char*, int*, int = -1); static int loadClasses(char*); static XObject* loadObject(char*); static int saveObject(char*, XObject*); ... public: static int getClassNum(); static char* getClassName(int); ... public: virtual ~XObject(); int isVoid(); int isInteger(); int isDouble(); int isString(); int isComplex(); int isGroup(); int isObject(); int isObject(char*); int isBasic(); virtual XObject* clone(); int initFrom(XObject*); char* getName(); int setName(char* n); char* getClassName(); int getClassVersion(); int getFlags(); int setFlags(int); void flagSet(int); void flagClear(int); int flagIsSet(int); int getSizeBin(); int getSizeText(); int getIntValue(); int setIntValue(int); double getDoubleValue(); int setDoubleValue(double); char* getStringValue(); int getStringSize(); int getStringLength(); int setStringSize(int); int setStringValue(int, char*); int setStringValue(char*); int getGroupSize(); XObject* getGroupElement(int); XObject* getGroupElement(char*); XObject* getGroupElement2(char*); int addGroupElement(XObject*); int removeGroupElement(int); int removeGroup(); int loadBinFile(char*); int readText(char*, int); int readBin(char*, int); int writeBin(char*, int); int writeText(char*, int, int = 0); virtual XObject* getValue(); virtual int show(int); virtual int print(int); // misc static char* strPrintInteger(char*, int, int); static char* strPrintDouble(char*, double, int, int); static char* strPrintString(char*, char*, int, int*); static char* strPrintStringEsc(char*, char*, int, int*); ... };Если вам понравилась описанная реализация идеи, вы можете скачать комплект для разработчика: xobject.zip, 324k.
NB! скоро будет готова новая версия комплекта -- следите за новостями.
Если вы хотите поспорить или просто поговорить с автором, пишите сюда.
Gregory Shrago - идея и реализация
Joseph Shrago - автор текста и потребитель :)
Интересные ссылки:
Комментариев к странице: 0 | Добавить комментарий
Редактор: Дмитрий Бан
Оформление: Евгений Кулешов