Система тестирования. Часть 2. Работа с данными

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

Структура XML файла

Рассмотрим структуру XML файла, в незашифрованном виде, на примере:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <test text="Delphi" qcount="0">
  3.     <question text="Delphi - объектно ориентированный язык?" active="true">
  4.         <answer right="False" text="нет"/>
  5.         <answer right="true" text="да"/>
  6.     </question>
  7.     <question text="Какая функция или процедура выполнит
  8.                     преобразование string в integer?" active="true">
  9.         <answer right="true" text="StrToInt"/>
  10.         <answer right="true" text="Val"/>
  11.         <answer right="false" text="StringToInteger"/>
  12.     </question>
  13.     <question text="Есть ли в Delphi динамические массивы?" active="false">
  14.         <answer right="false" text="Да, были в Delphi с самой первой версии"/>
  15.         <answer right="false" text="Нет"/>
  16.         <answer right="true" text="Да, появились с версии 4"/>
  17.     </question>
  18. </test>

Как видно, test  - корневая ветвь документа. Ее атрибуты:

  • Text – название теста
  • Qcount– количество используемых вопросов. Если 0, тогда используются все вопросы из файла.

Вопросом является ветвь question, которая является родительской для ветви answer – варианта ответа. Рассмотрим атрибуты вопроса:

  • Text – сам текст вопроса.
  • Active– флаг, определяющий является ли вопрос активным (будет ли использован в тесте)

Вариантов ответа может любое количество. Притом опять же любое количество из них может быть верным. Атрибуты:

  • Right – флаг, определяющий является ли вариант ответа верным или нет.  
  • Text – текст варианта ответа.

Данные

Весь код, который мы будем использовать для работы с xml файлом и данными из него целесообразно определить в один unit. Назовем его StData и положим в папку lib, так как с данными должны работать и представление и администрирование.

Перед тем как начать писать код, разбирающий XML нужно понять, как мы будем представлять данные и, собственно, с какими данными будем работать. Исходя из структуры XML файла, можно выделить следующее: сам тест (название и количество вопросов), вопрос (текст и активность) и вариант ответа (правильность и текст). Все это можно представить в виде классов. То есть получить классы TStTest, TStQuestion и TStAnser для теста, вопроса, и варианта ответа соответственно.  

Удобно будет, если вопрос будет хранить список вариантов ответа, а вариант ответа, знать какому вопросу он принадлежит.

Учитывая вышесказанное, получаем следующие классы:

  1. unit StData;
  2.  
  3. interface
  4.  
  5. uses
  6.   SysUtils, Windows, Messages, Classes, Graphics, Controls, Forms, Dialogs,
  7.   xmldom, XMLIntf, msxmldom, XMLDoc, StdCtrls, Contnrs, CodeUnit;
  8.  
  9. type
  10.   // Вопрос
  11.   TStQuestion = class
  12.   private
  13.     FActive: boolean;
  14.     FText: string;
  15.   public
  16.     // Список вариантов ответа
  17.     StAnswerList: TObjectList;
  18.  
  19.     constructor Create;
  20.     destructor Destroy;
  21.     // Активен ли вопрос
  22.     property Active: boolean read FActive write FActive;
  23.     // Текст вопроса
  24.     property Text: string read FText write FText;
  25.   end;
  26.  
  27.   // Вариант ответа
  28.   TStAnswer = class
  29.   private
  30.     FRight: Boolean;
  31.     FStQuestion: TStQuestion;
  32.     FText: string;
  33.   public
  34.     // Верный ли варинат ответа
  35.     property Right: Boolean read FRight write FRight;
  36.     // Сам вопрос
  37.     property StQuestion: TStQuestion read FStQuestion write FStQuestion;
  38.     // Текст варианта ответа
  39.     property Text: string read FText write FText;
  40.   end;
  41.  
  42.   // Тест
  43.   TStTest = class(TObject)
  44.   private
  45.     FQCount: Integer;
  46.     FText: string;
  47.   public
  48.     // Название (описание) теста
  49.     property Text: string read FText write FText;
  50.     // Максимальное количество вопросов
  51.     property QCount: Integer read FQCount write FQCount;
  52.   end;
  53.  
  54. implementation
  55.  
  56. constructor TStQuestion.Create;
  57. begin
  58.   StAnswerList := TObjectList.Create;
  59. end;
  60.  
  61. constructor TStQuestion.Destroy;
  62. begin
  63.   StAnswerList.Free;
  64.   inherited Destroy;
  65. end;
  66.  
  67. end.

Следует отметить, что для хранения списка вариантов ответа используется контейнер TObjectList. Это не лучший вариант, так как в нем хранятся объекты типа TObject, и для использования придется постоянно приводить к типу TStAnser. В последних версиях Delphi появились generetic (шаблоны), и лучше использовать их, если точно известно, что программа не будет компилироваться на ранних версиях.

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

Создадим для этого отдельный класс, назовем его TStData. Для парсинга XML будем использовать встроенный в Delphi компонент TXMLDocument.

Разберемся, что же нужно от класса TStData:

  • Получение списка вопросов и настроек теста из XML.
  • Сохранение вопросов и настроек теста в XML.

Кроме этого, еще стоит вынести возможность управления вопросами (добавление и удаление) в этот класс. Список вопросов и настройки теста стоит сделать доступными только для чтения. Так же необходимо некоторое свойство доступное как для записи, так и для чтения, в котором будет храниться имя XML файла с данными.

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

И, в итоге, получим следующий класс:

  1. TStData = class(TObject)
  2. private
  3.   FStQuestionList: TObjectList;
  4.   FStTest: TStTest;
  5.   // Парсер XML
  6.   XMLDocument: IXMLDocument;
  7.   FFileName: string;
  8.  
  9.   // Процедура заргузки данных из XML
  10.   procedure LoadData;
  11. public
  12.   procedure Load(setFileName: string);
  13.   // Загрузка данных из строки
  14.   procedure LoadFromXml(XMLStr: string);
  15.   // Получение данных, если XML файла нет
  16.   procedure LoadEmpty;
  17.   // Сохранение в XML
  18.   procedure Save;
  19.  
  20.   procedure AppendQuestion( newQuestion: TStQuestion );
  21.   procedure DeleteQuestion( delQuestion: TStQuestion );
  22.  
  23.   constructor Create;
  24.   destructor Destroy;
  25.  
  26.   // Имя XML файла
  27.   property FileName: string read FFileName write FFileName;
  28.   // Список вопросов
  29.   property StQuestionList: TObjectList read FStQuestionList;
  30.   // Настройки теста
  31.   property StTest: TStTest read FStTest;
  32. end;

Рассмотрим его методы.

Конструктор класса

В конструкторе идет создание объектов используемых в классе. Таких как,  объекта для парсинга XML– TXMLDocument, списка вопросов и настроек теста. Кроме того предполагается, что никакого файла нет, и загружается пустая структура xml (LoadEmpty).

  1. constructor TStData.Create;
  2. begin
  3.   XMLDocument := TXMLDocument.Create( nil );
  4.   FStQuestionList := TObjectList.Create;
  5.   FStTest := TStTest.Create;
  6.   LoadEmpty;
  7. end;

Метод Load

Принимает имя xml файла, он загружается в объект XMLDocument, и вызывается процедура загрузки данных – LoadData.

  1. procedure TStData.Load(setFileName: string);
  2. begin
  3.   FFileName := setFileName;
  4.   // Загружаем xml документ из файла
  5.   XMLDocument.LoadFromFile( FFileName );
  6.   LoadData;
  7. end;

Метод LoadEmpty

Как правило, этот метод вызывается, тогда, когда никакого файла, а соответственно, данных, еще нет. В строковую константу записана валидная структура пустого XML. Потом она загружается в XMLDocumentкак строка и вызывается процедура LoadData.

  1. procedure TStData.LoadEmpty;
  2. const
  3.   DEFAULT_HEADER = '<?xml version="1.0" encoding="UTF-8"?>' +
  4.     '<test text="" qcount="0"></test>';
  5. begin
  6.   XMLDocument.LoadFromXML( DEFAULT_HEADER );
  7.   LoadData;
  8.   FFileName := '';
  9. end;

Метод LoadData

Этот метод как раз выполняет чтение данных из XML и заносит их в объекты StQuestionList (список вопросов) и StTest (настройки теста). Метод достаточно объемный, но благодаря парсеру XMLDocument, чтение XML достаточно понятно и удобно.

  1. procedure TStData.LoadData;
  2. var
  3.   i, j: integer;
  4.   tmpQuestion: TStQuestion;
  5.   tmpAnswer: TStAnswer;
  6.  
  7.   Root: IXMLNode;
  8.   Node: IXMLNode;
  9. begin
  10.   // Очистим список вопросов на тот случай если загрузка данных
  11.   // вызывается еще раз
  12.   FStQuestionList.Clear;
  13.   // Корневая ветвь документа
  14.   Root := XMLDocument.DocumentElement;
  15.  
  16.   // Получаем название теста и кол-во вопросов
  17.   FStTest.Text := Root.Attributes['text'];
  18.   FStTest.QCount := StrToInt( Root.Attributes['qcount'] );
  19.   // Цикл по всем ветвям второго уровня (по вопросам)
  20.   for i := 0 to Root.ChildNodes.Count - 1 do
  21.     begin
  22.       // Создаем вопрос, и получаем значения свойств
  23.       tmpQuestion := TStQuestion.Create;
  24.       tmpQuestion.Text := Root.ChildNodes[i].Attributes['text'];
  25.       tmpQuestion.Active := StrToBool(
  26.                               Root.ChildNodes[i].Attributes['active'] );
  27.      
  28.       Node := Root.ChildNodes[i];
  29.       // Цикл по ветвям третьего уровня (по вариантам ответа)
  30.       for j := 0 to Node.ChildNodes.Count - 1 do
  31.         begin
  32.           // Создаем вариант ответа и заполняем свойства
  33.           tmpAnswer := TStAnswer.Create;
  34.           tmpAnswer.StQuestion := tmpQuestion;
  35.           tmpAnswer.Right := StrToBool(
  36.                                 Node.ChildNodes[j].Attributes['right'] );
  37.           tmpAnswer.Text := Node.ChildNodes[j].Attributes['text'];
  38.          
  39.           // Добавляем в список вариантов ответа вопроса
  40.           tmpQuestion.StAnswerList.Add( tmpAnswer );
  41.         end;
  42.      
  43.       // Добавляем вопрос в список
  44.       FStQuestionList.Add( tmpQuestion );
  45.     end;
  46. end;

Метод Save

Выполняет сохранение данных из списка вопросов и настроек теста в XMLфайл. Этот метод как и LoadData для работы с XML использует TXMLDocument.

  1. procedure TStData.Save;
  2. var
  3.   List: TObjectList;
  4.   i, j: integer;
  5.   tmpAnswer: TStAnswer;
  6.  
  7.   Root: IXMLNode;
  8.   Node: IXMLNode;
  9.   NodeAns: IXMLNode;
  10. begin
  11.   List := FStQuestionList;
  12.  
  13.   // Получаем корневую ветвь документа
  14.   Root := XMLDocument.DocumentElement;
  15.   // Задаем ее атрибуты, полученные из StTest
  16.   Root.Attributes['text'] := StTest.Text;
  17.   Root.Attributes['qcount'] := IntToStr(StTest.QCount);
  18.   // Очищает корневую ветвь (удаляем вопросы)
  19.   Root.ChildNodes.Clear;
  20.  
  21.   // Цикл по всем вопросам
  22.   for i := 0 to List.Count - 1 do
  23.     begin
  24.       // Создаем ветвь вопроса и задаем ее атрибуты
  25.       Node := Root.AddChild( 'question' );
  26.       Node.Attributes['text'] := TStQuestion( List.Items[i] ).Text;
  27.       Node.Attributes['active'] := TStQuestion( List.Items[i] ).Active;
  28.      
  29.       // Цикл по вариантам ответа
  30.       for j := 0 to TStQuestion( List.Items[i] ).StAnswerList.Count - 1 do
  31.         begin
  32.           // Создаем ветвь варианта ответа и задаем ее атрибуты
  33.           tmpAnswer := TStAnswer( TStQuestion(
  34.                                     List.Items[i] ).StAnswerList.Items[j] );
  35.           NodeAns := Node.AddChild( 'answer' );
  36.           NodeAns.Attributes['right'] := BoolToStr( tmpAnswer.Right, true );
  37.           NodeAns.Attributes['text'] := tmpAnswer.Text;
  38.         end;
  39.  
  40.     end;
  41.  
  42.   // Сохраняем XML в файл
  43.   XMLDocument.SaveToFile( FFileName );
  44. end;

Методы AppendQuestion и DeleteQuestion (добавление и удаление вопросов)

Эти методы являются лишь оболочками для методов Add и Remove для списка вопросов – StQuestionList. Особой нужды в них нет, сделаны лишь для удобства.

  1. procedure TStData.AppendQuestion( newQuestion: TStQuestion );
  2. begin
  3.   FStQuestionList.Add( newQuestion );
  4. end;
  5.  
  6. procedure TStData.DeleteQuestion( delQuestion: TStQuestion );
  7. begin
  8.   FStQuestionList.Remove( delQuestion );
  9. end;

Деструктор

В деструкторе уничтожаются список вопросов, настройки теста и XMLDocument.

На этом, собственно, работа с XMLпочти закончена. Мы написали удобный интерфейс для получения и сохранения данных, правда пока не учли возможность шифрования, и еще кое-что…

Паттерн «Синглтон» (Одиночка)

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

Следовательно, нам нужно быть точно уверенным, что существует лишь только один объект класса TStData, и желательно предоставить к нему доступ из любой точки программы, что бы ни маяться с передачей его как параметра.

Все это позволяет сделать паттерн (шаблон) проектирования Одиночка или Singleton. Его смысл в том, что при попытке создать новый объект,  возвращается ссылка на старый. И таким образом обеспечивается существование одного объекта и к нему предоставляется глобальная точка доступа.

Реализация этого паттерна довольно простая. Достаточно скрыть конструктор класса в private или protected, и добавить два статических члена:

FInstance – статическая переменная типа TStData, в ней как раз и будет храниться единственный объект этого типа. Инициализируется nil'ом.

GetInstance – статическая функция, которая проверяет является ли FInstance nil'ом. Если да, то создается новый объект этого класса и записывается в FInstance. Возвращается значение поля  FInstance.

  1. TStData = class(TObject)
  2. public
  3.   ...
  4.   class function GetInstance: TStData;
  5.   ...
  6. strict private
  7.   constructor Create;
  8. strict private
  9.   class var FInstance: TStData;
  10. end;
  11.  
  12. ...
  13.  
  14. class function TStData.GetInstance: TStData;
  15. begin
  16.   if FInstance = nil then
  17.   begin
  18.     FInstance := TStData.Create;
  19.   end;
  20.   Result := FInstance;
  21. end;

Теперь что бы начать работу с объектом класса TStData, достаточно вызвать метод GetInstance. То есть будет что-то типа такого:

  1. var
  2.   Data: TStData;
  3. begin
  4.   Data := TStData.GetInstance;
  5. end;

Наконец, можно перейти к шифрованию.

Шифрование данных

Создадим еще один unit в папке lib. Назовем его CodeUnit.

Для шифрования будем использовать простейший метод XOR.  Подробно про него останавливаться не буду, в интернете можно найти достаточно информации. Стоит отметить, что для шифрования и дешифрования применяется один и та же функция.

Введем класс TStEncode, который будет содержать три статических метода. Сама функция шифрования и дешифрования. Функция, которая читает файл, и возвращает дешифрованные данные. А так же функция, которая выполняет шифрование строки и сохранение ее в файл.

  1. unit CodeUnit;
  2.  
  3. interface
  4.  
  5. uses
  6.   Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  7.   Dialogs;
  8.  
  9. type
  10.   TStEncode = class(TObject)
  11.   public
  12.     // Шифрование строки. В качетсве параметра принимает строку,
  13.     // которую нужно зашифровать и ключ шифрования.    
  14.     class function Code(const EString: string; Key: integer): string;
  15.     // Получение дешифрованного содержимого файла в виде строки
  16.     // Ключом является число 10
  17.     class function GetFile(const fileName: string): string;
  18.     // Сохранение строки в файл с шифрованием
  19.     class procedure SaveFile(const Content: string; const FileName: string);
  20.   end;
  21.  
  22. implementation
  23.  
  24. class function TStEncode.Code(const EString: string; Key: integer): string;
  25. var
  26.   dd: integer;
  27.   i: integer;
  28.   str: string;
  29. begin
  30.   str := EString;
  31.   for i := 1 to Length(str) do
  32.     begin
  33.       dd := Ord(str[i]);
  34.       dd := dd XOR Key;
  35.       str[i] := Chr(dd);
  36.     end;
  37.   Result := str;
  38. end;
  39.  
  40. class function TStEncode.GetFile(const fileName: string): string;
  41. var
  42.   myFile : TextFile;
  43.   text   : string;
  44. begin
  45.   Result := '';
  46.   AssignFile(myFile, fileName);
  47.   Reset(myFile);
  48.  
  49.   while not EOF(myFile) do
  50.     begin
  51.       ReadLn(myFile, text);
  52.       Result := Result + text;
  53.     end;
  54.  
  55.   Result := Code( Result, 10 );
  56.  
  57.   CloseFile(myFile);
  58. end;
  59.  
  60. class procedure TStEncode.SaveFile(const Content: string; const FileName: string);
  61. var
  62.   myFile : TextFile;
  63.   Str: string;
  64. begin
  65.   Str := Code( Content, 10 );
  66.   AssignFile(myFile, fileName);
  67.   ReWrite(myFile);
  68.   Write(myFile, Str);
  69.   WriteLn(myFile);
  70.  
  71.   CloseFile(myFile);
  72. end;
  73.  
  74. end.

Поскольку мы ввели шифрование, то нужно немного подкорректировать некоторые методы класса TStData.

В методе Load нужно загружать документ в парсер не из файла (т.к. файл шифрованный) а из строки полученной при дешифровке.

  1. procedure TStData.Load(setFileName: string);
  2. begin
  3.   FFileName := setFileName;
  4.   XMLDocument.LoadFromXml( TStEncode.GetFile(setFileName) );
  5.   LoadData;
  6. end;

А в методе Save нужно добавить шифрование при сохранении:

  1. procedure TStData.Save;
  2. var
  3.   ...
  4.  
  5.   XMlStr: string;
  6. begin
  7.   ...
  8.  
  9.   XMLDocument.SaveToXML( XMlStr );
  10.   TStEncode.SaveFile( XMlStr, FFileName );
  11. end;

На этом работа с данными можно считать полностью завершенной. Мы реализовали простую загрузку и сохранение данных, а так же разобрались с шифрованием файла.

delphi, тест, xml, паттерны

Комментарии (1)

эдсон | 13 января 2012 г. 2:55 Ответить

как создать xml документа????

Добавить комментарий

  • Допустимые html-теги:
    <b> </b> - жирный шрифт
    <em> </em> - наклонный
    <s> </s> - зачеркнутый
    <pre> </pre> - сохранение отступов (печать кода)
    [?]
Введите текст с картинки