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.