Maintaining bad code, as a lesson

Alex and bcs suggest teaching students to maintain code (and, hopefully, showing them why they might want to write maintainable code) by giving them horrible code to modify:

  • a bunch of test cases
  • a pile of #$%#$ library that passes them but will fail on just about anything else
  • incomplete, inconstant and wrong documentation and specifications.
  • several applications that use the library (only some of which they are allowed to alter).
  • more bug reports than the whole class can address in the time allotted.

Grade them on how many bugs they fix. Include performance issues, feature requests, usability issues and even a few can-not-reproduce and works-as-intended issues. Just to be evil, include a bug where the code is clearly wrong but fixing it introduces a bug in one of the apps (one that can’t be altered) that is easy to spot by inspection but not covered by any tests.

Setup time: 15 man-years.

This isn't nearly as hard as it sounds, because bad code is ridiculously easy to write. You can take a piece of good code and turn it into an unmaintainable mess much faster than you could write that unmaintainable mess from scratch. It doesn't take clever obfuscation, just repeated stupidity. Hints:

  • Think of the worst code you've had to deal with, and imitate its mistakes.
  • Use unsuitable data representations. Strings are often a good bad choice. Don't forget to forget to provide escape sequences for any embedded strings.
  • Inline rampantly. This is how you get thousand-line functions nearly duplicated three times.
  • Misname things. There's nothing like a misleading name to delay understanding. A little Hungarian notation can help lengthen names without adding useful information.
  • Abuse state: Factorial fact; fact.compute(3); fact.getResult() ⇒ 6
  • Take commandments literally: if told you should “program to an interface, never an implementation”, add redundant interfaces to every class, even private ones.
  • Flout commandments: if told to “favor composition over inheritance”, replace perfectly good composition with inheritance.
  • Replace uses of the standard library and other convenient language features with unnecessary code.
  • Move variables to larger scopes, or to different classes or globals.
  • Add useless diagnostics, e.g. logging entry and exit to a function but not its arguments.
  • Add redundant, unnecessary safety checks, and omit necessary ones.
  • Introduce abstractions in the wrong places.
  • When you find a bug, add a special case to hide it. Or declare it a feature and add a test for it.
  • Expose the wrong things in interfaces.
  • Ignore your better judgement. Do what an idiot would do.
  • Handle new requirements by adding special cases. Do this last, so the special cases hinder refactoring.
  • Write tests, but don't worry too much about whether they pass. You'll only give the students the ones that pass; the ones that fail will become the bug reports.

For example, here's a simple perverse factorial:

#include <string.h>
#include <stdio.h>

char s[10];

void factorial(int n, char *num) {
  int i = 0, p = 0;
  s[0] = '1';
 add:
  for (i = 0; i < 10; ++i) {
    int x = (s[i] - 48) * n + p + 48;
    if (n == 1) {
      for (i = 0; i < 10; ++i)
        num[i] = s[9-i];
      return;
    }
    if (s[i] == 0) {
      s[i] = '0';
      x = (s[i] - 48) * n + p + 48;
    }
    p = 0;
    while (x > 57) {
      p++;
      x -= 10;
    }
    s[i] = x;
  }
  n--;
  goto add;
}

void test_one(int n, const char *expected) {
  char result[20] = "           "; //to detect lack of null-termination
  factorial(n, result);
  if (strncmp(result, expected, 10) || result[10] != ' ')
    fprintf(stderr, "factorial(%d) ⇒ %s (expected %s)\n", n, result, expected);
}

int main(int argc, char **argv) {
  //test_one(0, "0000000001"); //infinite loop
  test_one(3, "0000000006");
  test_one(10, "0003628800");
  test_one(3, "0000000006"); //fails due to leftovers
  test_one(14, "87178291200"); //overflows
  return 0;
}

That will probably take as long to understand as it did to write, and longer to make all five tests pass (never mind making it correct).

This is easy (and fun) enough that you could get students to do it — maybe have them write a problem to inflict on the next class, as the last homework of the term.

Homework for the more advanced student in computational mischief: automate this breakage.

Homework for the Ph.D. student in software engineering research: set up a puzzle site like 4clojure where users solve maintenance problems. Measure the effects of different stylistic flaws on the time taken and success rate. Find out which ones really matter.

No comments:

Post a Comment

It's OK to comment on old posts.