The ABCs Of OOP - Part 3
Posted June 28 1999
Virtual methods are one of the most useful and powerful features of object-oriented programming. I don't think it's understating the case to say that understanding virtual methods opens up whole new vistas of software development. In fact, without virtual methods, the ABC templates and class library simply couldn't exist, at least not in their present form.
I've had several opportunities to present object-oriented programming basics in articles and in seminars, and I remain convinced that while OOP concepts often look intimidating, they're really not that difficult to grasp. If anything, it's the appearance of difficulty that is the real difficulty. But if there is an aspect of OOP that can be a bit tricky to get, it's virtual methods.
In this installment I'll build on the information I presented in the previous articles in this series. As you'll recall, the first article described some basic OOP concepts, and the second article elaborated on inheritance and encapsulation.
The DebugClass Example
Both of those articles discuss a small example class which can be used to store debugging messages in a log. Listing 1 shows the declaration for the class, and Listing 2 shows the implementation.
Listing 1. The DebugClass declaration.
TraceQueue QUEUE,TYPE
Text STRING(200)
END
DebugClass CLASS,TYPE,MODULE('DEBUG.CLW')
NextLineToWrite long(1)
TraceQ &TraceQueue
Construct PROCEDURE
Destruct PROCEDURE
ShowTrace PROCEDURE
Trace PROCEDURE(STRING Text)
WriteTrace PROCEDURE
END
Listing 2. The DebugClass implementation.
MEMBER
MAP
END
INCLUDE('DEBUG.INC')
TraceLog FILE,DRIVER('ASCII'),NAME('TRACE.LOG')
,CREATE,PRE(TRACE)
Record RECORD,PRE()
Text STRING(1000)
END
END
DebugClass.Construct PROCEDURE
CODE
SELF.TraceQ &= NEW(TraceQueue)
DebugClass.Destruct PROCEDURE
CODE
FREE(SELF.TraceQ)
DISPOSE(SELF.TraceQ)
DebugClass.ShowTrace PROCEDURE
window WINDOW('Debug Messages'),AT(,,331,206),|
FONT('MS Sans Serif',8,,,CHARSET:ANSI),|
SYSTEM,GRAY,DOUBLE
LIST,AT(5,5,320,180),USE(?List1),HVSCROLL,|
FONT('Courier New',8,,FONT:regular,CHARSET:ANSI)|
,FROM(self.TraceQ)
BUTTON('Close'),AT(150,190,,14),USE(?Close)
END
CODE
OPEN(WINDOW)
ACCEPT
IF FIELD() = ?Close AND EVENT() = EVENT:Accepted
BREAK
END
END
DebugClass.Trace PROCEDURE(STRING Text)
CODE
SELF.TraceQ.Text = Text
ADD(SELF.TraceQ)
IF RECORDS(SELF.TraceQ) % 10 = 0 THEN SELF.WriteTrace().
DebugClass.WriteTrace PROCEDURE
X LONG
CODE
OPEN(TraceLog)
IF ERRORCODE()
CREATE(TraceLog)
OPEN(TraceLog)
IF ERRORCODE()
MESSAGE('Unable to open error log: ' & ERROR())
RETURN
END
END
LOOP X = SELF.NextLineToWrite TO RECORDS(SELF.TraceQ)
GET(SELF.TraceQ,X)
TraceLog.Text = self.TraceQ.Text
ADD(TraceLog)
END
SELF.NextLineToWrite = RECORDS(self.TraceQ) + 1
CLOSE(TraceLog)
You will notice at least one difference between the class discussed in the previous article and the one shown in Listings 1 and 2. This version of the class has a WriteTrace method which figures heavily in this discussion of virtual methods.
The WriteTrace Method
The WriteTrace method isn't a virtual method - it's just a normal method like the others. The purpose of WriteTrace is to write the debug messages out to a text file. You could call this method periodically yourself (as when exiting a procedure) but it's much easier to have the debug class handle the call automatically.
The Trace method (see Listing 3) accomplishes this by calling the WriteTrace method on every tenth trace call. The modulus (%) operator simply tests for the remainder of division, in this case by 10. This effectively creates a cache of up to ten records, which is a much more efficient approach than opening the file, writing out one record, and closing the file again. (You could use a property in place of "10" and have an adjustable cache size, if you wished.)
Listing 3. The Trace method calls WriteTrace.
DebugClass.Trace PROCEDURE(STRING Text) CODE SELF.TraceQ.Text = Text ADD(SELF.TraceQ) IF RECORDS(SELF.TraceQ) % 10 = 0 THEN SELF.WriteTrace().
Accept for the moment that you've decided you'd like DebugClass to store its log of information in one of your application's data files rather than in a text file. Remembering the principle that it's usually better to derive a new class than modify an existing class, you decide to create a derived class that will work with your application. Can you simply rewrite the WriteTrace method so it uses your application's data file?
You can try, but the compiler
won't be impressed. As you can tell by the empty
MEMBER statement in Listing 2, this class is generic,
which means it can be compiled with any application. Now in Clarion
2.1, modules with empty MEMBER statements
automatically gained access to the global data of the application
in which they were compiled. That's not the case any more.
Modules can now only "see" declarations to which they're given
explicit access, either by a member statement that points to an
application source file, or by use of INCLUDE
statements.
DebugClass doesn't have any
knowledge of your application's data, and that's the way
you want it, because this class needs to work with any application.
If you use an application-specific INCLUDE or
MEMBER statement that points to an application,
you've effectively prevented yourself from using that class in
any other application.
Two-Tiered Development
The answer to the problem is to use
a two-tiered development strategy, which is what ABC does. In the
top tier are generic classes with empty MEMBER
statements. In the bottom tier are the application-specific
classes. All the common code (or as much of it as possible) goes in
the top tier, thereby maximizing code reuse. Figure 1 shows the two
tiers.
Figure 1. A two-tiered class design.

This two-tiered approach is an ideal way to solve the problem of making DebugClass work with a specific application's data (and code).
To test this, create a TPS file in
the application's data dictionary (or follow along with the example application). This file, called Trace,
only needs one field (Text STRING(1000)) and
doesn't need any keys, although if you wanted to jazz it up
you could add the date and time the trace record was added.
In the application, create a main menu item to call a browse procedure which will display the trace log records. Again, you don't have to specify a key, since if no key exists the browse will display the records in the order they were added. You don't need to bother with an update procedure for this browse since all the records will be created by the derived debug class. Use the Browse wizard to create a procedure to browse the Trace TPS file.
The Derived Debug Class
When I create generic classes I
usually put them in my LIBSRC directory along with all
of the ABC classes. When I create an application-specific class I
normally put it in the application directory, as I won't be
wanting to use it with any other apps.
The derived
AppDebugClass declaration is shown in Listing 4. There
are several differences between the way this class is declared and
the way its parent class (DebugClass) is declared.
Listing 4. The derived AppDebugClass declaration (AppDebug.INC).
OMIT('_EndOfInclude_',_APPDEBUGPRESENT_)
_APPDEBUGPRESENT_ EQUATE(1)
INCLUDE('DEBUG.INC')
AppDebugClass CLASS(DebugClass),TYPE,
MODULE('appdebug.clw'),LINK('appdebug.clw')
WriteTrace PROCEDURE
END
_EndOfInclude_
As with last article's DebugTimedClass
AppDebugClass has a CLASS(DebugClass) attribute
which tells the compiler which class to use it comes across methods
and properties which aren't declared within
AppDebugClass. The INCLUDE statement
points to the source file which contains the parent class's
declaration.
The LINK attribute
tells the compiler which source file contains the class
implementation. If you have a LINK attribute on the
class you don't need to explicitly add the source file to the
project.
NOTE: Although it's
usually best to use the LINK attribute, you may on
occasion wish to add the source to the project manually, as this
lets you set compiler pragmas on just that file. You will also need
the DLL attribute if using classes exported from a
DLL. See the ABC class declarations for examples.
Another curious aspect of this
include file is the use of an OMIT statement. The
OMIT prevents the declaration from being referenced by
the compiler more than once. This is a way of preventing duplicate
symbol warnings which can happen if you have a number of nested
includes. The first time the compiler references this code the
AppDebugPresent flag is false, by default, and
immediately after the OMIT it's set to true, so
on all subsequent passes the compiler ignores the
OMITted code.
There is only one method declared in AppDebugClass, and that is WriteTrace. The implementation, shown in Listing 5, replaces the original WriteTrace method with code that puts the trace statements in the application's Trace file.
Listing 5. The AppDebugClass implementation (APPDEBUG.CLW).
MEMBER('TEST.CLW')
MAP
END
AppDebugClass.WriteTrace PROCEDURE
X LONG
CODE
ACCESS:Trace.OPEN()
ACCESS:Trace.UseFile()
LOOP X = SELF.NextLineToWrite TO RECORDS(self.TraceQ)
GET(SELF.TraceQ,x)
TRA:Text = SELF.TraceQ.Text
ACCESS:Trace.Insert()
END
SELF.NextLineToWrite = RECORDS(self.TraceQ) + 1
ACCESS:Trace.CLOSE()
At the top of the source listing is
the MEMBER('TEST.CLW') statement which,
following the two-tier model, ties this class to the
application's data and allows the use of all global data,
classes, and procedures. If you compare this code with the base
class code, you'll see that one of the benefits of using ABC
is a lot of tedious code is taken care of in the libraries. Instead
of the original's somewhat hacked attempt to make sure that
the trace file can be opened, you have a single call to
ACCESS:Trace.Open which takes care of all of the error
checking. Similarly ACCESS:Trace.Insert checks for any
problems with the add. Other than this, the replacement method
simply takes the same approach as the original but adds the records
to the application's TPS Trace file rather than to the ASCII
trace file.
To implement the class put the following in a global embed point, such as After Global Includes:
INCLUDE('APPDEBUG.INC')
db AppDebugClass
If you've been following along
with the previous articles you'll be working with an
application that already has a db object, but which is
declared as an instance of DebugClass not
AppDebugClass. You will also have an
INCLUDE statement that points to
'DEBUG.INC' rather than
'APPDEBUG.INC'. Because
AppDebugClass is derived from DebugClass,
you can leave all of the calls to the db object's
methods as they were, and simply make db an instance
of the derived class. Derived classes don't lose any data or
methods, although they may change the implementation of some of
those methods.
Testing AppDebugClass
The apps from the previous articles, and the example application for this article demonstrate a simple use of the debug classes. The each have the code:
db.Trace('Event ' &
EVENT())
in the TakeEvent method of the Names browse object in the BrowseNames procedure. If you call BrowseNames, the Trace method records all of the events that the browse receives.
This application uses AppDebugClass which has a replacement WriteTrace method which writes messages to the TPS Trace file. So what happens if you run the application using AppDebugClass?
Well, not what you might expect. If you call the BrowseNames procedure, it turns out that AppDebugClass functions exactly the same way as DebugClass. The trace messages are still logged to the ASCII file, and the TPS Trace file remains empty.
What went wrong?
As Figure 2 shows, only the WriteTrace method is declared in both classes. The actual object or instance of the class which is used in your application contains both methods, and there are rules which govern which of the two methods will be called.
Figure 2. The DebugClass/AppDebugClass class diagram.

The problem is that although the
object is an instance of AppDebugClass, in the example
application code the WriteTrace method is never
directly called by the application, as is the Trace method.
WriteTrace is always called by the Trace
method itself, which is declared only in
DebugClass.
To understand what's happening
you need to be clear on several concepts. One is that the db
object is a combination of AppDebugClass and
DebugClass. The other is that within that object are
(in this case) two levels at which code exists: the upper (parent)
and lower (child) levels.
When your code calls the
Trace method it's making a call to the upper
parent part of the object. The rule is that by default, an upper
level cannot call down to a lower level. This makes sense, since
the upper part (DebugClass) doesn't have an
INCLUDE statement that points to the lower level
(AppDebugClass), although the reverse is true. Another
way to say this is that the child class always knows about the
parent class, but the parent doesn't normally know about the
child.
If you call a lower level method,
that method can call a higher level method, implicitly (if the
method doesn't exist at the lower level) or explicitly (by
using the PARENT keyword). But under normal
circumstances a higher level method can never call a lower level
method. So once control passes to the higher level, that's
where it's going to stay.
It would be possible to get around this problem by copying the Trace method to AppDebugClass since that would bring it down to the same level as the replacement WriteTrace method, but that would defeat the whole purpose of reusing code. What you really want is a way to tell the upper part of the object that you've created a replacement for one of its methods, and you want it to call your replacement instead of its own method.
Thankfully there is a way to do this, and it's called the Virtual Method.
Virtual Methods
Virtual methods are ridiculously easy to implement. Simply add the VIRTUAL attribute to the WriteTrace method in both classes, as shown in Listings 6 and 7.
Listing 6. DebugClass with the VIRTUAL attribute on WriteTrace.
DebugClass CLASS,TYPE,MODULE('DEBUG.CLW')
NextLineToWrite long(1)
TraceQ &TraceQueue
Construct PROCEDURE
Destruct PROCEDURE
ShowTrace PROCEDURE
Trace PROCEDURE(STRING Text)
WriteTrace PROCEDURE,VIRTUAL
END
Listing 7. AppDebugClass with the VIRTUAL attribute on WriteTrace.
AppDebugClass CLASS(DebugClass),TYPE,MODULE('APPDEBUG.CLW'),LINK('APPDEBUG.CLW') WriteTrace PROCEDURE,VIRTUAL END
The VIRTUAL attribute
reverses the natural direction in which the program, at runtime,
looks for methods. By default, methods are always checked for on
the current level. If they can't be found, the program looks
in the parent, and then that class's parent if present, and so
on up until it finds a method.
Virtual methods work the other way around. If a virtual method exists at a particular level in an object, the program looks in the object to see if a replacement virtual method has been declared at a lower level.
Now if you compile the test application, and you create more than ten trace messages (or whatever the cache size has been set to) and bring up the Trace browse, you'll see that trace records have in fact been added to the TPS file.
NOTE: You need to put the
VIRTUAL attribute on both the parent and
child WriteTrace methods.
This is an incredibly powerful feature of object-oriented programming. With virtual methods you can plug and play with the methods of a class without having to know how or when those particular methods are called. In fact, once you understand how virtual methods work, you understand how ABC works!
Virtual Methods and ABC
It's no understatement to say that without virtual methods there would be no ABC templates and class library. It's not that difficult to create a block of code, class or procedure, that can display and browse records. But how do you allow someone to plug their own code into that code? The legacy templates handle this problem by generating all of the code and allowing you to insert your code in as needed, but this results in excessive code generation and mixes templates and source code.
In ABC, almost all of the logic is in top-tier generic classes, which makes testing and debugging (by Topspeed) much easier. In most cases when you put code in an embed point, the templates create a derived method which is automatically called by the parent class (such as BrowseClass) at the appropriate time.
The derived classes are located in your modules, so they have access to all of your application's information. By means of virtual methods, the base classes "call down" into your created code whenever appropriate.
If you wish to format a browse box field for display, for instance, your code is generated into the virtual SetQueueRecord method which is called whenever the browse object needs to copy the data from the file to the queue used for display. (And this derived, generated SetQueueRecord also contains a call to PARENT.SetQueueRecord to ensure that the default behaviour still happens as well.)
I'm sometimes asked if, since virtual methods are so powerful, all methods should be made virtual. There is a small performance penalty associated with using virtual methods, since the system must maintain a virtual method table (VMT), or list of methods, so it knows at runtime which methods to call under which situation (this is an example of "late" binding). VMT lookups do take some time, though it's debatable whether in most situations you'd notice.
Under The Hood
At the start of this series of articles I commented that most ABC procedures begin with one line of code:
GlobalResponse =
ThisWindow.Run()
and that this code appeared to not
call any other code generated as part of the procedure. As you can
now see, the answer to this puzzle is (drum roll) virtual methods.
The call to ThisWindow.Run passes control up to the
top tier base class, and the code rambles about at that level for a
while until it comes across a virtual method (such as
ThisWindow.Init), at which time control passes down to
the lower derived level.
Virtual methods really do make working with the ABC class library "plug and play." You can also use this mechanism in your own classes. It's a good exercise to look at your class designs and ensure they're structured in such a way that someone else could selectively replace methods to change the behaviour without breaking the code. And by going to a two-tiered approach you help to maximize code reuse and maintainability, two key benefits of object-oriented programming.
Article comments
Post a comment
You must be logged on to post comments.
Talk To Us!
Search ClarionMag
From the archives
Superfiles and NAME
9/14/2009 12:00:00 AM
Having covered Superfiles in the previous episode, Steve Parker tackles the intricacies of how to set arbitrary names for the tables inside Superfiles.
