This article was written by Gregory L. Reichert and uploaded to the CompuServe Fox Forum on the 19th January 1995. The text remains unaltered although I have taken the liberty of tidying up the code samples. The casual Web surfer should be warned, there's some pretty complex stuff here.
Malc.
Object Orientation Programming in FoxPro? Yes. The OOPs technique is not an alternate form of programming, but am extension of Structure Programming. Much the same way that structure programming inherits commands and features of unstructure languages like BASIC and Assembler, OOPs should inherit structure programming languages features. The general notion is to enhance the programming style, and still maintain backward compatibility. In the case of implementing OOPs into FoxPro, this should be a rule. The technique I about to describe is simple and requires no external modules. The implementation uses straight forward fox features, and allows greater flexibility then found in many of the other object orientated languages.
Recently there has been alot of talk about Object Orientation, but until Nantucket announced Clipper 5.0, there has been no evidence of it finding its way to the xBase dialects.
But before we jump into how to construct and using object, let's determine what a object is. The world is full of objects. Everywhere we look, we find objects. These objects are described by their characteristics and behaviors. The same definition applies to software objects. For example, take a book of matches, it can be described to a computer as something of X width, Y height, containing Z items, and etc. This does little to help define what the matches really are, it simple describes the characteristics, but does nothing to describe its usage or behavior. A set of separate routines need to be designed to implement the matches usage. Currently, FoxPro offers little means of binding the routines with the associated data, outside of the routines dependency on the data.
This is where objects come into play. They offer a method of Encapsulating, or binding the routines with the associated data. Within a software object are defined the characteristics as a set of data variables. The matches behaviors are defined as Methods, or variables containing the names of routines that manage the data. Like actors on a stage, we simply request from the object to perform one of its services, and the object acts out the predefined operation using the associated data. In respect to the Matches concept, we could have defined some methods like: REMOVE, LIGHT, BURN, PUTOUT to describe to basic activates. These methods would manipulate the data variable REMAIN, the remaining number of matches.
After some common-sense investigating, we discover that the feature of Encapsulation has been a concern of programmers all along. In most applications, the code and data are viewed as related entities; where the code goes, the data follows, and vise-versa. In this case, the needed binding agent is missing. Leaving the responsibility of keeping them together up to the programmer's memory, not the computer's.
Objects definitions resolve this dilemma. As mentioned, they offer a common, but unique, binding agent to maintain the relationship between the code and the data it effects. Objects and their binding agent can be thought of a containers that group data with its related code. Normally, we refer to these containers as Classes. A Class is a logical definition, while an Object is physical, a working representation of the Class. In principle, there is one, and only one, Class definition, but many Objects can be generated from that Class. From this point on, the term Class will refer to the structure of the object, and Object to the run-time representation the class.
The attributes of a class are bound together by the common agent, called Self. It serves two purposes, first being the binding agent, a unique entity shared by all the attributes of any single class. Second, it acts as the objects identifier, or the Instance of a Class. We address an object's attributes with use of Self. Every class attribute is declared, accessed, and released with respect to the self reference.
At this point, we have devised a method that enhances reusability, or the ability to port various portions of one application to another. Object orientation doesn't stop there, but includes many other enhancements. Object classes can expanded to incorporate new features without modifying the existing code or data. This concept is called Inheritance. For instance, take our Matches example, matches serve their purpose well, but only to a limited degree. Only twenty attempt can be offered, and a strict environment control need to be observed. We can improve on our situation by purchasing a disposable lighter. The lighter perform all the function of the matches, plus insures greater number of lights, and better durability. In a sense, the lighter inherits all that attributes of the matches, improving on it's weak areas, while maintaining it's strong points and purpose. This is important in designing software object classes.
The matches still exist, but now we have a lighter. In computers, this translates to an easier and natural method of Version Control. As the application evolves through it's versions, the previous versions remain intact deep inside the current. By instigating the object that defines a previous version, the application performs that version as if the enhancements where never included.
The previous version of a class is the parent to the new child class. In fact, a parent class can spawn many variations of child classes. This multiple offspringing is refered to as Polymorphism, meaning Many-Shapes.
Polymorphism can be observed in the Matches/Lighter example, if we first suggest that disposable lighter is a child to a class of flint/steal igniters. Flint/steel class offers the basic concept of any lighter, without defining any one type of lighter. Not only can the disposable lighter be defined from this base class, but we could also derive a ZIPPO-type of lighter and even electical igniter lighters.
In the computer aspect, this can be illustrated by means of the output operations. We start by defining a class that provides a basic text-style of output to the screen. Through an object generated from this class we are able to display information. But display is not the only function in placing the data on screen, the color of the displayed data needs to be considered. A child class is defined to include this new features. Now if the monitor is detected to be monochrome, the color aspect is not need, so the application generate the parent class. But if a the monitor supports color, then the child class needs to be generated.
We can take this example even further, by defining further child classes that describe different types of video displays, and whether it's being display on a text or graphic screen, or even a class for printer and file redirection. To the application, it's just a output device, it is up to the object to determine how to get the information to the device.
Inheritance and Polymorphism are probably the two greatest assets that OOPS can offer to application development. With a natural version control aspect, and the ability to expand the applications functions and the environments domain, Object Orientation is defiantly a evolutionary advancement to software engineering.
So far, I have not demonstrated any FoxPro code illustrating OOP. I have attempted to illustrate that Object Orientation is not a solution to many problems plagueing the language, but is simply an alternate way of view and defining the problem. Now I will start explain one of possibly many ways that the accepts of OOPS can be implemented in to FoxPro xBase dialect. In a progressive manner, I will redescribe each of the previously aspects in the way that they can be applied to xBase. Unforunately, the method described here requires more process time in order to perform certain operations, not to mention a complete facelift in the way the programmer view the application they are developing. Also a greater number of public memory variables are declared, plus the string space required by the assignments of method variables. The preplanning and predefining of the boundaries and limitations of the application become a rule. The modulization of object orientation will aid in the designing of the project. Also, unlike many other OOPS languages, this methodology does not dictate that programmer use only objects. This method is defined as a language enhancer. Most of the application should remain as structure code, with the object calls weaved in, to perform more generized tasks. Tasks like keyboard and mouse events, screen and window control (which may include predefined data-entry screens), database manipulation, and printers. From these examples, it is obvious that in the term of FoxPro, it is best to use objects only when addressing I/O related operations. Processing part of the application should continue to the designed and wrote in the current structured fashion. The only possibly exception to this rule would be if some blocks of processing code could be generalized into a class; ie. Arrays, or mathimatics.
The first thing we need to do is define a base from which to biuld future classes. For ease, we will call it the Base class. The Base class is the simplist class in the system. All other class are children of the Base class, so that Base provides a common platform of all the classes. The Base class performs no services, except in the creation and destruction of objects. It provides the basic attributes for controlling objects within application's environment.
To define a class in FoxPro, we need a method of parsing a templete of the class into a functioning object at run-time. The simplist way is the use User-defined functions. They offer a means of naming and dynamicly creating objects. The parameter declaration is used to pass any default initializing values to the object being created.
Along with these parameters, a special parameter is declared: the Self reference. The Self reference parameter aids in defining the new object uniquely from any other objects defined. Giving each object a sence of individuality. Normally, when a class procedure is called, Self is passed as an empty character string.
The class attribute are declared as public variable in a special way. These attributes come in two types: data and methods. The syntex to declaration and usage is the important part. The Self reference is concatentated to the front of the attribute, as indicated in example 1.
Example 1.a. The Base Class Procedure:
procedure Base && Base Class parameter Self Self = construct(Self) && Generate Self if .not. empty(Self) && Test for error store program() to (Self) && assign Class name public (Self + 'DONE') && Declare Method store [DONEBASE] to (Self + 'DONE') && Assign Method endif return(Self)Example 1.b. Sample Creation Of An Instance Of Base:
private Base && Instance
Base = Base('') && Object Creation
do (evaluate(Base + 'DONE')) with Base && Object Destruction
Example 1.a illustrates the complete Base Class procedure, along with a
sample construction and destruction operation of a Base Object. Let's take
moment and diesect the example to better understand how objects are created.
Following the Class Procedure statement is the parameter statement. All class
procedure have a parameter statement with at least one parameter, Self. When
the class procedure is called, a user suggested self reference, or a null string
is passed to Self. The first statement of the procedure is a call to the class's
parent constructor, in which returns a self reference value. In the case of the
Base example, Base does not have a parent class so a call to Construct is made.
The Construct Function's single purpose is to generate a Self Reference, if a
valid one wasn't passed to it. The Construct function will be discussed in
greater detail shortly, but for the purpose of the explaination, we'll assume a
value of 'O00' was recieved by Self. The IF statement at the second line is a
spot error check. If Construct was unable to generate a Self Reference, a null
string is returned.The third statement assign, to the variable repersented in Self, the class name. Because the procedure name is the class name we need only assign it the value of Program(). Notice that through Indirect Reference, O00 is assigned the name of the class, not the variable Self. Each decendent class should update this variable with name of the current class.
The forth statement is the public declaration of the method DONE. As explained earlier, Self is tacked on to the front of the method's name to compose a unique variable name, which results in 'O00DONE'. The fifth statement is the assignment of the method procedure name to the method attribute. The final statement of the class is the return of the Self reference used during the construction of the object. Class procedures are declared as a PROCEDURE, but also returns a value like a function. This allows the UDF to be accessed safely as a both a procedure and a function.
These lines of Example 1. generalize the basic requirements for composing objects. Every child object should be comprised of these features (some exception are allowed).
The Example 1.b uses the Base Class procedure to construct an Base object. The Private statement declares a variable to be used as an Instance of the newly created object. The following statement is the call to the Base class procedure (called as a function), returning the Instance. The Instance then used there-after to access the various attributes of the object. The last statement demonstrates this, by using the instance to call the object's DONE method, destroying the object. The method call syntex is comprised of the instance, and the name of the method, combined inside a Indirect Reference parentheses. A copy of the Instance is passed as the object's Self.
Example 2. Base's DONE Method Procedure:
procedure BaseDone && Method Name parameter Self && Object Instance release (Self + 'DONE') && Release method do Destruct with Self && Release Self Reference return(Self)Within the BaseDone Method (Example 2.) we find that it releases the varaibles used to hold the object's attribute. Followed by a call to the Destruct Procedure. The Destruct Procedure release the Instance back into the pool of available Self References. Normally, this call is to the most immediate parent's DONE method, but Base is defined as the root of all Classes and has no parent. The return of Self is erroneous, for any result of the call is discarded. The Return statement is only included to maintain compatibility with all the other methods in the system.
The Construct and Destruct procedure define the true base to implimenting OOPs into FoxPro. They provide a automatic way to generate and destroy the self refereneces used to manipulate objects.
Example 3. The Construct and Destruct Procedures:
procedure Construct
parameter Self
private x_
x_ = 0
do while (x_ <100 ) .and. (tpye(Self)="C" ) x_="x_" + 1 Self="ObjRef(" x_ ) enddo if (x_="100)" wait "Error Out of Object References" window nowait return('') endif public (Self) store program() to (Self) return(Self) function ObjRef
parmaerer x_
return("O"+ padl(ltrim(str(x_ ) ), 2, "0") )
procedure Destruct
parameter Self
if .not. empty(Self) && If reference exist
release (Self) && release reference
endif
return(Self)
The Construct procedure searchs the pool of objects, and returns the first
available Self Reference found. The name of a prospective reference is generated
by the function ObjRef. A return of a null string from the construct procedure
indicates an error has occur in the attempt. The example limits the number of
auto-generated instances to 100, but the only real limitation is the size of the
string segment, and the number variables that can be generated by the system.Next, the found Self reference is a declared publicly, and is assigned the name of the constructor as default. This declaration signifies to the future Construct calls that this reference is in uses by an object. Finally, the choosen reference is returned to be used as the instance to the object.
Now that some of the basic have been laid out, let's proceed to construct some simple classes. For purpose of explination, I will pass over the preplaning stage of OOP developement for now, and proceed as if it has already been considered.
Example 4. Class Heritage:
Base |--> Location |--> At |--> Box |--> WindowExample 5.a. Class Definitions:
procedure Location
parameter Self, R, C
Self = Base(Self)
store program() to (Self)
public (Self + [ROW]), (Self + [COL])
public (Self + [DONE]), (Self + [Show]), (Self + [Move])
store [Loc_Done] to (Self + [Done])
store iif(type('R') = "N", R, 0) to (Self + [Row])
store iif(type('C') = "N", C, 0) to (Self + [Col])
store [Loc_Show] to (Self + [Show])
store [Loc_Move] to (Self + [Move])
return(Self)
Example 5.b. Location Method Declarations:
procedure At
parameter Self, R, C, Content
Self = Location(Self, R, C)
store program() to (self)
public (Self.[Content])
public (Self.[Done]), (Self + [Show])
store [At_done] to (Self + [Done])
store Content to (Self + [Content])
store [At_show] to (Self + [Show])
return(self)
procedure Box
parameter Self, T, L, B, R, Style
Self = Location(Self, T, L)
store program() to (self)
public (Self + [Bottom]), (Self + [Right]), (Self + [Style])
public (Self + [Done]), (Self + [Show]), (Self + [Move])
store [Box_Done] to (Self + [Done])
store iif(type("B") # "N", evaluate(Self + [ROW]) + 1, B) to (Self + [Bottom])
store iif(type("R") # "N", evaluate(Self + [COL]) + 1, R) to (Self + [Right])
store iif(type("Style") # "C", "", iif( inlist(Style, "DOUBLE", "PANEL"), Style, "")) to (Self + [Style])
store [Box_Show] to (self + [Show])
store [Box_Move] to (self + [Move])
return(self)
procedure Window
parameter Self, T, L, B, R, Style
Self = Box(Self, T, L, B, R)
store program() to (Self)
public (Self + [Name])
public (Self + [Done]), (Self + [Show]), (Self + [Hide]), (Self + [Move]), (Self + [Size])
store [Win_Done] to (Self + [Done])
store Self to (Self + [Name])
store [Win_show] to (Self + [Show])
store [Win_hide] to (Self + [Hide])
store [Win_move] to (Self + [Move])
store [Win_Size] to (Self + [Size])
store iif(type("Style") = "C", Style, "") to (Self + [Style]), Style
define window (self) from T, L to B, R &Style
return(Self)
The idea of default parameter augments is not unique to the OOPs. It can be used in standard structured programming as well. The concept is testing the various parameters passed to the function to insure that they are of the purpore type and value. In the case of Default Parameter values, if no value was passed for a specific parameter, a valid value is then substituted.
Example 6. Default Parameter Auguments:
procedure Box
parameter Self, Top, Left, Bottom, Right, Style
top = iif(type("Top") # "N", 0, Top)
left = iif(type("Left") # "N", 0, Left)
bottom = iif(type("Bottom") # "N", Top + 1, Bottom)
right = iif(type("Right") # "N", left + 1, Right)
style = iif(type("Style") # "C", "",;
iif(InList(upper(style), "DOUBLE", "PANEL", ""), style, ""))
The concept of Overloading function attributes is impossible in the FoxPro language, because the function's parameter are declared as untyped. Overloading occurs when functions of the same name are declared within the scope of a single class, but contain a parameter of different types. The only complimentary way to approuch this concept is to test for the various types with in the body of the function itself. The ability of declaring multiple functions of the same name is omitted, and the different parameter types would be noted in the comments provided with the attribute declaration.
Example 7. Overloading By Imbedded Type Testing:
procedure Func
parameter Aelf, A
private _pcnt, _type
_pcnt = parameters()
_type = type("A")
do case
If none passed, declare as character.
case _pcnt=1 A = "" case _type="C" case _type="N" case ..... endcase ..... return(A)An alternate method of Overloading can be achieved by way of Inheritance. Each child Method is declared in a Virtual manner, and handles a single type. If the child Method determines that the parameter is not the type required for the Method then the previous parent declaration of the Method is called, with use of the Obj.DO method.
This method of Overloading requires more overhead of code and processing time, and should be used sparingly.
Rethinkng Object Construction
Idea to controlling object creation
procedure ObjSys
parameter Self, Depth_, Key_
set udfparms to reference
public Obj, ObjDepth, ObjKey, UnitName
ObjDepth = iif( type("Depth_") = "N", Min(Max(Depth_, 1), 3), 2 )
ObjKey = iif( type("Key_") = "C", left(KEY_, 1), "O")
UnitName = iif(type("UnitName") # "C", sys(16), UnitName)
Obj = ObjectO(iif(type("Self") # "C", "", Self))
return(Obj)
procedure ObjEnd
do (evaluate(Obj + [Done])) with (obj)
release Obj, ObjDepth, ObjKey, UnitName
return(0)
Initialize new Object Reference
procedure Construct parameter SelfIf no Self is being implied search for first unused reference
if empty(Self) private x_ for x_ = 0 to (10^ObjDepth) - 1 if type((ObjKey + padl(ltrim(str(x_)), ObjDepth, '0'))) = "U" exit endif endforCreate reference
Self = ObjKey + padl(ltrim(str(x_)), ObjDepth, '0')Check for overflow
if type(Self) # "U" ? "Error - Out of Object Handles" cancel endif endifInitialize Object Reference
public (Self) return(Self)Destroy existing Object Reference
procedure destruct parameter Self if (! empty(Self)) .and. (type(Self) # "U") release (Self) Self = [*] endif return(Self)procedure ObjectO parameter Self = Construct(Self) store program() to (Self) do declare with Self, [Define],[Declare], program() do declare with Self, [Constr],[Construct], program() do declare with Self, [Destr], [Destruct], program() do declare with Self, [Free], [Free], program() do declare with Self, [Get], [Get], program() do declare with Self, [Put], [Put], program() do declare with Self, [Do], [Perform], program() do declare with Self, [Save], [ObjSave], program() do declare with Self, [Load], [ObjLoad], program() do declare with Self, [Done], [ObjDone], program() return(Self) procedure declare parameter Self, Var, Value, Class, Dir, Proc private pcnt, tvalue, Sx pcnt = parameters() if empty(var) return(.f.) else var = upper(alltrim(var)) endif tvalue= iif(type("Value") # "C", [*], Value) class = iif(pcnt >= 4, upper(alltrim(class)), []) if (pcnt >= 5) dir = upper(alltrim(dir)) dir = iif(' '$dir, substr(dir, rat(' ', dir) + 1), dir) dir = iif('\'$dir, left(dir, rat('\', dir)), dir) else dir = [] endif if (pcnt >= 6) proc = upper(alltrim(proc)) proc = iif(' '$proc, substr(proc, rat(' ' ,proc) + 1), dir) else proc = [] endif if empty(proc) .and. (!empty(dir)) .and. (!('\'$value)) value = dir + value endif public (Self + Var) Var = iif("("$Var, left(Var, at('(', Var) - 1), Var) Var = iif("["$Var, left(Var, at('[', Var) - 1), Var) store Value to (Self + Var)Declare VML if need to.if type(Self + "VML") = "U" Sx = iif(type("OBJ") # "C", Self, Obj) do (evaluate(Sx + "define")) with Self, [VML], [] endif store proc + chr(251) + dir + chr(252) + cass + chr(253) + var + chr(254) + tvalue + chr(13) + chr(10) + evaluate(Self + "VML") to (Self + "VML") return(.t.) procedure Free parameter Self, Var, Class private Pnt, X Temp, VML Pcnt = parameters() if empty(Var) .or. empty(self) return(.f.) endif X = atcline(iif(pcnt = 3, chr(252) + alltrim(class), '') + chr(253) + alltrim(var) + chr(254), VML) if X > 0 temp = mline(evaluate(Self+ "VML"), X) store stuff(evaluate(Self + "VML"), at(temp, evaluate(Self + "VML")), len(temp) + 2,[]) to (Self + "VML") X = atcline(chr(253) + alltrim(var) + chr(254), evaluate(Self + "VML")) if X > 0 temp = mline(evaluate(Self + "VML"), X) store substr(temp, at(chr(254), temp) + 1) to (Self + Var) else if ((chr(251) + chr(252) + chr(253) + [VML] + chr(254) + chr(13) + chr(10)) = evaluate(Self + "VML")) do (evaluate(Obj + "Free")) with Self, [VML] endif release (Self+var) endif return(.t.) endif return(.f.) procedure Get parameter Self, Var, Value, Class if type("Class") = "C" private X X = mline(evaluate(Self + "VML"), atcline(chr(252) + alltrim(Class) + chr(253) + alltrim(Var) + chr(254), evaluate(Self + "VML"))) Value = substr(X, at(chr(254), X) + 1) else Value = evaluate(self + alltrim(Var)) endif return(Value) procedure Put parameter Self, Var, Value, Class if type("Class") = "C" private Token, X Token = chr(252) + alltrim(Class) + chr(253) + alltrim(Var) + chr(254) X = atc(Token, VML) if X > 0 store stuff(VML, X + len(Token), len(mline(VML, atline(Token, VML))) - len(Token), alltrim(upper(Value))) to (Self + "VML") return(.T.) endif else store Value to (Self+Var) return(.T.) endif return(.F.) procedure Perform parameter Self, Var, Class, P0, P1, P2, P3, P4, P5, P6 private Pcnt, Tpara, X, Temp, Tproc, Tdir, Tvar, _oldproc pcnt = PARAMETERS() if empty(self) .or. empty(var) return(.f.) endif Tpara = substr(", P0, P1, P2, P3, P4, P5, P6", 1, (pcnt - 3) * 3) Var = upper(alltrim(var)) Class = iif(type("Class") = "C", Class, []) Temp = evaluate(Self + "VML") X = atcline(iif((Pcnt > 2) .and. (! empty(Class)), chr(252) + alltrinm(Class), "") + chr(253) + var + chr(254), Temp) Temp = mline(Temp, X) Tproc = left(Temp, at(chr(251), Temp) - 1) Tdir = substr(Temp, at(chr(251), Temp) + 1) Tdir = left(Tdir, at(chr(252), Tdir) - 1) Tvar = iif( ! empty(Class), substr(Temp, at(chr(254), Temp) + 1), (Self + Var)) if empty(Tvar) return(.f.) endif if ('\'$tvar) .or. (':'$tvar) do (Tvar) with Self &Tpara return(.t.) endif if ! empty(Tdir) if empty(Ttproc) Tvar = Tdir + Tvar do (Tvar) with self &tpara return(.t.) endif if .not. (('\'$tproc) .or. (':'$tproc)) Tproc = Tdir + Tproc endif else if empty(Tproc) do (Tvar) with Self &Tpara return(.t.) endif endif if .not. (empty(UnitName) .or. empty(tProc)) _OldProc = UnitName UnitName = Tproc if (_OldProc # Tproc) do (Tvar) in (UnitName) with Self &Tpara else do (Tvar) with Self &Tpara endif UnitName = _OldProc return(.t.) endif return(.f.) procedure ObjLoad parameter Self, From_, Asmemo private l0para, l0memo l0para = parameters() l0meno = iif(Asmemo .and. (! empty(alias())) .and. (type(To_) = "M"), [memo], []) restore from &l0memo &From_ additive do Publics with Self, evaluate(Self + "VML") restore from &l0memo &From_ additive return(Self) procedure Publics parameter Self, p0VML private X, Temp, A release (Self) public (Self) for X = 1 to memline(p0VML) Temp = mline(p0VML, X) a = at(chr(253), temp) + 1 temp = substr(temp, a, (at(chr(254), temp) - a)) release (Self + Temp) public (Self + Temp) endfor return procedure ObjSave parameter Self, To_, Asmemo private l0Self, l0memo l0meno = iif(asmemo .and. (! empty(alias())) .and. (type(To_) = "M"), [memo], []) save to &l0memo &to_ all like &Self.* return(Self) procedure ObjDone parameter Self do Free with Self, [Constr] do Free with Self, [Destr] do Free with Self, [Define] do Free with Self, [Get] do Free with Self, [Put] do Free with Self, [Do] do Free with Self, [Save] do Free with Self, [Load] do Free with Self, [Done] do Free with Self, [Free] = Destruct(Self) return(Self)
Bibliography: