Visual
FoxPro Refactoring Redux
Andrew MacNeill, AKSEL,
2007
www.aksel.com
Visual FoxPro has a lot inside. There are hundreds of commands,
designers, builders, samples, and wizards -- all designed to make the
development process easier. But in this day of endless upgrades and scarce
resources, development is only half the battle. Maintenance is the other half.
When Microsoft announced the Sedna project, the community also
got involved with the public domain SednaX project While some developers get to
start on brand new projects, many existing applications are being upgraded to
the latest versions of FoxPro, providing the perfect opportunity to refactor,
in other words, rewrite, redesign, and rework code to improve it. In this article, I'll run through three easy concepts
that can help in refactoring your code, ensuring your code is in good shape.
The
tools approach
Before I get to the three concepts, it's important to be
familiar with the tools that make refactoring easier. Both the Fox team and
third-party developer community have jumped into the refactoring market,
providing tools to help identify the little issues that trip up developers.
When working with other developers' code, running Beautify can
instantly reformat the code to make it more readable. This may sound like an
extremely minor benefit, but prior to Beautify, the debate between "three
spaces" or "tabs" often prompted heated exchanges among
developers. The other key benefit of Beautify highlights a common problem:
using key words for variables or field names.
Concept
1: Never, ever, use keywords for variable or field names
This isn't just because it makes the code more readable. I
recently came across problems dealing with remote tables that were all caused
by this one little issue. A table had a "description" field that had
been named DESC. The application called for the field to be run in descending
order:
SELECT
* FROM Table ORDER BY DESC DESC
Visual FoxPro never had a problem with this. But when the table
was converted into a remote database, neither SELECTs or UPDATEs
would run properly -- all because
of the field name. Now the Beautify option makes it easier by letting you
capitalize all FoxPro keywords which should make it easier to identify those infractions.
But it doesn't let you do any analysis of the usage of those keywords. Using
some of the same basic concepts found in Beautify, however, you can easily
identify where you're using keywords, variables and analyze them.
The table FDKEYWRD is located in the FoxPro Wizards folder and
contains all the keywords
FoxPro uses. Table 1 shows the structure of the table. Even
four-letter keywords are listed in the table separately, making it easier to
search the table.
Field Name |
Description |
Token |
Keyword name |
Code |
Type of keyword: P – Property M – Method C – Command O – Object I – Start of control loops U – End of control loops F – Function/Procedure Start D – End of definitions |
Table 1: What's a keyword? -- The second CODE column may often
be empty, but it's useful for identifying the most common uses for specific
types of code words.
Use VFP's GETWORDCOUNT and GETWORDNUM functions to test your own
code for keywords:
LPARAMETERS tcCode
lnLines = ALINES(laContent,tcCode)
FOR lnLine = 1 TO lnLines
lcLine =
laContent(lnLine)
=checkLine(lcLine)
ENDFOR
PROCEDURE checkLine(tcLine)
lnWords = GETWORDCOUNT(tcLine)
FOR lni = 1 TO lnWords
lcWord = GETWORDNUM(tcLine,lni)
lcWord = STRTRAN(lcWord,CHR(13))
IF NOT SEEK(UPPER(lcWord),"FDKEYWRD",1)
INSERT INTO noFind VALUES (lcPrg,lnLine,lni,lcWord)
ENDIF
ENDFOR
The above code shows some areas you should be aware of when
using GETWORDCOUNT. A string with parentheses and comments is considered a
single word. The following expression appears as three words and not the
expected 6 or 7:
IF NOT SEEK(UPPER(lcWord3),"FDKEYWRD",1)
Call GETWORDCOUNT again but this time, with a delimiter
parameter:
IF "("$lcWord
lnCore = GETWORDCOUNT(lcWord,"(")
FOR lnj = 1 TO lnCore
lcWord2 = GETWORDNUM(lcWord,lnj,"(")
IF RIGHT(lcWord2,1)=CHR(13)
lcWord2 = LEFT(lcWord2,LEN(lcWord)-1)
ENDIF
IF RIGHT(lcWord2,1)=")"
lcWord2 = LEFT(lcWord2,LEN(lcWord)-1)
ENDIF
lcWord2 = STRTRAN(lcWord2,CHR(13))
IF NOT SEEK(UPPER(lcWord2),"FDKEYWRD",1)
INSERT INTO noFind VALUES (lcPrg,lnLine,lni,lcWord2)
ENDIF
ENDFOR
ENDIF
The final code creates an output cursor called NOFIND you can
use to review your variables or other non-defined keywords. No, it isn't
perfect, but it does let you do some fairly useful things to analyze your code.
Now you can check out your naming convention, or lack thereof,
as well as ensure that your variables are being properly defined and used. The
final code also tracks the use of used keywords (through the use of a cursor
named KEYFIND). This can be useful if you are seeing if there are areas where
you may want to improve your code, which leads us to a second core concept.
Concept
2: Use the most recent functions, where appropriate
Developers are notorious for falling into bad habits. Get
comfortable with one approach and it takes a huge effort to ever try something
new. The funny thing is that, while we may often complain about the efforts an
approach takes, when a new solution appears, it takes time to start using it.
Oftentimes, existing habits are assisted by tools that make them even harder to
break. Many developers had their own code-reference-like
tools they've coded from scratch. When VFP 8 introduced the code reference
tool, to make it easier to search and replace within projects, many developers
weren't excited because they already had their own tools that, in some cases,
did much more.
The code in the first example uses functions like GETWORDCOUNT() and GETWORDNUM(). However, the FOXTOOLS
library has had similar functions in it for years. As a result, developers who
are used to using the WORDS() and WORDNUM() functions
from FOXTOOLS might never think about using the newer functions, which are, by
and large, faster and have more options.
Another good example of this is TEXTMERGE. I had the opportunity
to look at some older code recently written for VFP 5. Back then, to export
large amounts of text to a string, a developer might use:
SET
TEXTMERGE TO myText.txt NOSHOW
SET
TEXTMERGE ON
\This
is my text
\Going
to a variable
\Here
is today's date: <<DATE>>
SET
TEXTMERGE OFF
SET
TEXTMERGE ON
Today,
you can do this with far less code:
TEXT
TO myText TEXTMERGE NOSHOW
This
is my text
Going
to a variable
Here
is today's date: <<DATE>>
ENDTEXT
Or even:
myText = TEXTMERGE("This is my text"+CHR(13)+"Going to
a variable"+;
"Here is today's Date: <<DATE()>>")
The above might not seem like much of a
trade-off but when the actual merged text is in the hundreds of lines, the
ability to break it into smaller pieces with built-in functions can reduce the
number of lines of code and possibly the amount of debugging required.
Consider that a single IIF statement reduces
code by two-thirds and that a CASE statement defining a variable can be reduced
to one or two lines using ICASE. This is where readability comes in.
IF EMPTY(tcValue)
tcValue =
"Default"
ENDIF
DO
CASE
CASE tcCountry = "USA"
lcCaption =
"State"
CASE
tcCountry= "Canada"
lcCaption =
"Province"
OTHERWISE
lccaption = "Region"
ENDCASE
Or
tcValue = IIF(EMPTY(tcValue),"Default" , tcValue)
lcCaption = ICASE(tcCountry="USA","State",;
tcCountry='Canada',;
"Province","Region")
Many programmers prefer the clarity of the three-line IF/ENDIF
statement but, in large chunks of code, reducing variable definitions to a
single line can make code easier to move through. Reducing the size of code is
also covered in the next concept.
Concept
3: Encapsulate, encapsulate, encapsulate
A few months ago, I did an informal survey about the ideal length
for programs. This survey was inspired by some studies on maintainability of
code, where more lines of code typically equate to more bugs and thus more
debugging. Few programmers debate the benefits of black-box programming or
encapsulation, where a complex operation is put into a single procedure. A
well-named and debugged procedure or method makes code more readable and
reduces the amount of effort required to debug the calling methods. Yet is there an ideal size of function or at least some
consensus among developers.
The results of my survey surprised me. The majority of
respondents thought the ideal size of a single procedure or function is about
30 lines. A total of 70 percent thought it should almost certainly be less than
100 lines. What can you write in under 100 lines? Certainly, if you use IIF and
ICASE a lot, you could probably fit an awful lot of functionality into 100
lines -- but there's more to think about than just efficient code.
Compare debugging a 300-line program to a 10- to 30-line program.
For one thing, if you have multiple developers in your company, this is a great
opportunity for code reviews. In addition, forcing yourself to program to a set
number of lines discourages scope creep. Each function will do only what it
needs to do. And setting a "line-limit" might help identify
conditions you didn't previously think of -- conditions you can then either
choose to ignore or prepare stubs for new programs or functions.
The refactoring factor
Refactoring is a subjective area. What is more maintainable to
one developer may be hopelessly complex or redundant to another -- yet it's
something that we should always be on the look out for. When you're in the
middle of a project, it may not seem like refactoring is something you can
start doing, but by identifying older functions you can improve or simply
moving code to make it easier to debug later on, your development project will
become more manageable. You can get started just by following concepts 1,2 and 3.