every once in a while the question about whether public variables are something that should be used in vfp. now, there are lots of opinions on this matter and there are several arguments that come up time and time again in defense of using public variables. here are a few:
"I use public variables to hold application wide data - global data - and have no problem with them"
the only possible answer to this one is "well good for you!" this is not an argument for using public variables (or anything else for that matter). this is the same as the clothes shop salesman i once ran into who, when i asked for a pair of plain black trousers, (i.e. with no cuffs or pleats) told me that "pleated front trousers are our most popular style" . maybe it was true but that wasn't a reason for me to buy them when i had specifically asked for trousers without pleats!
just because in your particular environment you have never run into problems with public variables, does not mean that there are none. all it says is that you don't understand the issues.
"I name all global/public wide variables so that they begin with a 'g' - and never define local variables with a name beginning with a 'g'"
this simply makes no sense and shows only that the person has no understanding of how vfp handles variables - because when you define your variables there is no issue anyway. vfp handles the scope appropriately all by itself, problems arise only when you do not explicitly define your variable scope.
thus, if you declare a variable as local with a name that happens to clash with a public variable, vfp only accesses the local variable (for as long as it remained in scope). if you re-declare an existing global variable as private, then vfp automatically hides any existing variable of the same name and again, only accesses the declared variable for as long as it remains in scope. you cannot re-define a private variable as local (you get an "illegal re-definition error) so that is no issue either. paste the following code into a prg and run it from the command line. you will see the results in figure 1:
clear *** re-define a public variable as private and assign a value public gcpath gcpath = "g:\vfp90" ? 'public: ' + gcpath private gcpath gcpath = "p:\vfp90" ? 'private: ' + gcpath list memory like g* *** re-define a public variable as local and assign a value clear memory public gcpath gcpath = "g:\vfp90" ? 'public: ' + gcpath local gcpath gcpath = "l:\vfp90" ? 'local: ' + gcpath list memory like g* *** re-define a private variable as local and assign a value clear memory private gcpath gcpath = "g:\vfp90" ? 'public: ' + gcpath local gcpath gcpath = "l:\vfp90" ? 'local: ' + gcpath list memory like g*
"public variables are part of the vfp language and so they should be used"
that is a completely specious argument and has no relevance whatsoever! after all @...say and @...get are also "part of the vfp language", and so is the accept, save and restore screen and create color set commands. do you use them too?
basically the concept of a public variable is a hang-over from the earliest versions of foxbase, which was designed to run in the dos environment where there was only ever a single screen and you navigated through the application by opening and closing screens. global variables were needed in that environment to persist values between screens that were totally independent and disconnected.
however, this is not the environment in which we work today. there is absolutely no reason to use inappropriate methodologies just because they exist. after all, just because you can do something, doesn't mean you should do it.
"their main advantage is that they are always available not matter what objects are released; intentionally or otherwise. especially when a system crashes. this makes them good for tracking user paths through software and reporting under an on error routine"
again, this is completely irrelevant. all variables that are in scope are always available in an error handling routine. whether they are public or not makes absolutely no difference. the only issue is whether your error handling is properly designed and constructed or not.
but enough of this 'defense of the indefensible' let's get down to the nitty gritty of why public variables are a bad idea in an object-oriented, form-based, event-driven application environment. there are essentially three issues:
- public variables break encapsulation - a fundamental principle of good object oriented programming is that an object should not depend on external entities for any its function. that means that if a value is required, that is not a part of the object's definition, it should be explicitly passed to the object when that object is invoked
- public variables can be overwritten - by definition a public variable is not only accessible at all levels of the application, but can also be changed at all levels of the application. in my little example code above, omitting the declaration line in either private or local case would have resulted in the value of the public variable being changed. since the variable scope is global, that change is also effective globally
- public variables have a single value - one of the commonest reasons for using public variables is for defining 'global' data. of course this is fine as long as there is only ever a need for one value of each global data item at a time. as soon as you need two instances of an object, but with different values for the public variable in each, you are trouble
let's look at each of these in more detail.
The OOP aspect
the crucial issue that must be addressed when considering the design of any class is "what are the responsibilities for objects based on this class?" once the responsibilities are identified, then it is easy to figure out what data the object needs to manage and what it needs access to, but does not control directly. anything in the latter category must be passed into the object when it is invoked and either acted upon immediately, or saved to an internal property for future use.
either way, the one thing that you should never see in any object code is something like this:
this.value = lnSomeValue * gnmyvar
obviously "lnSomeValue" is a local variable (and that is fine because it is an internal reference) but "gnmyvar" is presumably an external "global" value. since it is being used in a calculation we assume it is numeric but there is no reason why it should be! the following code is perfectly valid in vfp:
public gnmyvar gnmyvar = "fred"
now what will be the result of the code that ran the calculation? a data type mismatch error!
obviously it is very important that this value be numeric! so how would assigning it to a property help? well, one possibility is that a property (unlike a public variable) can be made strongly typed by using an assign method. so if, instead of accessing the variable directly the object code had been:
this.value = lnsomevalue * this.nvaluetouse
we could then create an assign method on the property like this:
lparameters tunewvalue *** must be numeric if vartype( nvl( tunewvalue, '' )) = "n" *** ok, assign it this.nvaluetouse = tunewvalue else messagebox( 'newvalue must be numeric', 16, 'data error') endif
now we need worry about it no more. whenever anything tries to assign a value to the property it will be checked and, if not numeric, will not be saved. now we may still get an error message if the source for this value is incorrect but the crucial difference is that the object now controls its data, and is not relying directly on the randomness of an external value.
this is what is meant by encapsulation and it is not "merely a theoretical" issue. it is actually the basis for creating robust, bug-free, reliable applications. when applied properly there is no longer any possibility of 'unexpected' errors – however they may be caused because each object controls its own destiny.
(note: from wikipedia:
"object-oriented programming has roots that can be traced to the 1960s. as hardware and software became increasingly complex, quality was often compromised. researchers studied ways to maintain software quality and developed object-oriented programming in part to address common problems by strongly emphasizing discrete, reusable units of programming logic. the methodology focuses on data rather than processes, with programs composed of self-sufficient modules (objects) each containing all the information needed to manipulate its own data structure
……
an object-oriented program may thus be viewed as a collection of cooperating objects, as opposed to the conventional model, in which a program is seen as a list of tasks (subroutines) to perform. in oop, each object is capable of receiving messages, processing data, and sending messages to other objects and can be viewed as an independent 'machine' with a distinct role or responsibility. the actions (or "operators") on these objects are closely associated with the object. )"
Public variables can be overwritten
As noted in the introduction, public variables are totally exposed. there is no control over them whatsoever and any value can be assigned to a public variable at any time. any change affects any object that accesses the variable irrespective of how, when or where. the following is an absolutely true story (only the names have been changed to protect the innocent):
Many years ago I was involved in debugging a vehicle fleet management application for a major company in europe. the application had been running for several years without error in production, but had suddenly started crashing occasionally for no apparent reason. there was no pattern – it would run for days, even weeks without error but then for no obvious reason would crash with a "data type mismatch" error. the crash always happened in a form, but not always the same form. oddly enough the one form which never seemed to crash was the "select vehicle" screen and that, after many days effort, provided the clue.
the problem was the fact that the application defined a public variable named "curveh" (i.e. the "current vehicle") that was used to store the id (an integer) of the currently selected vehicle. unfortunately, a newly hired developer, had added a new report to the application that defined a report variable named "curveh" which was used to store the license plate number (i.e. a character string!!!) of the vehicle currently being processed by the report (it was printed out in the group footer i seem to recall, but i wouldn't swear to it) .
of course, this overwrote the system level public variable's integer value with a character string and so, after running this report anything which accessed, without first setting, the "curveh" variable (which was almost everything in the system apart from the "select vehicle" screen) would immediately crash the app with a "data type mismatch" error.
as long as you didn't run the report, the app was fine and everything worked. run the report and the app crashed immediately! but detecting the correlation between the report running and the app crashing was tough to do since it was an "occasional" report to begin with.
this is the major issue with public variables, and it is one that is avoided totally by using an application object that has properties (with assign methods) for all system critical values. of course, you can never prevent a "valid but wrong" scenario (i.e. the vehicle id is an integer but is not the correct one for the vehicle you wanted) but at least you can prevent the data type of the property from becoming invalid.
the introduction of support for null, along with the left outer join syntax, to vfp added another nail to the coffin of public variables. in earlier versions of foxpro, there was no null support and so there was never any issue with data that evaluated to null. this is no longer the case and even if you don't explicitly set a value to an invalid type, you can still get errors, or worse, no error and bad results, when global variables are used to store values derived from sql queries that involve outer joins. in this case the issue is even more insidious because, in vfp, once a variable has been assigned a value it retains that type even if the actual value is null. thus:
x= 'fred' ? type("x") && = "c" x = null ? type("x") && = "c" !!!
so with your variable, even testing it's type will leave you with potential errors. notice that in the assign method above i used nvl() to check the value! again, the use of a property instead of a public variable avoids this issue too.
Public variables have a single value
This one is so obvious that it needs no real explanation. if you have an application where the same form can be opened multiple times – for example where several instances of the 'tombstone' form (i.e. a supplier or a customer's, name, address, phone, fax, email address) may be on screen at any time, public variables are useless for holding information for the form since the forms may well be using different tables, in different directories, will certainly have different "current" ids and so on and so on.
however, it's not just when you need multiple instances of the same form. the issue can also arise when an application requires that more than one form be open at the same time but draw their data from different locations, or simply have different "save to" locations, or any one of a dozen different items. in other words using public variables for global data is only practical in single-instance "sovereign" applications (where the current form fills the screen and there is never any other form available unless it is directly called from, and relates to, the main form). of course, this type of application is exactly how the dos world worked, and where public variables were intended to be used.
So what should we do?
the first thing to do is to ensure that all variables, irrespective of their scope, are always explicitly declared. not only is this generally a good practice, but vfp does have, through intellisense, the ability to display the list of parameters and currently declared local variables (in a code, or method, editing window type 'zloc' and you will get the list from which you can insert the required name into your code). for a full discussion of scope see my blog entry http://weblogs.foxite.com/andykramek/archive/2005/05/04/421.aspx
second, use either your application object, or create a special "globals" object, and give it properties for your globally required information (some people like to use _screen but, although you can add properties, you cannot add methods to _screen and it is useful to be able to use assign methods to handle data typing and validation). then in your application startup program create this object (using a private declaration) so that it is available throughout the application. when you need access to a value you have one place to go for it. more importantly by using your own object you can, as i indicated earlier, use access and assign methods to enforce strong typing and prevent invalid values from ever being saved.
third, whenever you find yourself thinking that you need a public variable, ask yourself the question: is this the only way to solve my problem? the answer will, almost certainly be "no"! i know of only one scenario where i routinely use a public variable. this is when i want to make program to test a class definition that is defined in a prg so that i can simply "do" the program from the command window. of course, all variables created in the command window are public anyway, so in this case it really doesn't matter. here is what i mean:
release otest public otest *** instantiate the class to test otest = createobject( 'myclass_to_test' ) define class myclass as custom *** class definition here
as you can see, this simply creates, and leaves behind an instance of the class that is defined. more importantly that is the whole point of a public variable! it has persistence in the environment beyond the scope of procedure or method in which it was created. when this is your requirement, then a public variable really is the only answer. however, how often is this really the requirement? in a runtime application environment i cannot even begin to conceive of a case where this would be necessary.
finally, the attached zip file contains the code for my variable manager class. this is a little class, defined on the session base class (so it runs in its own datasession) that uses a metadata table to define a set of variable names and values. it exposes three methods:
- listvars() that simply returns a string with the list of all variables and their current values
- setvar( varname, varvalue ) that sets a value on a defined property (and creates it if it doesn't already exist)
- getvar( varname ) that retrieves the current value of the specified variable
What this means, of course, is that i can now standardize my "global" variables by adding records to a metadata table instead of explicitly defining them in code (how often have you used different names for the same variable in different scenarios? i know i have, but not any more.) it also means that i have a standard methodology to define, retrieve and update variables that need to be available globally. so in my code, instead of:
this.cpath = gcdatapath
I can use:
this.cpath = ovarmgr.getvar( 'cdatapath' )
The extra few keystrokes are worth it to me because i know that any critical value that i get from my variable manager will at least be valid because the only way a value can get there is through an explicit call to the manager object – and that is not something that will ever happen by accident – either in code, or in a report, or anywhere else.
As always, please feel free to modify the code to suit yourself and if you come up with an improvement, please share it.
No comments:
Post a Comment