In order to write your own classes, first you will need to understand OOP syntax.
Rule number 1, you must define each class in a separate PRG. This is because methods are actually preprocessed into static functions.
There are three scoping terms used in class definition; EXPORT (or PUBLIC), PROTECTED (or READONLY), and HIDDEN (or LOCAL). All variables and methods are exported by default so the "export" syntax is not required. Here we will use the terms EXPORT, PROTECTED, and HIDDEN.
I must point out that in the FiveWin class source code no scoping is used, therefore, everything is EXPORTed. This is really not good OOP practice. Because nothing is protected or hidden, you are able to access data and methods that can cause serious consequences.
As with any code, it is important for stability to make classes self sufficient and as polite as possible. By this I mean that classes should assume nothing, scope all variables as narrowly as possible, and return any changed states back into the state in which they were found (e.g. workareas, record pointers, SETs, etc.).
You can probably guess at the meanings of the three scoping terms, but let's review each one.
For the sake of clarity, even though it is not required, I recommend using the EXPORT term.
class myclass export: var itemno var descrip method new method end endclassThe above class behaves the same with or without the "export:" Notice that it is followed by a colon and that the term is "export" not "exported" with an "ed" like "protected." Strange, but true.
You can use either "var" or "data" to define instance variables but since the FW source code itself uses DATA we should probably stick to that one.
Variables can also be made "private" or "readonly." These variables are readonly when accessed outside the class. They can be changed within the class.
class myclass data itemno data onHand readonly endclass
The above can be rewritten this way with no change in functionality:
class myclass export: data itemno protected: data onHand endclass
I prefer the second style, again for clarity.
Note that protected variables can be changed within the class and also within any subclasses of the class.
Although you can define a methods as PROTECTED, there is no point since methods are readonly anyway. So methods are either visible outside the class or not. They will be visible whether defined as exported or protected.
Both variables and methods can be HIDDEN which means they are invisible outside the class. According to OOP principles of information hiding, you should hide any data and methods that are only needed for internal use of the class. The advantage of this is that you can modify the innards of your class without any concern about what other code might be calling it. You can change the syntax, etc.
class myclass export: data itemno hidden: method calcReorder() endclass
One other thing you might want to do just for code clarity is to define the type of your variables. This code just gets stripped out during preprocessing since Clipper is not strongly typed.
class myclass export: data itemNo as char protected: data onHand as number hidden: data oCustomer as object endclass
Or, you can use Hungarian notation which makes the data type self explanatory:
class myclass export: data cItemNo protected: data nOnHand hidden: data oCustomer endclass
For a reason we will discuss later, this is not always feasible.
Methods are just static functions, so you can return variables from them. You can also return "self" which just refers to the self object. Returning self allows you to chain method calls in a single line like this:
oCustomer:new():edit()
You'll see how handy this is later.
Sometimes you will want to use a Clipper reserved word for a method name, for example, "delete." If you try:
method delete
you will get an error message. There is a way around this. You define the method using the MESSAGE clause and make up a different name for the actual method.
class Customer message delete method _delete endclass
Then write the method like so:
method _delete() ...
Now you can call it using "delete:"
oCustomer:delete()
Fivewin's own source code is totally devoted to data manipulation and interface design. We can write subclasses to modify FW's default behavior to better suite our applications and/or we can write new classes to add additional features.
To create a class that inherits from an existing class you just use the FROM clause:
class myClass from TWhatever method new method balance endclass
The above creates a new class from the existing class TWhatever. Each defined method either is a new method or it overwrites the method of the parent class.
If you write lots of different programs, one very useful tool to have would be an application class. This serves as a starting shell for a new application. It takes care of basic setup of the application.
Below is a simple example of a applicaton class. Use this as your main PRG. All you have to do is to fill in the buidlMenu() and buildBar() methods to have a good start on a new application.
//-------------------------------// // Application Class
#include "fivewin.ch"
#define APP_RESOURCES "myapp.dll" #define APP_HANDLECOUNT 120 #define APP_HELPFILE "myapp.hlp"
function main() TApp():new():activate() return nil
class TApp export: data oWnd,oMenu,oBar,oFont method new method activate hidden: method buildMenu method buildBar endclass
method new() set epoch to 1980 set deleted on set default to (cFilePath( GetModuleFileName( GetInstance() ) )) set resources to APP_RESOURCES setHandleCount(APP_HANDLECOUNT) setHelpFile(APP_HELPFILE) define font ::oFont name "MS Sans Serif" size 0,-12 return self
method activate()
define window ::oWnd MDI; menu ::buildMenu()
::buildBar()
set message of ::oWnd to "My application"
activate window ::oWnd
set resources to close databases ::oFont:end()
return nil
method buildMenu() menu ::oMenu menuItem "&File" menu menuItem "Exit Alt-F4" action ::oWnd:end() endmenu menuItem "&Edit" endmenu return ::oMenu
method buildBar() define buttonbar ::oBar of ::oWnd size 26,28 3d define button of ::oBar return ::oBar
//------------------------------------//
Where I personally think we can gain the most using OOP is in the design and use of business classes. One of the advantages of using OOP is that we can more easily bridge the gap between the users and developers by using a common syntax to describe the domain. In essence, our goal is to first develop a model of the business situation, then build this model using OOP.
To describe the domain, we need to take off our programmer's hats for awhile and just try to describe the business in terms of real world objects, like customer, item, order, invoice, etc. Into this mix we can also add process objects like build-a-widget, process-an-order, etc. It is interesting to note that this is not new thinking; Aristotle described this in his monograph, "Categories" written in 350BC.
For a really good book on designing business classes, see "Business Engineering with Object Technology," available on my web site. I use this book a lot. It is not technical, in that there are no code examples, but it does cover the philosophy and design of business class objects.
As programmers we have already done some classification when we put together data files such as customer, item, etc. These files contain the DATA of a real-world object of the same name. What they don't contain are the methods. Fortunately, FW provides us with the basis for a good solution to creating business objects from data files with its TDatabase class. This class has the curious capability of building a class definition on-the-fly. When the DBF is opened all the fieldnames automatically become DATA of the class. We could manually code this into a class definition, but TDatabase automatically handles this for us.
TDatabase also added methods for all the common database functions like seek, skip, etc. One of the most important features it adds is a data buffer. All field data is automatically loaded into an array which we access using the class data variable names:
oCustomer:name
This really is referring to oCustomer:aBuffer[x] where x is the location in the aBuffer array that the name data is stored. This means we don't have to worry about using scatter and gather routines anymore.
There is a bit of a conflict we need to discuss. A "customer" object properly refers to a single customer, whereas a "customers" (plural) object properly refers to a table (database) of customers. One could view this difference to be like a record vs. a database. An oCustomer object would contain all the DATA like name, address, etc, and load() and save() methods. It might also contain other methods too. A oCustomers table object would have navigation methods, index methods and the like. It would have no need for load() and save() methods because those would be taken care of by the oCustomer (singular) object.
OK, so a oCustomer object would be different than a oCustomers object. But the FW TDatabase class isn't designed using a record object. It contains all the DATA of a oCustomer object and all the methods of a oCustomers table too. Quite a mixture.
To separate out a singular oCustomer object what we need to do is copy all the data from the dbf record to a new object something like this:
class customer export: data custNo data name data address data city data state method new method load method save hidden: oCustomers endclass
method new(cCustNo,oCustomers) ::custno:= cCustNo ::oCustomers:= oCustomers return self
method load() ::oCustomers:seek(::custno) ::name := ::oCustomers:name ::address := ::oCustomers:address ::city := ::oCustomers:city ::state := ::oCustomers:state return self
method save() ::oCustomers:seek(::custno) ::oCustomers:name := ::name ::oCustomers:address := ::address ::oCustomers:city := ::city ::oCustomers:state := ::state ::oCustomers:save() return self
This nicely separates the single oCustomer from the oCustomers table. There are some downside issues. First, this does require extra coding and will require maintenance every time a field is added or changed in the DBF. There will also be a slight performance penalty which may or may not become a significant issue.
I prefer to just use the table object for both the plural and singular customer. This is simpler and faster. By convention, I always give the table object a singular name, like oCustomer so when referring to the data it looks more natural:
oCustomer:name
This is NOT good OOP practice, but it does make things easier and more efficient. Just remember that these classes are a combination of table and business object classes.
TDatabase is lacking in some areas, and I have addressed them by developing a subclass I call TData. This is a shareware product which is available on my web site. The unregistered version is a fully functioning OBJ which contains just a single nag screen. Full source code is supplied upon registration. Obtaining the TData demo will allow you to test all the code in the following discussions.
One important addition provided by TData is the automatic generation of alias names. When you create a new object the alias name is handled automatically. This allows you to open multiple copies of the same DBF without worrying about aliases.
I am finding TData invaluable in implementing business classes. But before we get into that we need to discuss some basic housekeeping issues.
You will need to create a subclass of TData for each of your database files. First define the class:
class TCustomer from TData export: data cTitle init "Customer" method new method add method edit method browse endclass
The first thing you want to do is put your index opening code into the new method (encapsulating this into your class):
method new() super:new(,"cust") ::use() if ::used() addIndex("cust") addIndex("cust2") ::setOrder(1) ::gotop() endif return self
Now, whenever you need to use this DBF all you need to code is:
oCustomer:= TCustomer():new()
This opens the cust.dbf file and all its indexes and moves the pointer to the top of the first index.
If you ever need to add an index, you just add the code in the new() method and it is automatically used everywhere. You could also change the filename or index names without worrying about it affecting your program. This is the advantage of "information hiding" one of the concepts that OOP promotes.
Now we need to add the basic functions for these business object classes. Each will need methods for adding and editing. If you are creating a MDI application you can also add a generic browse method.
The add method is quite simple. You can use the edit method for the entering of the data, you just need to blank out the buffer and set a flag so that the edit method knows to append a new record.
method add() ::lAdd:=.t. ::blank() ::edit() ::lAdd:=.f. ::load() // in case they cancel, this reloads the buffer return self
Create an edit method that just contains a standard edit dialog. In the ACTION clause we handle the append if it is a new record.
method edit(oWnd) local oDlg default oWnd:= wndMain() define dialog oDlg resource "charge" of oWnd title ::cTitle redefine get ::date ID 107 of oDlg update redefine get ::custno ID 108 of oDlg update redefine get ::company ID 109 of oDlg update redefine get ::address ID 110 of oDlg update redefine get ::phone ID 104 of oDlg update redefine button ID 1 of oDlg action (if(::lAdd,::append(),),::save(),oDlg:end()) activate dialog oDlg centered return self
Now we add a generic browse in an MDI child window. Here we are just using the FIELDS clause which displays all the fields. Note that when the browse window is closed we call the ::end() method which closes the database (like the CLOSE statement). We do this so we can call a browse with one line (an example later).
method browse() local oWnd, oLbx define window oWnd mdichild of wndMain() title ::cTitle @ 0,0 listbox oLbx fields alias ::cAlias of oWnd update font appFont() oLbx:bSkip:={|nRecs| ::Skipper(nRecs) } oWnd:setControl(oLbx) activate window oWnd maximized valid (oLbx:cAlias:="",::end()) return self
Look carefully at the add() and edit() methods. You will see that both of these are completely generic--they can be used for any database class. Ah ha! These are perfect candidates for a parent class. Both methods would be inherited by any child class. So lets create a new child class of TData that we use as the parent to our database classes. First lets look at the inheritance tree:
class TDatabase class TData from TDatabase class TXData from TData class TCustomer from TXData
Here is the new TXData class. We add a virtual edit() method so the compiler won't complain since the edit() method is called in the add() method. This virtual method does nothing.
class TXData from TData export: method add method edit virtual method browse endclass
method add() ::lAdd:=.t. ::blank() ::edit() ::lAdd:=.f. ::load() // in case they cancel, this reloads the buffer return self
method browse() local oWnd, oLbx define window oWnd mdichild of wndMain() title ::cTitle @ 0,0 listbox oLbx fields alias ::cAlias of oWnd update font appFont() oLbx:bSkip:={|nRecs| ::Skipper(nRecs) } oWnd:setControl(oLbx) activate window oWnd maximized valid (oLbx:cAlias:="",::end()) return self
Now our TCustomer class consists of only two methods:
class TCustomer from TXData export: method new method edit endclass
method new() super:new(,"cust") ::use() if ::used() addIndex("cust") addIndex("cust2") endif ::setOrder(1) ::gotop() return self
method edit(oWnd) local oDlg default oWnd:= wndMain() define dialog oDlg resource "charge" of oWnd title ::cTitle redefine get ::date ID 107 of oDlg update redefine get ::custno ID 108 of oDlg update redefine get ::company ID 109 of oDlg update redefine get ::address ID 110 of oDlg update redefine get ::phone ID 104 of oDlg update redefine button ID 1 of oDlg action (if(::lAdd,::append(),),::save(),oDlg:end()) activate dialog oDlg centered return self
You can now inherit all your other databases from TXData and you only have to define new() and edit() methods. Of course, the browse() method is generic so it will only be useful for prototyping. You will probably want to write a new customized browse() method for each database.
The nice thing about having another class between TData and your specific database classes is that you can add other things that will be inherited by all your classes. Let's say you want to keep track of the last date and time that any record was updated. You will need to have a field called "updated" in each of your databases. Then just define a new save() method in TXData:
method save() ::updated:= dtoc(date())+" "+time() return super:save()
Now every time a record is saved, the updated field will show the current date and time!
Above I made note of the ::end() in the browse() method. This allows us to attach this short code to a button or menu:
define button of oBar action TCustomer:new():browse()
This will open a MDI child window with a browse of the customer database in it. When the window is closed, the database is also closed.
Ok, up until now everything we have discussed has to do with the user interface. The real exciting stuff
is business methods. It is with these methods that we reflect the behavior of
these objects in the real-world. For instance, in a customer class we could have
an acceptPayment() method.
method acceptPayment(nPayment) class TCustomer
::balance:= ::balance - nPayment
::lastPayment := nPayment
::save()
return nil
Or, you might want to update any invoice amounts due also:
method acceptPayment(nPayment) class TCustomer
::balance:= ::balance - nPayment
::lastPayment:= nPayment
::save()
oInvoice:=TInvoice():new()
oInvoice:acceptPayment(self, nPayment)
oInvoice:end()
return nil
Now to apply a payment to a customer object all you do is:
oCustomer:acceptPayment(nPayment)
Perhaps you are beginning to see the power of OOP and business objects.
I believe strongly that using OOP concepts in your programming will take you to a whole new level of productivity. Additionally, your programs will be much more stable, and changes and additions will become much easier. You will never regret taking the time to learn these concepts.
Copyright 2001 Intellitech. All rights reserved.
James Bott
Intellitech
Computer Consulting for Small Business
jbott@compuserve.com
http://ourworld.compuserve.com/homepages/jbott