 |
|
|
|
 |
The art of defensive programming
|
Or how to write code that will be easy to maintain
Article contributed by Jonathan West
The source code that you write has two quite separate purposes.
1. |
It is a set of instructions to the computer, telling it to
perform a particular task. |
2. |
It is a description of the task and how you have gone about
executing the task, for yourself and/or other programmers. |
This article is mainly about the second purpose, which a surprising number
of people don't really think about.
If you can't understand a program, then you can't debug it. Even with code
that you have written yourself, if you come back to it six months or a year
later, you may find yourself wondering Why on earth did I write that?
What was it for? It doesn't take long to forget the details of a program
when you aren't working on it any more. Make life easier for yourself, and
write programs as clearly as possible. Also, provide such defences as you can
against the possibility that VBA might change between versions of Word.
The following example is based on a real question that came up in the
newsgroups in 1999. The questioner wanted a macro that would do the following
for a particular set of styles named AGR1 to
AGR5:-
1. |
Check to see if the style already exists in the document. |
2. |
If it does, then do nothing. |
3. |
If it doesn't, then copy it from a central template. |
This is a fairly common kind of problem that Word VBA macros are excellent
at solving. The following code was suggested in response to the question.
Dim StyleArray As Variant
Dim StyleExists As Boolean
Sub Numbering()
With ActiveDocument
.UpdateStylesOnOpen = False
End With
StyleArray = Array("AGR1", "AGR2", "AGR3",
"AGR4", "AGR5")
StyleExists = False
For x = 0 To 4
For Each oStyle In ActiveDocument.Styles
If oStyle = StyleArray(x) Then
StyleExists = True
End If
Next oStyle
If StyleExists = False Then
Application.OrganizerCopy _
Source:="C:\Program Files\Microsoft Office\" & _
"Templates\Final Numbering TDS.dot", _
Destination:=ActiveDocument, Name:=StyleArray(x), _
Object:=wdOrganizerObjectStyles
End If
StyleExists = False
Next x
End Sub
Now, this code does the job, but there are lots of ways in which it could be
made easier to read and understand, and less prone to future problems.
Remember, source code isn't just there for the computer to execute. It is a
message to yourself (or to the programmer who someday may have to come after
you and maintain your code) as to what you were trying to achieve. If you make
the code easy to read, then that helps you. Anyway, here is a set of
improvements that could be made to the code.
1. |
There is no need for the Dim statements to be module-level
declarations. They aren't needed in any other routine. Therefore they should
come after the Sub Numbering() statement, so that there is no confusion with
any other routine which might happen to use the same variable names. People
tend to re-use favored and meaningful names, so this problem can be
significant. |
2. |
Numbering isn't a very meaningful name for the
routine. It's always a good idea give a routine a name that clearly describes
its purpose. Perhaps ImportAGRStyles would be better. |
3. |
You should have the Require Variable Declaration
checkbox set in the VBA Options dialog, so that there is an Option Explicit
statement at the start of every module. That way, there is much less chance
that VBA will interpret a typographical error in one of your variable
assignments as the creation of an entirely new variable of type Variant. This
being so, you would need Dim statements for x and
oStyle, as Long &
Style
respectively. See also Why variables should be declared properly. |
4. |
WithEnd With statements are a good idea, but there isn't
really any need to use them if you are only putting one item in between. If
you have two or more lines between them, then yes, great idea, use them
whenever you can. |
5. |
When updating the code at some point in the future, you
might want to add another one or two items to StyleArray. It would be quite
likely that you would forget (at least initially) to change the limit value of
x, so that it reflects the new size of the array. It is much easier to let VBA
to remember for you, by using UBound(StyleArray). That will adjust
automatically to the number of items you include using the Array function. |
6. |
A speedup trick. In the For Each oStyle
loop, once you find
a case where you set StyleExists = True, you don't need to go round the
loop any more times. Therefore you can add an Exit For statement to drop you
out of the loop. |
7. |
Using If StyleExists = False Then is not very efficient.
Better would be If Not StyleExists Then. In fact, for readability, better
still would be to rename the variable StyleIsMissing and swap the True/False
values round, so the line can now read If StyleIsMissing Then |
8. |
You can reset the value of StyleExists
(or StyleIsMissing,
as we would be renaming it) just once at the start of the loop, rather than
once outside and once at the end of the loop. Fewer lines of code means fewer
places that bugs can creep in! |
9. |
I don't trust default properties of objects. It would be too
easy for MS to change them between versions of Word and break something in my
code. Therefore I would prefer to use oStyle.NameLocal rather than just
oStyle
when comparing its value with StyleArray(x). We haven't been through enough
versions of Word with VBA to find out whether this may be a common problem
with MS, though a few things did change between Word 97 and Word 2000. I would
simply prefer to minimize the risk, especially as I think it improves
readability. Similarly, in the OrganizerCopy command, it would be better to
use ActiveDocument.FullName instead of just ActiveDocument. |
10. |
Indenting the code so that it follows the structure of the
IfThen statements and ForNext loops makes it much easier to understand where
the loops and branches are. Also, it is a good idea to indent continuations of
lines (perhaps by double the normal amount), so that it is quite clear that
they are not new instructions. Also, it is common to indent everything except
for comments and the lines which begin and end a routine. |
11. |
When a few lines of code together are needed for a specific
task, keep them together, and then put a blank line below to show that you are
starting on a new task. That way, the eye more clearly sees what your design
is. |
That's quite a few comments, they are longer than the code I've been
discussing! However, with every line of code that you publish (by which I mean
that will ever be seen by anyone other than yourself), you should be able to
justify it as being about the best way it could be written. I have learned the
need for this kind of coding discipline the hard way, by having to fix
extremely nasty and obscure bugs. Also, I have learned that for longer running
macros (though this particular macro should be over quickly) every speedup
trick in the book is worth applying. The code with these changes would now
look like this.
Option Explicit
Sub ImportAGRStyles()
Dim StyleArray As Variant
Dim StyleIsMissing As Boolean
Dim x As Long
Dim oStyle As Style
ActiveDocument.UpdateStylesOnOpen = False
StyleArray = Array("AGR1", "AGR2",
"AGR3", "AGR4", "AGR5")
For x = 0 To UBound(StyleArray)
StyleIsMissing = True
For Each oStyle
In
ActiveDocument.Styles
If
oStyle.NameLocal = StyleArray(x) Then
StyleIsMissing = False
Exit For
End If
Next oStyle
If StyleIsMissing
Then
Application.OrganizerCopy _
Source:="C:\Program Files\Microsoft Office\" & _
"Templates\Final Numbering TDS.dot", _
Destination:=ActiveDocument.FullName, _
Name:=StyleArray(x), _
Object:=wdOrganizerObjectStyles
End If
Next x
End Sub
Note that both versions of the routine do exactly the same thing. Maybe the
second is marginally faster, but not to an extent that really matters. However,
the second is much safer because the layout is clearer, the variables are
local, and default properties are not used. It is no harder to write like this,
once you get into the habit. For a short macro like this, perhaps it doesn't
matter so much, but once you start writing macros, you will probably think of
more complex tasks that you can automate with them. Then the defensive approach can make a big difference to the length of time
it takes to get things right.
There's one other thing to notice. I didn't put any comments in. For a
routine that's as simple as this, if the code is written well, there shouldn't
be any need for comments. At most, you might want a line or two at the start to
describe the purpose of the routine. (You might also have other standard comments describing the author and revision level of the
routine, but that's another issue altogether.) For a longer routine or a
particularly obscure bit of code, it's probably a good idea to include a few
extra comments at strategic points. These comments should aim to explain what
you are trying to do, rather than how you are doing it. Once you know the what,
the how should be clear from the code itself, and so doesn't need duplication.
Recommended further reading.
If you want a much fuller treatment of this subject, I would very strongly
recommend that you buy a copy of Code Complete, by Steve McConnell,
published 1993 by Microsoft Press, ISBN 1-55615-484-4. The book was written
before Word VBA existed, and even before Visual Basic existed, and so its
examples are mainly in C, Pascal and some in BASIC. Despite this, it contains
the best set of guidelines I have come across for writing good code in any
language. It covers most of the points I have made in this article,
in much greater depth than I possibly could here. The book is beautifully clear
(just like code should be!), and you will have no difficulty following the
examples, whichever language they are written in.
|





|