One of the questions that i see over and over again on the various technical support forums that i frequent is “what is the best way to pass multiple values from one object to another”. let’s begin with the simplest case, where we want to call a procedure (or method) that returns a single value. obviously can just design the procedure so that it returns the desired result and, inside a form (or other object) method, we call it as a function and capture the returned value to a local variable, like this:
*** call the external program/procedure and catch the return lucalcvalue = myprocedure() this.control.value = lucalcvalue
Use a reference
This is fine when all that we want is a single value, but of course, there are many scenarios in which we need to return multiple values from the procedure. probably the first thing that leaps to mind is to pass a reference to the calling object to the child procedure so that it can access the calling object’s properties and methods directly like this:
luCalcValues = myprocedure( thisform ) or lucalcvalues = myprocedure( this )
While this looks attractive in many ways, it does break encapsulation and is, therefore, not “good oop practice”. but if “myprocedure” needs to call a method on a form then this is really the only way to do it. (i have seen it suggested that you explicitly name the instance of the form and then have the procedure look for an object with that name – but this only works if you are using the do form command, and it only works as long as there can only ever be one instance of the form. it is not a good solution!)
However, having an external procedure or object rely on being able to call a method on another object is bad design because the procedure is therefore inextricably coupled to the calling object. it is a fundamental rule of oop that no object should ever depend upon the internal implementation of another – because such objects cannot be used unless a another object (with the appropriate method) is also involved. if you find yourself in this scenario, then you probably should be thinking about re-designing things anyway.
For example a better approach would be to have the procedure return a flag indicating that the calling object should initiate some appropriate action. that way the decision on what to do in a given situation rests with the calling object, not the child procedure – clearly a much more flexible way of doing things.
Use public variables
If we aren’t going to let the procedure manipulate the object, then what about using public variables (or declaring private variables in the calling method so that they are in scope for the procedure) and having the procedure set their values. but this is really just a non-oop variation on passing a reference and it is poor design for all of the same reasons (now the procedure is dependant upon the existence of appropriately named variables which have to be in scope). worse, if we use public variables there is always the risk of “collisions” where the same variable is being changed from more than one place so that the value referred to is not necessarily the one that we were expecting.
Use a parameter object
So if we can’t use these methods, what can we do. fortunately vfp allows us to create and use parameter objects and these really are the best way to go. so the first thing to decide is what should our parameter object be.
If you are using VFP version 8.0 or later, you can use the “empty” base class (which is also the class used by scatter name) which can be instantiated and released very quickly because it has no native properties, events or methods. to add properties you have to use the addproperty() function.
If you are using an earlier version of vfp then you can use any lightweight base class, providing that it has an addproperty() method, either ‘relation’, which can only be defined in code, or ‘line’, if you want to define it in a visual class library, will do nicely.
Incidentally this is one of those cases where we really can use a base class directly (other commonly used base classes include session and collection) – although if you use the same set of values frequently in an application it may be worth creating a standard sub class for the object to avoid the necessity of repeating the code and to handle the maintenance of the properties it uses. so, how do we do this?
Version 8.0 or later
*** create an instance of the empty base class loparams = createobject( 'empty' ) *** add and initialize a string property llok = addproperty( loparams, 'cname', 'andy kramek' ) *** add and initialize an array property llok = llok and addproperty( loparams, 'alist[2,2]', '' ) if llok ** populate the array with loparams .alist[1,1] = 'akron' .alist[1,2] = 'ohio' .alist[2,1] = 'phoenix' .alist[2,2] = 'arizona' endwith endif
Version 7.0 or earlier
*** create an instance of a relation loparams = createobject( 'relation' ) *** add and initialize a string property llok = loparams.addproperty( 'cname', 'andy kramek' ) *** add and initialize an array property llok = llok and loparams.addproperty( 'alist[2,2]', '' ) if llok ** populate the array with loparams .alist[1,1] = 'akron' .alist[1,2] = 'ohio' .alist[2,1] = 'phoenix' .alist[2,2] = 'arizona' endwith endif
Having got our parameter object, and populated it we can simply pass it around as if it were simply a single value. so now in our procedure, instead of returning a single value, we simply create and return the parameter object:
return loparams
How do we know what we got back?
Now you may be wondering how we know what we got back in our parameter object. well that is easy too! there are two methods, we can either use the amembers() function to get a list of all the properties on the parameter object. this is fine when using the empty() base class but can get messy if you are using relation or line. here is an example of this method:
*** call the procedure that returns the parameter object loresults = myprocedure() *** use amembers() to get a list of property names lnprops = amembers( laprops, loresults, 0 ) *** get the name and the value for lncnt = 1 to lnprops *** get the name of the property lcname = laprops[lncnt] *** and it’s value luval = eval( “loresults.” + lcname ) *** use it however necessary… ? lcname, luval next
Alternatively we can use pemstatus() to determine if the appropriate property exists on the parameter object (this is easier when you are looking for specific, known values only):
*** call the procedure that returns the parameter object loresults = myprocedure() *** check for the property and assign a default value if not found lcname = iif(pemstatus( loresults, 'cname', 5 ), loresults.cname, "" ) *** use the result however necessary… ? lcname
Remember this works both ways…
I have been talking about using a parameter object to get multiple return values, but of course there is no reason why we should not use a parameter object to pass multiple values as well. there advantages to doing this because it gives us explicitly named parameters. in other words instead of relying on the position of a value in the list of parameters we can simply name. as a result we no longer need to count the parameters either – think of how many times you have used code like this:
if pcount() = 3 *** passed lastname, firstname and initial else if pcount() = 2 *** assume lastname and firstname else if pcount() = 1 *** assume lastname only else *** nothing passed endif && pcount() = 1 endif && pcount() = 2 endif && pcount() = 3
That now get replaced by:
with loparameters lcfirstname = iif(pemstatus( loparameters, 'firstname', 5 ), loparameters.firstname, "" ) lclastname = iif(pemstatus( loparameters, 'lastname', 5 ), loparameters.lastname, "" ) lcinitials = iif(pemstatus( loparameters, 'initials', 5 ), loparameters.initials, "" ) endwith
This is so much easier to work with and to maintain! if for no other reason than to make your own life simpler i hope you will adopt parameter objects as enthusiastically as i have.
No comments:
Post a Comment