I know most of you are scared to death of OOP, but the basic concept of OOP is really not all that complicated, although the real, "ah ha," may not come for 6 months or more. But first, why do we need OOP? The simple answer is that software is just getting way too complicated to write in the old procedural manner. It becomes very hard to grasp a thousand line procedure. Of course, you can break it down into function calls, but then you end up having to pass lots of variables, and you may have to pass back an array to hold all the results. This adds back more complication.
Let's start with something we know and then work toward OOP. As Clipper programmers we have what is called "file-wide statics." These can be declared at the beginning of a PRG and used throughout the PRG. This alleviates the passing of vars and returning an array issues mentioned above. We could declare 50 file-wide statics and use and change them in any UDF in the same PRG. This helps, but doesn't give us all the advantages of OOP.
We need to define some OOP terms. First we have "class" and "object." A class is like a blueprint and an object is an instance of that class, like a house is an instance of a blueprint. There is only one class (defining a particular object) but you may have many instances of that class (objects).
Using OOP we can create "instance variables" that are similar to file-wide statics. Thus we can create a person class, TPerson, and declare instance vars for name, DOB (date of birth) etc. We do this like so:
create class TPerson var cName var dDOB endclass
Using OOP we can also create methods which are similar to static functions but with more flexibility (more in this later). Let's add an age method to our customer class. For the sake of simplicity lets just calculate it in days:
create class TPerson cName dDOB method age endclass method age return date()-::dDOB [Note that only one class can be defined in a single PRG]
An object should be very "black-box like." In other words it should be self contained and only the interface need be known by the user of the object. Note that here the user will be another programmer, not an end user. So for our person class, the user only needs to know to call the age method to get the age of the person. They do not need to know how the age was calculated. This concept is called "encapsulation."
In keeping with this philosophy, we need to make our code as rock solid and bomb proof as possible. We should use only local and static vars and any changes made to the Clipper environment should be restored to their original state. Every class has a method called "new" which initializes the object. Here we assign default values to all instance vars and possibly pass some starting parameters.
method new(cName,dDOB) default cName=:"",dDOB:=ctod(" / / ") ::cName:=cName ::dDOB:=dDOB return self
The DEFAULT command is a handy command added by FiveWin. We use it to assign defaults to all the available passed parameters. Note that cName is NOT the same as ::cName. The double colon (::) is just shorthand for self:, so ::cName preprocesses to self:cName. Self refers to the person object so ::cName is the variable belonging to the person object. cName is the passed parameter.
OK, so the first thing we do when we want to use a person object is create an empty person object by calling the new method. We assign this object to a variable, oPerson in this case:
oPerson:=TPerson():new()
Or, we can pass parameters at the same time:
oPerson:=TPerson():new("John Smith",ctod("01/01/50"))
Note above when we defined the new class that we return SELF. This is so we can use single line syntax and not even assign the object to a variable. For instance:
TPerson():new(,ctod("01/01/50")):age()
This will return the age of a person born on January 1, 1950. We are returning self from the new method which is then passed to the age method. It is a good idea to return self from any method that doesn't need to return anything else.
On the other hand, we can have multiple instances of the person class in existence at the same time.
oFather:=TPerson():new("John Smith",ctod("01/01/50")) oMother:=TPerson():new("Mary Smith",ctod("05/02/55"))
Now we can find the name of each of them:
msgInfo( oFather:name ) msgInfo( oMother:name )
And even more interesting, we can find the age:
msgInfo( oFather:age() ) msgInfo( oMother:age() )
If it helps, you can think of an object as a container that holds not only data (instance vars), but code (methods). Now, here is a really powerful feature. We can pass entire objects to functions or other objects. For example, we could pass oFather to a report object and print his name, DOB, and age from within the report object. This is just a taste of the power of OOP.
Inheritance is one of the most useful and powerful features of OOP. One class can inherit all the vars and methods of another. So, let's create a new class, customer, and inherit from our person class above (let's assume that all customers are people and not companies). Here we want to add a couple of new instance vars, customer number, and current balance due. We also need to initialize them.
class TCust from TPerson var cCustNo var nBalance method new endclass method new(cName,dDOB,cCustNo,nBalance) default cCustNo:="",nBalance:=0 return super:new(cName,dDOB)
Note that we refer to the NEW method of the parent class as super:new(), so we just pass the needed vars to the parent method to initialize them.
Now we can initialize a customer object:
oCust:=TCust():new("John Smith",ctod("01/01/50"),"1111","500.00")
Then we can find his name, age, and balance:
msgInfo( oCust:name )
msgInfo( oCust:age() )
msgInfo( oCust:nBalance )
Here is another feature to consider. Changes to parent classes are reflected in child classes. So if you discovered a bug in the age method of the person class, you could fix it and it would be reflected in the customer class too. Or, you could add new vars or methods to the person class and they would automatically be inherited by the customer class.
Notice that we have used the syntax oPerson:age() and oCust:age(). We have an age method in both classes. In this case one class inherited the method from the other, but we could also have an age method in any other unrelated class, say for instance a car class. The ability to use the same syntax in different classes is called polymorphism (meaning many forms). Not only does this help code readability but it enables you to make more generic code, say by passing any type of object to a report object and just calling the print method, e.g. oObject:print().
The concept of granularity means breaking your code down into smaller pieces to aid in readability and maintainability. Thus instead of just a print method in a report class, you might divide it up into a header, body, and footer methods. Likewise, the body method might be broken up into column header, line item, and column footer methods.
Some of you may be wondering how the FiveWin language has avoided all this OOP syntax. FW simply uses the preprocessor to convert it's command style syntax into OOP syntax. For our person object, we could write:
DEFINE PERSON oFather NAME "John Smith" DOB ctod("01/01/59")
And the preprocessor would just translate it to:
oFather:=TPerson():new("John Smith",ctod("01/01/50"))
One can immediately see that the command syntax is much easier to remember and write. Note that the preprocessor is usually smart enough to handle the clauses being in different orders, so, for instance, you could write it as follows and the preprocessor would still translate it properly:
DEFINE PERSON oFather DOB ctod("01/01/59") NAME "John Smith"
In Part I we have discussed the basics of OOP syntax and building simple classes. In Part II we will discuss some more advanced OOP issues including more syntax and some analysis and design concepts.