Good Programming Practices: What to Do (or Not)
Good Programming Practices: What to Do (Or Not!)
Overview
Most programmers suck. No, I'm not trolling or being glib. I'm simply
stating what I've concluded after updating countless other people's code
over the years. In fact, statistically speaking, chances are your
code sucks. Don't believe me? Here is a list of good programming habits
that most people would agree are Good Things in principle, yet precious few
adhere to in practice:
-
Be consistent with formatting.
I don't care whether you use spaces or tabs for indentation (okay, I do
care, but that's a subject for a different article), as long as you
stick with one or the other. There's nothing worse than code that is
difficult to read because it is poorly formatted.
-
Be consistent with naming conventions.
Again, consistency is more important than the specific style you use.
Do you name your variables $bean_counter, $beanCounter or $BeanCounter?
I really don't care; just pick a style and be consistent. (Of course,
only a n00b would use the same style for every type of variable; let's
hope you're smart enough to use one style for local variables, another
for class names, etc.)
-
Use globals sparingly.
Global variables suck. 'Nuf said.
-
Consolidate literals.
Literals belong in a centralized spot (e.g. a separate module or a
database), not sprinkled liberally throughout your code. That means you
should refer to them symbolically (i.e. by name, not literal value).
-
Don't assume output formats.
Sometimes data will get output to the screen, other times to a printer,
and other times it will get logged to a database. So why are your
low-level functions returning their output in HTML format (or, worse,
printing them)? They should be returning values that the caller can
have its way with; it's the caller's job to decide whether
data will be returned as-is, reformatted, sent to stdout, or whatever.
-
Comment.
Add comments to your code in plain English (or your native language if
the other programmers who will be reading it also speak the same
language) that describe both what the code is doing and why you decided to do it one way and not another way that
somebody reading the code might think of on their own. Oh, and don't
get all cutesy with your comments; other programmers who read your code
just want to fix your stupid bugs; your pithy comments only serve to
distract and confuse them.
There's nothing worse than comments complaining about something unrelated
to the code, especially when the code is poorly written to begin with. Make
sure your house is clean before complaining about other people's houses.
If you want to go all the way and do the right thing, you'll actually
document your code as well. That means writing a separate document (not
comments in the code) that goes into more details on the operation and
function of your code.
-
Wrap.
Wrap all calls to built-in functions and third-party library functions
with your own wrapper functions. This is an important habit and serves
several purposes:
-
If the built-in functions change (e.g. you change languages or compilers)
or if the third-party libraries change, you'll have to change only your
wrapper functions, not your main application.
-
You can always add debugging code or breakpoints to your wrappers to trap
when they're called. Don't think that's necessary? Try trapping all the
calls to, say, the sqrt() function, counting how many times sqrt() is
called during a typical run, or writing a message to a log every time
sqrt() is called. You'll find those tasks to be far easier if you've
replaced all calls to sqrt() with my_sqrt(). And before you go around
making macros that simply replace sqrt() with my_sqrt(), remember that such
things only serve to obfuscate code; it isn't always clear to the viewer
that sqrt() really means my_sqrt(). If you want my_sqrt(), your should say
my_sqrt().
-
Taint check.
Ensure malformed user input can't corrupt your application or data,
whether by accident (typo) or design (malicious user). That means
checking for everything from SQL injection to buffer overruns to null
or malformed values to, well, anything else that falls out of the
bounds of acceptability.
-
Check return values for error conditions.
Just because you tried to open a file (or write to it or close it)
doesn't mean you were successful. Everywhere you call a function that
can throw an error, you should add code to deal with that potential
error.
-
Recover (or fail) gracefully.
Abending is so 1970s. Robust programs should report an error message
and attempt to continue. Failing that, they should halt gracefully.
Even failing gracefully means never doing something like this:
connect_to_database(...) or die("Unable to connect to DB!");
This type of error handling is:
-
Lazy
: you obviously copied and pasted it from a book or web site that
explains how to connect to a database. You're a n00b.
-
Ungraceful
: failure to connect to the database is not a sufficient reason to exit
the entire application right then and there. Don't be low-class.
-
Inconsiderate
: what if another module needs to do some clean-up before the
application exits? Don't be selfish.
-
Unhelpful
: of what real use is the message "Unable to connect to DB"? A better
message might be "3-Sept-2006 12:34:33 pm: connect_to_database() failed
on line "123" of module "foo" – at least if you're talking to a
programmer. To the end user, even that "improved" message is mindless
doublespeak. This says you're the type of old-school programmer geek
they keep away from the customers. That is so 1980s.
-
Provide useful error messages.
Expanding on the previous point, you should provide a user-friendly
error message while simultaneously logging a programmer-friendly
message with enough information that they can investigate the cause of
the error.
-
Separate business logic from other program logic.
Need I say more?
-
Separate code from data.
OO is only the tip of the iceberg here. I'm talking about things like
separating HTML from PHP, Perl or C code.
-
Operate on objects, not data structures.
Unless you're working in C or another language that lacks objects,
there's very little reason to pass around complex data structures,
which only results in brittle code. Instead, pass objects with methods
that encapsulate functionality and hide data formats.
-
Internal data structures should be in native format.
That means when you're doing date arithmetic you shouldn't be parsing
through strings in the form "Wednesday, September 27, 2006". Such
formats are for people, not computers. Parsing from such high-level
representations should be done only once; after all calculations are
performed by the low-level code you can convert back to a more
"verbose" format.
-
Push interface up and implementation down.
Years ago there was an excellent article in the C Users Journal that
had about a dozen good programming practices and this was one of them.
(In fact, I wrote the article you're reading because I was never able
to find that article from the CUJ again and I thought it was one of the
best articles I ever read.) In a nutshell, "interface" is what the user
sees and "implementation" is the code that, well, implements it. "Up"
means high-level code and "down" means low-level code. So in other
words, code that deals with what the user sees and does should be at a
high level (hierarchically speaking) while code that deals with how
data should be physically stored and processed should be at a low level
(hierarchically speaking). A simple hierarchy might look like this:
-
Level 1 (high): accept user input
-
Level 2: taint check and normalize user input, and check for errors
-
Level 3: process user input according to business logic
-
Level 4 (low): store data
-
Know what you don't know.
Without getting all Donald Rumsfeld, suffice it to say the key to being
a good programmer is to know what you don't know. That way you can at
least organize your code in advance to minimize refactoring when it
inevitably comes time to make those changes. For example, if you know
that the passwords you're generating aren't very secure, you'd do well
to:
-
Put a corresponding comment in the code
-
Suggest an alternate method for generating more secure passwords (even
if it's just a theory)
-
Organize your code so that when the new password-generating scheme is
put into place it will fit as seamlessly as possible with your existing
code.
Conclusion
No programmer can ever implement all of these suggestions 100% of the time.
Not only does one reach a point of diminishing returns, but also there will
always be areas that are open to interpretation, opinion, and individual
style.
But just because perfection isn't achievable doesn't mean you shouldn't
strive for it. In fact, the most important thing you can do is to identify
the area(s) listed above where your program is most lacking and take steps
to fix it. For example, your program might have excellent database handling
routines but your variable names may be completely unintuitive.
Half the battle is figuring out (and admitting) where your program needs
improvement; the other half of the battle is doing something about it.
===END===
Return to Kim Moser's Generic Home Page.
Copyright © 2024 by Kim Moser (email)
|
Last modified: Tue 10 May 2022 16:08:05
|