Refactoring mod/quiz/attempt.php

Refactoring mod/quiz/attempt.php

by Henrik Kaipe -
Number of replies: 4

The attached file contains an "attempt.php" that, according to me, is much better structured than the current version. The main difference is found inside the 'foreach' loop that parses "input for question->answers" from the names ($key) of the posted parameters.

It looks like this:
        foreach ($rawanswers as $key => $value) {       // Parse input for question -> answers
            if (substr($key, 0, 1) == "q") {
                $key = substr($key,1);
                if (isset($questions[$key])) {          // It's a real question number, not a coded one
                    $questions[$key]->answer[] = trim($value);

                } else if (substr_count($key, "rq")) {  // Random Question information
                    $check = explode("rq", $key);
                    $key   = $check[0];                 // The random question id
                    $real  = $check[1];                 // The real question id
                    $questions[$key]->random = $real; 

                } else if (substr_count($key, "a")) {   // Checkbox style multiple answers
                    $check = explode("a", $key);
                    $key   = $check[0];                 // The main question number
                    $value = $check[1];                 // The actual answer
                    $questions[$key]->answer[] = trim($value); 

                } else if (substr_count($key, "r")) {   // Random-style answers
                    $check = explode("r", $key);
                    $key   = $check[0];                 // The main question
                    $rand  = $check[1];                 // The random sub-question
                    $questions[$key]->answer[] = "$rand-$value";

                } else {
                    error("Answer received for non-existent question ($key)!");
                }
            } else if ($key == "shuffleorder") {
                $shuffleorder = explode(",", $value);   // Actual order questions were given in
            }
        }

My suggestion for refactoring is
        foreach ($rawanswers as $key => $value) {       // Parse input for question -> answers

            if (ereg('^q([0-9]+)$', $key, $keyregs)) { // It's a real question number, not a coded one
                $questions[$keyregs[1]]->answer[] = trim($value);

            } else if (ereg('^q([0-9]+)rq([0-9]+)$', $key, $keyregs)) { // Random Question information
                $questions[$keyregs[1]]->random = $keyregs[2];

            } else if (ereg('^q([0-9]+)a([0-9]+)$', $key, $keyregs)) { // Checkbox style multiple answers
                $questions[$keyregs[1]]->answer[] = $keyregs[2];

            } else if (ereg('^q([0-9]+)r([0-9]+)$', $key, $keyregs)) { // Random-style answers
                $questions[$keyregs[1]]->answer[] = "$keyregs[2]-$value";

            } else if ('shuffleorder' == $key) {
                $shuffleorder = explode(",", $value);   // Actual order questions were given in
           
            }
        }

My key argument for the refactoring is that my code would work fine also after removing all the 'else's:
        foreach ($rawanswers as $key => $value) {       // Parse input for question -> answers

            if (ereg('^q([0-9]+)$', $key, $keyregs)) { // It's a real question number, not a coded one
                $questions[$keyregs[1]]->answer[] = trim($value);
            }

            if (ereg('^q([0-9]+)rq([0-9]+)$', $key, $keyregs)) { // Random Question information
                $questions[$keyregs[1]]->random = $keyregs[2];
            }

            if (ereg('^q([0-9]+)a([0-9]+)$', $key, $keyregs)) { // Checkbox style multiple answers
                $questions[$keyregs[1]]->answer[] = $keyregs[2];
            }

            if (ereg('^q([0-9]+)r([0-9]+)$', $key, $keyregs)) { // Random-style answers
                $questions[$keyregs[1]]->answer[] = "$keyregs[2]-$value";
            }

            if ('shuffleorder' == $key) {
                $shuffleorder = explode(",", $value);   // Actual order questions were given in
            }
        }

This works because each one of the if clauses evaluates true for the input parameter its body concern and false for all other input parameters. Why even consider a different structure? The reason for considering a different structure, like the current one, is probably that the developer was not aware of the power of using regular expressions and the function ereg.

The original structure is not only nested, it is also dependent on having (some of) the if-bodies in the right order, i.e. it would not work to put "Random-style answers" before "Random Question information". That and simular problems disappear in the refactoring. It is then much easier for a new developer to shoot in another if-ereg clause for any new attempt.php input parameter (that is what I am about to do) as the ereg expressions definetely define the parameter names that are already in use. You can then come down with your own exclusive input parameter pattern (like ^q[0-9]+ma[0-9]+$ ) and then add it as a separate 'else if' clause without worring about any possible name conflicts.

PS
I have kept the 'else's for performance reasons.
DS

Average of ratings: -
In reply to Henrik Kaipe

Refactoring quiz module

by Martin Dougiamas -
Picture of Core developers Picture of Documentation writers Picture of Moodle HQ Picture of Particularly helpful Moodlers Picture of Plugin developers Picture of Testers
Thank you for that. I am/was very much aware of regular expressions - the structure is like that because it grew gradually as each question type was added, and because in general I avoid regexps to save processor cycles.

But it makes perfect sense to fix this in the way you describe (as well as other things about the quiz module) given the strong interest from others in expanding the quiz capabilities. Have you tested all the question types with this new code? - if so I'll check it in.

If you're thinking about refactoring anyway, perhaps you'd like to give some thought to a complete plug-in architecture where question types are completely defined in external classes (a little like the import formats). It would be less efficient certainly but infinitely flexible. I've had this in mind for some time, which is why I have these big separate switch statements in lib.php even though some of the code is duplicated, but there's about 100 other things I need to look at first.

What do you think?
In reply to Martin Dougiamas

Re: Refactoring quiz module

by Henrik Kaipe -

Aren't Australians supposed to be sleeping at this time? biggrin.gif

Anyway

I have tested all the question types with this new code. Everything seems to work fine (if we excuse bug #320).

I have also wished for a plug-in architechture for questions types (as it is for import formats) but that refactoring would demand a greater insight and much more effort than this rather small refactoring.

However, it could be possible to do it step by step. We add one class for each question type and then gradually implement functionality from one lib.php switch-statement at a time.

I am not sure whether this plug-in architechture is easily implemented for some of the more special question-types, like the very sophisticated "Random".

In reply to Martin Dougiamas

Re: Refactoring quiz module

by Henrik Kaipe -
Before you check it in, there is a little more than the foreach-loop that I've been defending. Please review the code before you check it in!
In reply to Martin Dougiamas

Re: Refactoring quiz module - lib.php

by Henrik Kaipe -

As we're at it. I refactored the grading-attempt part of lib.php as well.
The previous special treatment of qtype DESCRIPTION and RANDOM was rather awkward.

I have now removed that special treatment and included the cases DESCRIPTION and RANDOM in the switch statements, therewith having them follow the same pattern as all the other qtype.

The methods modified are

1. quiz_get_answers

2. quiz_grade_attempt_results, where the switch statement as been seperated out into a new method - quiz_grade_attempt_question_result - in order to allow a recursive algorithm for the case RANDOM.

I hope you see this as one step on the way to make a plug-in structure for qtype.

I have tested this on my local system and it seems to work just fine.