Creating a Multiple Choice Quiz System, Part 2

Designing our CGI quiz to be more robust and to include error checking.

Two months ago, we began to write a
simple multiple-choice quiz engine in Perl. Virtually all of that
column covered the nuts and bolts of the engine—creating the
QuizQuestions object and the programs using that
object to create a simple multiple-choice quiz, as well as to check
its answers.

The end result was two CGI programs. The first,
askquestion.pl, creates an instance of
QuizQuestions and uses it to select a random
question, which is then turned into an HTML form that is sent to
the user's browser.

The other program, checkanswer.pl, accepts the submission of
this form from the user, and then checks that the user chose the
correct answer.

Even more important than the QuizQuestions
object is the “quiz file”, an ASCII text file containing three
different types of items:

Comments beginning with a hash character (#).
Comments are ignored by the quiz engine. Therefore, questions must
not begin with #, but we can use
# inside a question or answer without having to
fear that the end of the question will be chopped off.

Whitespace, such as spaces, tab characters and
carriage returns that are also ignored. We allow for whitespace
because users will undoubtedly separate items in the quiz file with
blank lines, for example, and we need not require them to comment
out the lines.

Question records containing the questions and
answers for the quiz. Each record contains the text of a question,
followed by each of the four possible answers, and then by an A, B,
C or D, indicating the correct answer. The fields in each record
(question, answer 1, answer 2, answer 3, answer 4 and the correct
answer) are separated by tab characters, and so, neither questions
nor answers can contain tabs.

A sample quiz file that tests users on their knowledge of the
GNU Emacs text editor is shown in Listing
1. While this may not be obvious on paper, it is important
to remember that the fields within each line are separated by tab
characters, not by spaces.

One of the main flaws with the original quiz system was that
it depended on the ability of users to create quiz files that
conformed to these standards. Moreover, the
QuizQuestions object didn't check for errors in
format when reading the quiz file.

This month we take a look at how we can make the quiz system
a bit more robust, while staying within the confines of the CGI
standard.

Checking for Errors

First, we will modify the definition of
QuizQuestions so that it checks for errors while
loading the quiz file. What sorts of errors could we have it check
for? One simple test ensures that each non-commented,
non-whitespace line contains exactly six fields (one question, four
answers and one answer key). Lines having a different number of
fields will be flagged as errors.

The original version of QuizQuestions.pm
is shown in Listing 2. To make sure that the quiz file is correct,
we have to modify methods that read from the quiz file—which in
this particular case, means the new method, the
constructor for QuizQuestions. We can create a
new instance of QuizQuestions with the following
line:

my $quiz = new QuizQuestions("emacs");

Before we decide how to check for errors in the quiz file, we
should think about how errors should be reported. If a method
within QuizQuestions.pm discovers an error in
the quiz file, should the method produce an HTML response for the
user to see? Should it fail, calling die and
indicating the error in the HTTP server's error log? Should it do
both?

I suggest that QuizQuestions.pm should not
use either of these options, since both violate the abstraction
that we have created. QuizQuestions is an object
for manipulating questions within a quiz file easily, and does not
“know” whether it is being used from within a CGI program.
Methods within QuizQuestions should report
errors, when they occur, to the calling program rather than
directly to the user.

If we were using a language such as Java that includes an
extensive exception-handling mechanism, this would be a perfect
time to use it; we don't want the calling routine to receive a
return value that could be misinterpreted as a legitimate value for
$quiz. At the same time, we do want to return
information about any errors that have occurred.

Perl's exception handling isn't as extensive as that of Java.
Luckily, though, Perl does permit assigning various types of data
to the same operator. In this case, if the file contains no errors,
new returns a new instance of
QuizQuestions. If there are errors in the file,
new returns a string that consists of the line
containing the error. It could simply return 0 in such cases;
however, since we have the flexibility to return any scalar value,
it is better to return a value that encodes more
information.

Now that we have determined that error messages will be sent
back to the calling method, let's think about how to determine
which lines in the quiz file contain errors. Fortunately, this is a
simple problem to solve, since each non-comment, non-blank line of
a quiz file should contain exactly six tab-separated fields. Thus,
if a line is not a comment, is not an empty line and does not
contain six fields, it must be an error and should generate an
error value.

Here is the loop in the existing version of
new inside the QuizQuestions
object that loads the quiz file from disk:

To check for errors, we simply break each line into its
constituent fields using the split operator and
count the number of list elements. If that number is not six, then
we have a syntax error to be reported by returning the offending
string to the calling routine. Here is a modified version of the
above loop that implements this strategy:

This code is the same as the original while loop
with only one difference. Before adding the current line,
$_, to @questions (an array
containing questions and answers from the quiz file), we split it
at each tab, creating a list with one element per field in the quiz
file. If the list contains six elements, then this line of the quiz
file is acceptable, and we continue with the original version of
new--adding the current line to
@questions, incrementing
$counter, and moving on to the next line of the
file.

If the list does not contain six fields,
the line obviously contains an error. By the time we perform this
test, we have already eliminated the possibility that the current
line could be a comment or solely contain whitespace.

But wait a second—the caller is expecting to receive an
object of type QuizQuestions in return. Because
the QuizQuestions object can return many
different kinds of scalar data, we have to make sure that the
caller can determine whether the method invocation was a success
(i.e., an object was returned) or a failure (i.e., a string was
returned).

In this case, we use Perl's ref operator
to find out if a scalar is a reference to an object and what kind
of object it is. Invoking ref on a non-object
scalar returns an empty string, which makes such testing easy. So,
in the above version of new, we can create an
instance of QuizQuestions with this code:

The second line checks to see if
$questions is an instance of
QuizQuestions. If not, we call
&log_and_die, a routine (included in in
Listing 5) that provides nicer logging of errors than a simple call
to die.

While this code works, it makes for a poorly designed object.
After all, why write the constructor so that the caller has to test
the type of the object it returned? A better solution is to make
new a minimalist creation method, and put the
quizfile-loading mechanism into another method, called
loadFile. This new method could then return
either 0 indicating no error or a string containing the offending
line.

This code creates an instance of
QuizQuestions using the new
operator, which does only the bare essentials. We load quiz file
with the loadFile method. The
loadFile method returns either 0, indicating
that the file was loaded successfully, or a text string containing
the line that caused a problem.

Since we modified loadFile to deal with
errors, I have replaced the original uses of die
which are inappropriate in a low-level object, (as mentioned
earlier), with calls to return.

Trending Topics

Webinar: 8 Signs You’re Beyond Cron

Scheduling Crontabs With an Enterprise Scheduler
11am CDT, April 29th

Join Linux Journal and Pat Cameron, Director of Automation Technology at HelpSystems, as they discuss the eight primary advantages of moving beyond cron job scheduling. In this webinar, you’ll learn about integrating cron with an enterprise scheduler.