Система тестирования. Часть 2. Работа с данными
В первой части цикла статей, посвященного написанию системы тестирования, были описаны основные требования к программе и ее общая структура. Вопросы, как и основные настройки теста, было решено хранить в зашифрованном xml файле. В этой части цикла статей мы подробно рассмотрим механизм работы с этим файлом.
Структура XML файла
Рассмотрим структуру XML файла, в незашифрованном виде, на примере:
- <?xml version="1.0" encoding="UTF-8"?>
- <test text="Delphi" qcount="0">
- <question text="Delphi - объектно ориентированный язык?" active="true">
- <answer right="False" text="нет"/>
- <answer right="true" text="да"/>
- </question>
- <question text="Какая функция или процедура выполнит
- преобразование string в integer?" active="true">
- <answer right="true" text="StrToInt"/>
- <answer right="true" text="Val"/>
- <answer right="false" text="StringToInteger"/>
- </question>
- <question text="Есть ли в Delphi динамические массивы?" active="false">
- <answer right="false" text="Да, были в Delphi с самой первой версии"/>
- <answer right="false" text="Нет"/>
- <answer right="true" text="Да, появились с версии 4"/>
- </question>
- </test>
Как видно, test
- Text – название теста
- Qcount– количество используемых вопросов. Если 0, тогда используются все вопросы из файла.
Вопросом является ветвь question, которая является родительской для ветви answer – варианта ответа. Рассмотрим атрибуты вопроса:
- Text – сам текст вопроса.
- Active– флаг, определяющий является ли вопрос активным (будет ли использован в тесте)
Вариантов ответа может любое количество. Притом опять же любое количество из них может быть верным. Атрибуты:
- Right – флаг, определяющий является ли вариант ответа верным или нет.
- Text – текст варианта ответа.
Данные
Весь код, который мы будем использовать для работы с xml файлом и данными из него целесообразно определить в один unit. Назовем его StData и положим в папку lib, так как с данными должны работать и представление и администрирование.
Перед тем как начать писать код, разбирающий XML нужно понять, как мы будем представлять данные и, собственно, с какими данными будем работать. Исходя из структуры XML файла, можно выделить следующее: сам тест (название и количество вопросов), вопрос (текст и активность) и вариант ответа (правильность и текст). Все это можно представить в виде классов. То есть получить классы TStTest, TStQuestion и TStAnser для теста, вопроса, и варианта ответа соответственно.
Удобно будет, если вопрос будет хранить список вариантов ответа, а вариант ответа, знать какому вопросу он принадлежит.
Учитывая вышесказанное, получаем следующие классы:
- unit StData;
- interface
- uses
- SysUtils, Windows, Messages, Classes, Graphics, Controls, Forms, Dialogs,
- xmldom, XMLIntf, msxmldom, XMLDoc, StdCtrls, Contnrs, CodeUnit;
- type
- // Вопрос
- TStQuestion = class
- private
- FActive: boolean;
- FText: string;
- public
- // Список вариантов ответа
- StAnswerList: TObjectList;
- constructor Create;
- destructor Destroy;
- // Активен ли вопрос
- property Active: boolean read FActive write FActive;
- // Текст вопроса
- property Text: string read FText write FText;
- end;
- // Вариант ответа
- TStAnswer = class
- private
- FRight: Boolean;
- FStQuestion: TStQuestion;
- FText: string;
- public
- // Верный ли варинат ответа
- property Right: Boolean read FRight write FRight;
- // Сам вопрос
- property StQuestion: TStQuestion read FStQuestion write FStQuestion;
- // Текст варианта ответа
- property Text: string read FText write FText;
- end;
- // Тест
- TStTest = class(TObject)
- private
- FQCount: Integer;
- FText: string;
- public
- // Название (описание) теста
- property Text: string read FText write FText;
- // Максимальное количество вопросов
- property QCount: Integer read FQCount write FQCount;
- end;
- implementation
- constructor TStQuestion.Create;
- begin
- StAnswerList := TObjectList.Create;
- end;
- constructor TStQuestion.Destroy;
- begin
- StAnswerList.Free;
- inherited Destroy;
- end;
- end.
Следует отметить, что для хранения списка вариантов ответа используется контейнер TObjectList. Это не лучший вариант, так как в нем хранятся объекты типа TObject, и для использования придется постоянно приводить к типу TStAnser. В последних версиях Delphi появились generetic (шаблоны), и лучше использовать их, если точно известно, что программа не будет компилироваться на ранних версиях.
Но теперь, когда разобрались с представлением данных, можно заняться XMLфайлом. Нам нужно разобрать этот файл, и полученную из него информацию раскидать по созданным выше классам. Так же нужно учесть и обратное, когда данные из программы нужно будет занести в XML.
Создадим для этого отдельный класс, назовем его TStData. Для парсинга XML будем использовать встроенный в Delphi компонент TXMLDocument.
Разберемся, что же нужно от класса TStData:
- Получение списка вопросов и настроек теста из XML.
- Сохранение вопросов и настроек теста в XML.
Кроме этого, еще стоит вынести возможность управления вопросами (добавление и удаление) в этот класс. Список вопросов и настройки теста стоит сделать доступными только для чтения. Так же необходимо некоторое свойство доступное как для записи, так и для чтения, в котором будет храниться имя XML файла с данными.
Получение данных стоит предусмотреть не только из файла, но и из строки. Кроме того, если тест только создается, и нет еще никакого файла, то нужно что бы программа смогла создать базовую структуру пустого XML.
И, в итоге, получим следующий класс:
- TStData = class(TObject)
- private
- FStQuestionList: TObjectList;
- FStTest: TStTest;
- // Парсер XML
- XMLDocument: IXMLDocument;
- FFileName: string;
- // Процедура заргузки данных из XML
- procedure LoadData;
- public
- procedure Load(setFileName: string);
- // Загрузка данных из строки
- procedure LoadFromXml(XMLStr: string);
- // Получение данных, если XML файла нет
- procedure LoadEmpty;
- // Сохранение в XML
- procedure Save;
- procedure AppendQuestion( newQuestion: TStQuestion );
- procedure DeleteQuestion( delQuestion: TStQuestion );
- constructor Create;
- destructor Destroy;
- // Имя XML файла
- property FileName: string read FFileName write FFileName;
- // Список вопросов
- property StQuestionList: TObjectList read FStQuestionList;
- // Настройки теста
- property StTest: TStTest read FStTest;
- end;
Рассмотрим его методы.
Конструктор класса
В конструкторе идет создание объектов используемых в классе. Таких как, объекта для парсинга XML– TXMLDocument, списка вопросов и настроек теста. Кроме того предполагается, что никакого файла нет, и загружается пустая структура xml (LoadEmpty).
- constructor TStData.Create;
- begin
- XMLDocument := TXMLDocument.Create( nil );
- FStQuestionList := TObjectList.Create;
- FStTest := TStTest.Create;
- LoadEmpty;
- end;
Метод Load
Принимает имя xml файла, он загружается в объект XMLDocument, и вызывается процедура загрузки данных – LoadData.
- procedure TStData.Load(setFileName: string);
- begin
- FFileName := setFileName;
- // Загружаем xml документ из файла
- XMLDocument.LoadFromFile( FFileName );
- LoadData;
- end;
Метод LoadEmpty
Как правило, этот метод вызывается, тогда, когда никакого файла, а соответственно, данных, еще нет. В строковую константу записана валидная структура пустого XML. Потом она загружается в XMLDocumentкак строка и вызывается процедура LoadData.
- procedure TStData.LoadEmpty;
- const
- DEFAULT_HEADER = '<?xml version="1.0" encoding="UTF-8"?>' +
- '<test text="" qcount="0"></test>';
- begin
- XMLDocument.LoadFromXML( DEFAULT_HEADER );
- LoadData;
- FFileName := '';
- end;
Метод LoadData
Этот метод как раз выполняет чтение данных из XML и заносит их в объекты StQuestionList (список вопросов) и StTest (настройки теста). Метод достаточно объемный, но благодаря парсеру XMLDocument, чтение XML достаточно понятно и удобно.
- procedure TStData.LoadData;
- var
- i, j: integer;
- tmpQuestion: TStQuestion;
- tmpAnswer: TStAnswer;
- Root: IXMLNode;
- Node: IXMLNode;
- begin
- // Очистим список вопросов на тот случай если загрузка данных
- // вызывается еще раз
- FStQuestionList.Clear;
- // Корневая ветвь документа
- Root := XMLDocument.DocumentElement;
- // Получаем название теста и кол-во вопросов
- FStTest.Text := Root.Attributes['text'];
- FStTest.QCount := StrToInt( Root.Attributes['qcount'] );
- // Цикл по всем ветвям второго уровня (по вопросам)
- for i := 0 to Root.ChildNodes.Count - 1 do
- begin
- // Создаем вопрос, и получаем значения свойств
- tmpQuestion := TStQuestion.Create;
- tmpQuestion.Text := Root.ChildNodes[i].Attributes['text'];
- tmpQuestion.Active := StrToBool(
- Root.ChildNodes[i].Attributes['active'] );
- Node := Root.ChildNodes[i];
- // Цикл по ветвям третьего уровня (по вариантам ответа)
- for j := 0 to Node.ChildNodes.Count - 1 do
- begin
- // Создаем вариант ответа и заполняем свойства
- tmpAnswer := TStAnswer.Create;
- tmpAnswer.StQuestion := tmpQuestion;
- tmpAnswer.Right := StrToBool(
- Node.ChildNodes[j].Attributes['right'] );
- tmpAnswer.Text := Node.ChildNodes[j].Attributes['text'];
- // Добавляем в список вариантов ответа вопроса
- tmpQuestion.StAnswerList.Add( tmpAnswer );
- end;
- // Добавляем вопрос в список
- FStQuestionList.Add( tmpQuestion );
- end;
- end;
Метод Save
Выполняет сохранение данных из списка вопросов и настроек теста в XMLфайл. Этот метод как и LoadData для работы с XML использует TXMLDocument.
- procedure TStData.Save;
- var
- List: TObjectList;
- i, j: integer;
- tmpAnswer: TStAnswer;
- Root: IXMLNode;
- Node: IXMLNode;
- NodeAns: IXMLNode;
- begin
- List := FStQuestionList;
- // Получаем корневую ветвь документа
- Root := XMLDocument.DocumentElement;
- // Задаем ее атрибуты, полученные из StTest
- Root.Attributes['text'] := StTest.Text;
- Root.Attributes['qcount'] := IntToStr(StTest.QCount);
- // Очищает корневую ветвь (удаляем вопросы)
- Root.ChildNodes.Clear;
- // Цикл по всем вопросам
- for i := 0 to List.Count - 1 do
- begin
- // Создаем ветвь вопроса и задаем ее атрибуты
- Node := Root.AddChild( 'question' );
- Node.Attributes['text'] := TStQuestion( List.Items[i] ).Text;
- Node.Attributes['active'] := TStQuestion( List.Items[i] ).Active;
- // Цикл по вариантам ответа
- for j := 0 to TStQuestion( List.Items[i] ).StAnswerList.Count - 1 do
- begin
- // Создаем ветвь варианта ответа и задаем ее атрибуты
- tmpAnswer := TStAnswer( TStQuestion(
- List.Items[i] ).StAnswerList.Items[j] );
- NodeAns := Node.AddChild( 'answer' );
- NodeAns.Attributes['right'] := BoolToStr( tmpAnswer.Right, true );
- NodeAns.Attributes['text'] := tmpAnswer.Text;
- end;
- end;
- // Сохраняем XML в файл
- XMLDocument.SaveToFile( FFileName );
- end;
Методы AppendQuestion и DeleteQuestion (добавление и удаление вопросов)
Эти методы являются лишь оболочками для методов Add и Remove для списка вопросов – StQuestionList. Особой нужды в них нет, сделаны лишь для удобства.
- procedure TStData.AppendQuestion( newQuestion: TStQuestion );
- begin
- FStQuestionList.Add( newQuestion );
- end;
- procedure TStData.DeleteQuestion( delQuestion: TStQuestion );
- begin
- FStQuestionList.Remove( delQuestion );
- end;
Деструктор
В деструкторе уничтожаются список вопросов, настройки теста и XMLDocument.
На этом, собственно, работа с XMLпочти закончена. Мы написали удобный интерфейс для получения и сохранения данных, правда пока не учли возможность шифрования, и еще кое-что…
Паттерн «Синглтон» (Одиночка)
Обе части системы тестирования (адмниская и пользовательская) могут работать лишь с одним файлом и с один набором данных. Если попытаться подключить еще один файл, или создать другой набор данных, ни к чему хорошему это не приведет.
Следовательно, нам нужно быть точно уверенным, что существует лишь только один объект класса TStData, и желательно предоставить к нему доступ из любой точки программы, что бы ни маяться с передачей его как параметра.
Все это позволяет сделать паттерн (шаблон) проектирования Одиночка или Singleton. Его смысл в том, что при попытке создать новый объект, возвращается ссылка на старый. И таким образом обеспечивается существование одного объекта и к нему предоставляется глобальная точка доступа.
Реализация этого паттерна довольно простая. Достаточно скрыть конструктор класса в private или protected, и добавить два статических члена:
FInstance – статическая переменная типа TStData, в ней как раз и будет храниться единственный объект этого типа. Инициализируется nil'ом.
GetInstance – статическая функция, которая проверяет является ли FInstance nil'ом. Если да, то создается новый объект этого класса и записывается в FInstance. Возвращается значение поля FInstance.
- TStData = class(TObject)
- public
- ...
- class function GetInstance: TStData;
- ...
- strict private
- constructor Create;
- strict private
- class var FInstance: TStData;
- end;
- ...
- class function TStData.GetInstance: TStData;
- begin
- if FInstance = nil then
- begin
- FInstance := TStData.Create;
- end;
- Result := FInstance;
- end;
Теперь что бы начать работу с объектом класса TStData, достаточно вызвать метод GetInstance. То есть будет что-то типа такого:
- var
- Data: TStData;
- begin
- Data := TStData.GetInstance;
- end;
Наконец, можно перейти к шифрованию.
Шифрование данных
Создадим еще один unit в папке lib. Назовем его CodeUnit.
Для шифрования будем использовать простейший метод XOR. Подробно про него останавливаться не буду, в интернете можно найти достаточно информации. Стоит отметить, что для шифрования и дешифрования применяется один и та же функция.
Введем класс TStEncode, который будет содержать три статических метода. Сама функция шифрования и дешифрования. Функция, которая читает файл, и возвращает дешифрованные данные. А так же функция, которая выполняет шифрование строки и сохранение ее в файл.
- unit CodeUnit;
- interface
- uses
- Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
- Dialogs;
- type
- TStEncode = class(TObject)
- public
- // Шифрование строки. В качетсве параметра принимает строку,
- // которую нужно зашифровать и ключ шифрования.
- class function Code(const EString: string; Key: integer): string;
- // Получение дешифрованного содержимого файла в виде строки
- // Ключом является число 10
- class function GetFile(const fileName: string): string;
- // Сохранение строки в файл с шифрованием
- class procedure SaveFile(const Content: string; const FileName: string);
- end;
- implementation
- class function TStEncode.Code(const EString: string; Key: integer): string;
- var
- dd: integer;
- i: integer;
- str: string;
- begin
- str := EString;
- for i := 1 to Length(str) do
- begin
- dd := Ord(str[i]);
- dd := dd XOR Key;
- str[i] := Chr(dd);
- end;
- Result := str;
- end;
- class function TStEncode.GetFile(const fileName: string): string;
- var
- myFile : TextFile;
- text : string;
- begin
- Result := '';
- AssignFile(myFile, fileName);
- Reset(myFile);
- while not EOF(myFile) do
- begin
- ReadLn(myFile, text);
- Result := Result + text;
- end;
- Result := Code( Result, 10 );
- CloseFile(myFile);
- end;
- class procedure TStEncode.SaveFile(const Content: string; const FileName: string);
- var
- myFile : TextFile;
- Str: string;
- begin
- Str := Code( Content, 10 );
- AssignFile(myFile, fileName);
- ReWrite(myFile);
- Write(myFile, Str);
- WriteLn(myFile);
- CloseFile(myFile);
- end;
- end.
Поскольку мы ввели шифрование, то нужно немного подкорректировать некоторые методы класса TStData.
В методе Load нужно загружать документ в парсер не из файла (т.к. файл шифрованный) а из строки полученной при дешифровке.
- procedure TStData.Load(setFileName: string);
- begin
- FFileName := setFileName;
- XMLDocument.LoadFromXml( TStEncode.GetFile(setFileName) );
- LoadData;
- end;
А в методе Save нужно добавить шифрование при сохранении:
- procedure TStData.Save;
- var
- ...
- XMlStr: string;
- begin
- ...
- XMLDocument.SaveToXML( XMlStr );
- TStEncode.SaveFile( XMlStr, FFileName );
- end;
На этом работа с данными можно считать полностью завершенной. Мы реализовали простую загрузку и сохранение данных, а так же разобрались с шифрованием файла.
delphi, тест, xml, паттерны
Комментарии (1)
как создать xml документа????
Добавить комментарий