![Credit: The People Speak!](http://i0.wp.com/www.usefuljs.net/wp-content/uploads/2016/10/4619130750_a6f594fd67_o.jpg?resize=640%2C452)
Some programmers are better than others. In fact, there’s a statistical distribution: a few are absolutely brilliant, some are good, most are at least competent, some are barely competent and a few are truly dire. It’s an interesting observation that the Microsofts and Googles of the world will have seriously incompetent people writing parts of their software. And the really bad software can live for years, long after the author has taken his mediocrity with him to another unsuspecting company because one characteristic of bad software is that it is fantastically baroque and no-one else wants to take ownership. So there it stays. It’s almost certainly inefficient and probably insecure but so long as it doesn’t explode, everyone just leaves it the heck alone.
I will point out that bad programmers are not necessarily stupid and are certainly not bad people (or if they are, it’s not related to them also being bad programmers), they’re just individuals who find themselves in a job that they’re unsuited for. That said, the worst programmers share a number of characteristics. There are a lot more than seven, of course, but here’s my list of seven of the worst. Bad programmers…
…Lack basic knowledge
Writing software is an art to be sure, but it’s also a craft and like any craft, there is certain basic knowledge that you need to be able to practise your craft effectively. I don’t expect you to be Donald Knuth but I do expect you to know the basics of data structures and algorithms. I don’t expect you to be able to code a hashtable but I do expect you to know that there is such a thing and why you might use one.
Not understanding the basics of algorithms and data structures will result in serious performance issues. Imagine a list whose members are in sort order that currently contains 1000 items. A method exists to insert new items into the list. The bad programmer’s implementation will add items to the end of the list and then sort it. This is O(nlog2n)
or roughly 10000 comparisons. The competent programmer will do a linear search of the list looking for the insertion point. This is O(n/2)
or roughly 500 comparisons. The good programmer will use binary search to find the insertion point. This is O(log2n)
or roughly 10 comparisons. The worst implementation is three orders of magnitude slower than the best implementation. If there are 1,000,000 items in the list, the worst implementation is heading for disaster.
…Fail to understand idioms
Programming languages are the medium through which we express our intentions. Like human languages, computer languages have their idioms and idiosyncracies. The good programmer will master the idioms while the bad programmer will create anti-patterns.
The anti-patterns are typically irksome rather than disastrous. For example, if you don’t understand the const
idiom in C++, you’re likely to write functions like this:
void writeData(char *data) { // Do stuff }
Leaving aside the fact that
char *
is effectively a promise to modify the data, the API is a pain to use, requiring ugly const_cast
invocations:std::string data("..."); ... writeData(const_cast<char *>(data.c_str());
If we don’t own the code, we’re stuck with it. If we do own the code, we’re probably still stuck with it for two reasons:
- If I fix the signature for
writeData
so that the argument is declaredconst
, I need to fix all existing calls towriteData
as well - The rest of the code in the file containing
writeData
is likely to be terribad. If I touch it, I own it and I don’t want it!
…Use the wrong tools
Imagine you’re looking at a C++ header file and you see something like this:
typedef struct LIST { struct LIST *next; void* payload; } list, *plist;
If you don’t recognize that, because you weren’t around in the 1990s, that’s a linked list struct, probably the same one that the bad programmer lifted from Herb Schildt’s ‘Teach Yourself C’ back in 1995 and which he’s been using ever since. Whatever its origin, it’s a sure sign that the programmer is using the wrong tools.
std::list
would be an obvious replacement but looking at the code more closely might indicate that std::vector
or std::deque
would be better suited to the task at hand. One thing is certain, however: the right tool for the job is never going to be a hand-rolled linked list.
Windows developers are forced to try and do everything in their graphical apps by the sheer poverty of the operating environment. They can’t reuse tools to do what their application requires because there are none, or none worth using. Unix developers, on the other hand, have more tools to play with than some of them know how to actually use. Have you ever seen something like this?
#!/bin/bash pattern=... file=... ... grep $pattern $file | awk '{ print $2 }'
In case you don’t recognize the syntax, a tool called grep
is being used to pick out matching lines from a file and these lines are being piped to another tool called awk
to print the second field of each line. As any fule kno, awk
can search files for patterns just fine so grep
is redundant:
awk "/$pattern/ {print \$2}" $file
…Ignore conventions
Well behaved Unix (and Windows) programs conform to well-established conventions:
- Input is read (or at least can be read) from a special filehandle called
stdin
- Output is written (or at least can be written) to a special filehandle called
stdout
- Success is indicated by an exit status of 0
- Failure is indicated by an exit status other than 0
- Associated error text is written to a special filehandle called
stderr
- Program options are specified by command-line switches that look like
-i <FILENAME>
or--inputfile <FILENAME>
Following these conventions allows marvelous things to happen. The output of another program, such as grep
, can be used as the input to my program, the output of which can be passed to, say, a PDF writer that produces beautifully formatted reports. My program doesn’t need to know how to do pattern matching and it certainly doesn’t need to know how to write PDFs. Instead, it can just get on with doing whatever it is that it does.
A long-departed colleague wrote dozens of Python scripts and compiled C++ programs that had the following characteristics:
- The command line options were always two XML documents, one for actual options and one for “parameters”
- The output was always an XML document on
stdout
, even in the case of failures - The exit status was always 0
- The actual status was somewhere in the output XML, along with any error text
- Progress text was written to a magic filehandle with a
fileno
of 3.
Needless to say, he was a terrible programmer.
…Are too clever by half
I said above that bad programmers are not necessarily stupid. Some are really quite clever. Too damned clever if you ask me. Have you ever seen something like this?
void majorfail(char *s) { char buf[256], *p = buf; while (*p++ = *s++); // Do stuff with buf }
Bad programmers are insecure programmers as well by which I don’t mean that they’re secretly afraid that they’re not very good but rather that the software they write has security holes large enough to drive trucks through. That
while
loop may look 1337 hardcore but if I see something like it, I’m not praising the author’s mastery of the pointer but rather asking why he’s reinventing strcpy
. And this message goes out to all the bad programmers of the world: 256 characters (or 512 or 1024) is not big enough to hold any possible input. The fix is not only safe, but way simpler and more comprehensible:void minorfail(char *s) { std::string str(s); // Do stuff with str }
Sometimes, a piece of clever code relies on some really obscure knowledge to make sense of it. Look at the following JavaScript:
var hardcoreMathFn = function(x) { if (x === x) { // Do loads of really cool stuff } };
Because I’m a bit of a clever-clogs myself, I recognize that this is an NaN test (NaN is the only value which is not equal to itself). If you, the maintainer of this obscurantist nonsense, are not aware of that piece of arcane knowledge, then the code looks like this:
if (true) { // Do loads of really cool stuff }
The correct fix is:
if (!isNaN(x)) { // Do loads of really cool stuff }
but the more likely one is:
// Do loads of really cool stuff
This sort of uncommented esotericism led to one of the worst bugs ever and I personally hold the smartypants openssl devs responsible, not the well meaning Debian package maintainer.
…Reinvent wheels. Badly
Have you ever seen code that does its own command-line processing? Something like this:
int main(int argc, char **argv) { char *file = NULL, *host = NULL, ... for (int i = 1; i < argc; i += 2) { if (!strcmp(argv[i], "-file")) { file = argv[i + 1]; } else if (!strcmp(argv[i], "-host")) { host = argv[i + 1]; } ... } ... }
This is ugly to read, a ballache to maintain and accommodating an option that doesn’t take an additional parameter, such as
-verbose
will be irksome. Also a variant argument form, -file
rather than --file
is used. If you don’t know already, you’ll probably guess that DIY command-line processing is quite unnecessary as well since the problem has been solved. The bad programmer is simply too ignorant to know that this is the case or too lazy to figure out how to do it properly.
Bad programmers don’t just reinvent, they invent as well. A sure sign of bad software design is the invention of (oxymoron alert!) a proprietary network protocol. Imagine that we are implementing a client/server system that receives JSON-encoded requests (no, not XML because this is the 21st century) and emits JSON-encoded responses. An exchange might look like this:
{"request":"Yo!"} {"response":"Wassup?"}
Now imagine that our “protocol” is simply that each request and response terminates on a newline so that the actual network exchange is this (“\n” signifies an actual newline character rather than an escape sequence):
{"request":"Yo!"}\n{"response":"Wassup?"}\n
Why is this a problem? Because implementing the protocol requires us to work at an uncomfortably low level. We have to get down and dirty with send
, recv
and select
. We have to handle the possibility that the other end might fall over halfway through a message ourselves. If, however, we’d chosen HTTP rather than inventing our own protocol, we could use something like libcurl
on the client side, a library that is not only mature and robust but more importantly is maintained by someone else. The server side could be implemented as a simple CGI script.
…Are plain sloppy
Look at my code samples in this article. Even when I’m demonstrating manifestly bad practices, the code is neat and orderly. You may question my opening brace placement since I favour the one, true placement of Kernighan and Ritchie over the deviant form of the heretic Stroustrup but you can’t deny that my coding style is readable and consistent. Bad software isn’t neat. The bad programmer can’t decide whether or not a space should go after keywords like if
and while
(hint: yes it should). You can generate random numbers from how many spaces go after commas: 0, 1 or other (hint: the correct value is 1). Similar strong randomness seems to govern whether or not there is a space between a function name and its argument list (hint: there shouldn’t be but I’ll let it pass if you’re consistent). And tabs and spaces are intermixed almost whimsically.
You may retort that whitespace isn’t significant, so what’s the problem? I said above that programming is a craft as well as an art. It’s also a discipline, one that requires an orderly thought process and a methodical approach. Code spends most of its time being maintained, not being written. If you don’t take a few minutes to ensure that your indentation matches your nesting how is the person who comes after you supposed to follow your logic? If you can’t pay attention to details how are you going to avoid sneaking in defects? In my experience, the probability of untidy code also being good code is close to 0.