Skip to end of metadata
Go to start of metadata

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

Version 1 Next »

There's an apparent paradox to learning object-oriented programming. On the one hand, OOP can be very difficult to grasp. On the other hand many people, when they get the basics in hand, look back and wonder what all the fuss was about.

OOP really isn't that difficult. Once you learn the basics of object-oriented programming you probably won't look back on the experience and think you've come a very long way. It's actually a short trip. But it is a challenging one because it requires a change in how you think about programming. And that change will open up vast new programming possibilities which certainly can be daunting. 

Core concepts

So, what does a class look like? Here's a cut-down version of the DCL's string class (originally written by Rick Martin) for illustration purposes (the actual class has a more complicated declaration with many more methods):

DCL_System_String   CLASS
! Properties
Value                   &STRING,PRIVATE
! Methods
Append                  PROCEDURE(STRING pNewValue)
Assign                  PROCEDURE(STRING pNewValue)
BeginsWith              procedure(string s),byte
Contains                PROCEDURE(STRING pTestValue, LONG pNoCase=0),LONG
Count                   PROCEDURE(STRING pSearchValue, <LONG pStartPos>, <LONG pEndPos>, BYTE pNoCase=0),LONG
EndsWith                procedure(string s),byte
Get                     PROCEDURE(),STRING
Split                   PROCEDURE(STRING pSplitStr)
                    END

And here's one of the methods:

DCL_System_String.Assign        PROCEDURE(STRING pNewValue)
stringLength                          LONG!,AUTO
    CODE
    Self.DisposeStr()
    stringLength = LEN(pNewValue)
    IF stringLength > 0
        Self.Value &= NEW STRING(stringLength)
        Self.Value = pNewValue
    END

As you can probably tell by looking at the code, classes (usually) contain both code and data. In order to use a class, you first have to create an instance of the class (if you want, you can have your Clarion program do this automatically for you - more later). And when you create an instance of a class you also have the opportunity for that instance to have its own data. In the above example there's a class property called Value - this is a reference to a string (it could also be a simple string but for a variety of reasons a reference works better here). The Value property continues to exist as long as the class exists. In contrast, the Assign method also has a variable, but that variable only exists for the duration of the call to Assign. As soon as the Assign method completes, the stringLength variable goes away. 

Because class properties exist as long as the class exists, the different methods can each operate on the class properties. In the case of the string class, the Value property contains the value of the string, and the different methods perform actions on that string such as setting the value, appending a string, determining if the string begins with a specified value, etc. 

I think the DCL_System_String class is a great example of the benefits of object-oriented programming. I recently had a requirement to find out how many times a comma occurred in a specified string. In hand code, that would look something like this:

! Declare some variables in the data section
StringPosition  Long
StartPosition   Long
Count           Long
! In the code section
Count = 0
StartPosition = 1
Loop
    StringPosition = Instring(',',theString,1,StartPosition)
    If StringPosition > 0
        Count += 1
        StartPosition = StringPosition + 1
    Else
        Break
    End
End

I haven't debugged that code so I don't know if it's exactly right. And that's kind of the point - I'd have to remember how to write the code, and I'd have to test it to make sure it worked. 

Here's how I solved the problem with the string class:

! In the data section
Str    DCL_System_String 
Count  Long
! In the code
str.Assign(theString)
Count = str.Count(',')

The object-oriented code has at least five huge advantages over the previous hand code:

  • It's much shorter
  • It took way less time to write
  • It's much easier to understand
  • It doesn't need testing because the class has already been tested
  • It's much easier to use

The string class illustrates what I think are the two main benefits of object-oriented code: testability and reusability. 

Testability

Most Clarion developers write code in embed points. 

 

Encapsulation

Briefly, encapsulation means that a class contains code and data. You may think hey, this is no big deal, everything I do is code and data. And you're not far off the mark. But there's more to this, as you'll see.

Inheritance

You've probably heard enough about OOP to have a bit of an idea of what inheritance is about already. Using inheritance you can create some code (a derived class) which automatically contains some existing code (a parent class). It's a tricky way of freeing you from retyping code when you want something that's almost like something you just did, but not quite. When you think of inheritance, think of code reuse.

Polymorphism

Polymorphism is something Clarion has had in one form or another since its inception. When you use OPEN on a file, or on a window, you're seeing a primitive form of polymorphism. Effectively you have what looks like one procedure (or in the case of classes, method) that operates differently depending on what parameter type it receives. Polymorphism in all its polymorphic glory is beyond the scope of this article and isn't critical to a basic understanding of Clarion OOP and ABC.

Composition

The fourth volume of this trilogy (apologies to Douglas Adams) is composition. Although not included in the classic threesome, composition is actually one of the most important features of OO programming as done by Clarion and many other languages. Composition lets you lump several (or many) classes together into a functional unit.

In my own work I find that composition is responsible for up to 80% of the code reuse in my applications. Object-based languages like VB which don't support inheritance have to rely entirely on composition for code reuse. Fortunately Clarion isn't hamstrung in this way, as AppGen-based development would be impossible or impractical without inheritance.

This handful of concepts and terms comes up repeatedly in any discussion of OOP. As I hope to demonstrate, a basic understanding of each is fairly easy to come by.

In order to understand how OO code works, however, you first need to see how it's structured.

Looking At ABC

ABC generated code is considerably more compact than procedural Clarion code. Here's the code that's used to run a typical ABC browse procedure:

CODE
      GlobalResponse = ThisWindow.Run()

Actually there's a little more to it than that. But if you look at your code statement, that's what you get. And on a simple form, you won't even find any source code for a Run() method! Clearly there's quite a lot going on under the hood. Although it isn't apparent from the generated code, the procedure is making heavy use of the ABC library.

As you probably know ABC stands for the Application Builder Classes, and so to understand ABC you need to understand classes.

The Class

The fundamental structure of all object oriented code is the class. A class is a sort of group structure which contains procedures as well as data. (In fact, you can use the Clarion deep assignment operator with classes just as you do with groups, though this is not generally recommended.)

In its simplest form, a class is also a bit like a Dynamic Linked Library (DLL). In a DLL, you have procedures, and you also often have some data that is shared by procedures inside the DLL. Similarly, a class typically contains procedures (usually called methods), and data. A class is much more flexible than a DLL, however, largely because of the mechanism of inheritance.

You will sometimes hear the terms object and class used interchangeably. To be accurate, the class is the declaration, and the object is an instance of the declaration. The distinction is important since you declare or define any class only once, but you can have multiple instances of the class in use. For example there is a popup class in ABC that handles browse context menus. There is only one definition of this class, but every browse you create will have its own instance of the popup class. It's not absolutely critical to differentiate between the two, however. Many developers use the term class when they really mean object, and occasionally the other way around. A lot depends on how high the level of "OO purity" is at your location. Then again an OO purist wouldn't be using a mixed language like Clarion.

Declaration and Code

Classes have two parts: the declaration, and the code. I always begin creating a class by writing the declaration.

I frequently need a means of adding some simple message logging to my application, for debugging purposes, particularly when writing code for complex processes. I find it easier to read a log of what happened when than to step through the debugger and try to remember afterward what order various pieces of code executed.

Listing 1 shows some source code which uses such a debugging object. In this example the debugging object is called db.

Listing 1. Code to create a trace log.
db.Trace('Before handling event ')
db.Trace('----- contents of listq -----------')
LOOP y = 1 to RECORDS(SELF.ListQ)
  GET(SELF.ListQ,y)
  GET(SELF.FieldColorRefQ,1)
  db.Trace(y & ' - markfield ' & SELF.MarkField & ', nfg ' |
   & SELF.FieldColorRefQ.Nfg & ', nbg ' |
   & SELF.FieldColorRefQ.Nbg & ', sfg ' |
   & SELF.FieldColorRefQ.Sfg & ', sbg ' & SELF.FieldColorRefQ.Nbg)
END
db.Trace('--------------------------------')
db.Trace('Keystate is ' & KeyState())
CurrSelection = SELF.ListBoxFEQ{PROPLIST:MouseDownRow}
IF BAND(Keystate(),0100h) then db.Trace('shift key held').

The log created by running the code might look like Listing 2.

Listing 2. An example trace log.
001 - Before handling event
001 ------------------- contents of listq -----------------------
001 - 1 - markfield 1, nfg 54234, nbg -1, sfg -1, sbg -1
001 - 2 - markfield 0, nfg -1, nbg -1, sfg -1, sbg -1
001 - 3 - markfield 0, nfg -1, nbg -1, sfg -1, sbg -1
001 -------------------------------------------------------------
001 - Keystate is 33024
001 - shift key held

In order to accommodate this functionality I need a class that is able to do at least two things: store trace messages, and display those messages on screen on demand (actually there are a lot of other useful features that could be added, but these two will do for a start).

I begin my debug class by creating the following class declaration:

DebugClass     CLASS,TYPE,MODULE('DEBUG.CLW')
TraceQ            QUEUE
Text                 STRING(200)
                  END
Trace             PROCEDURE(STRING)
ShowTrace         PROCEDURE
               END

Actually, this isn't going to work as written. Why not?

Anticipating Encapsulation

I've created a class structure that contains both the methods, and a queue to hold the data. One method adds the data to the queue, the other method displays the queue. This combination of code and data into a single structure is an example of encapsulation.

But the queue isn't going to work the way it's declared, because Clarion doesn't allow you to have a queue structure inside the class. That in itself isn't important - it's just a "feature" of the language - but what is important is the process you go through to get around this problem, because it's something that comes up time and again in Clarion OO programming, and in the ABC classes.

References

The way to get a queue into a class without declaring it in the class is to use a reference.

A reference is a sophisticated kind of pointer. A simple pointer points to a location in memory where something is located. The problem with pointers is they're just addresses - you can make a pointer point to anything at all, which can be quite dangerous. If you have what is supposed to be a pointer to a queue, only it points to a window structure, you're certainly not going to get the queue data you're expecting. Worse, what happens to your window when you write to what you think is a queue?

A reference can only point to a location that contains something of its own type. For instance, a reference to a string variable is declared like this:

StringRef &string

The & (ampersand) character indicates that this is a reference, and that StringRef can be made to point at any string variable. StringRef cannot be made to point at a byte variable, for instance. If you try, you'll get a compiler error.

To assign a reference you do the following

StringRef &= MyString

where MyString is a string variable. After you've done that assignment, you can treat StringRef exactly the same as MyString. StringRef is like an alias to MyString. Be sure to type

&=

and not just

=

or the reference assignment will not happen!

As I mentioned, you can't have a queue inside a class. You have to declare the queue outside the class, and then create a reference to the queue inside the class. Declare the queue outside the class, as in Listing 3. Then create a reference inside the class of the same type as the class.

Listing 3.  The class (and queue) declaration.

TraceQueue     QUEUE
Text              STRING(200)
               END


DebugClass     CLASS,TYPE,MODULE('DEBUG.CLW')
TraceQ            &TraceQueue
ShowTrace         PROCEDURE
Trace             PROCEDURE(STRING Text)
               END

Note the queue declaration. The reference type is the same as the label of the queue (TraceQueue), but with a & prepended.

If you've worked with OOP you might think Listing 3 is only a partial solution, and you'd be right. There's yet another wrinkle to this whole business of queues in classes, and you'll see shortly why it's important, and why it's a good example of one of the key aspects of OOP.

First, however, you'll need to write the Trace and ShowTrace methods.

Where Do I Put This Stuff?

One of the differences between procedural code and object-oriented code is that a block of procedural code that does one thing is normally contained almost entirely within a single source file. Classes, on the other hand, are typically contained in two source files.

Listing 3 shows the class declaration. By convention class declarations are kept in files ending in .inc, and you can create this one in a file called debug.inc. If you want this class to be available to all your applications, place it in the libsrc directory; otherwise put it in your current working directory. I usually put all generic classes (those which are not tied to a particular class) in the libsrc directory.

Create the class source in a separate file, called debug.clw. This file will contain the source code for the methods (procedures) which belong to the class.

This approach isn't actually all that different from procedural Clarion. In procedural code almost everything is contained in the procedure's module, but somewhere in your application a prototype for the procedure also has to be declared. The main difference is that in legacy applications procedure declarations are generated right into the main source file instead of into a separate INC file which is then INCLUDEd into the source (the ABC templates do take the INC approach with application procedures as well as class declarations).

Strictly speaking you don't have to have your class declaration and implementation in separate files, but it's generally a good idea. One reason is that by keeping your declaration separate from your implementation you can, if you wish, put the actual code in a DLL. Anyone who has the declaration can then use your classes, but they won't be able to see your source.

Listing 4 shows the debug class source.

Listing 4. The class source.

   MEMBER

   MAP
   END

   INCLUDE('DEBUG.INC')

DebugClass.ShowTrace         PROCEDURE

window WINDOW('Debug Messages'),AT(,,493,274),|
          FONT('MS Sans Serif',8,,),SYSTEM,GRAY,DOUBLE
       LIST,AT(5,5,483,246),USE(?List1),HVSCROLL,|
         FONT('Courier New',8,,FONT:regular),FROM(self.TraceQ)
       BUTTON('Close'),AT(230,255,,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)

All class methods are declared in the form classname.methodname, which is so that compiler knows to which class the method belongs. The two methods shown in Listing 4 are straightforward. The Trace method ads a record to the queue, and the ViewTrace method displays a window with a listbox. That listbox has as its FROM attribute the queue:

FROM(self.TraceQ)

Notice the use of the keyword SELF to refer to the current class. Any variable that belongs to the class (also known as a property) and any method that belongs to the class, must be written as

SELF.variable

or

SELF.methodname

Aside from making it easy to refer to the current object, SELF allows the compiler to differentiate between class data and global and method local data.

Testing the Trace Class

Create a small test application, or use one you have lying around. You can very quickly create an application by using Quick Start. This process is described in the Quick Start tutorial in the Getting Started booklet that comes with Clarion.

To test the trace class you need to let the application know about the declaration, and you have to provide the method code, either in source or compiled form. There are several possible approaches, and this is one of them:

1. Include the source file in the project. It's possible to have class source automatically compiled using the LINK directive, but in this case you'll need to choose Project|Edit, and in the External files list add debug.clw. If debug.clw is in the libsrc directory or the current working directory you won't need to supply the full pathname - just the file name will do.

2. Go to the application's global embed list and put the following code in a source embed in the After Global Includes embed point:

include('debug.inc')

Don't close that embed point yet! There's one other thing that you need to do here. Look back at the declaration and you'll see that the trace class has a TYPE attribute. That means that you can't use it directly. The compiler won't allocate any memory for any class (or queue, or any other data type, or even a method) which has a TYPE attribute. So why am I using this attribute?

I don't have to. But I always generally declare my classes with the TYPE attribute because it forces me to create a specific instance of a class. if I attempt to do this somewhere in my code:

DebugClass.Write('test')

I'll get the "Cannot use TYPEd structure in this way" error.

The easiest way to declare an instance of a class is as follows (you can type this just below the include('debug.inc') statement):

db DebugClass

Now you're ready to test the class. Assuming your test application has a browse procedure, go to that procedure embed list. You may find the following instructions easiest to follow if you first choose View|Contract All from the menu.

Under local objects choose ThisWindow and expand its embed list. Under WindowManager locate the TakeEvent method. Expand the embed list and double-click on the CODE entry. The Select Embed Type window appears, as shown in Figure 1.

Figure 1. The Select Embed Type window.

abcoop_fig1.gif (9287 bytes)

Choose Source, and in the resulting source embed type

db.Trace('Event ' &
    EVENT())

Save your changes. Go to the main menu and add a new menu item. Call it Show Trace and on the Actions tab do NOT enter a procedure name. Instead click on Embeds, and on the Accepted event generated code embed point add a source embed with the following code:

db.ShowTrace()

Save your changes and run the application. If you've followed instructions very carefully, as soon as you either run the browse or click on Show Trace your application will...blow up.

What's This GPF Doing Here?

I'm not in the habit of publishing code that GPFs, and the only reason I'm doing it now is because this is going to happen to you sooner or later, so you might as well get it out of the way.

The problem is that the queue reference (TraceQ) hasn't been initialized to anything. It has a value of NULL, and if you attempt to use a NULL reference, Windows will report a GPF. So you have to initialize the reference.

Constructors and Destructors

Many, if not most, classes have to do some kind of data initialization or object creation. The proper way to handle this is to set aside a class method to take care of this, and often another method to take care of cleaning up the class after you're done with it.

These methods are called constructors and destructors, respectively. And there are two schools of thought on how these methods should be handled. One school of thought (let's call this the DAB school) says these methods should be like any other methods, and you the intelligent and methodical programmer will call them as needed. Typically constructors have a name like Init() and destructors have a name like Kill().

The other school of thought (call this the great unwashed who are not of the DAB school, school) says constructors and destructors should be called automatically when the object is created or destroyed.

You can do either in Clarion, because DAB buckled under the pressure and added automatic constructors. For now, go automatic.

The Construct and Destruct Methods

The constructor's job is to initialize the queue reference so the trace() method will have a real queue to work with. Remember that when you assign a reference to an object, you treat the reference the same way you would the actual object.

DebugClass.Construct         PROCEDURE
   CODE
   SELF.TraceQ &= TraceQueue

When you use a constructor you almost always want to use a destructor as well. In this case all the destructor needs to do is free the queue.

DebugClass.Destruct          PROCEDURE
   CODE
   FREE(SELF.TraceQ)

Add these two methods to the debug.clw source file. You'll also need to add the method prototypes inside the class definition in debug.inc:

Construct         PROCEDURE
Destruct          PROCEDURE

Save your changes and compile and run the application. Open the browse and then from the main menu choose the Show Trace option you added. A window similar to the one shown in Figure 2 appears.

Figure 2. The trace window displayed by ShowTrace().

abcoop_fig2.gif (4679 bytes)

That's how easy it is to use a custom class in your application!

Achieving Encapsulation

There's still one significant problem with this class as declared.

The queue isn't actually part of the class, but is declared outside the class. Whenever possible, you want your classes to be completely self-contained. That's encapsulation - code and data together in a single object.

Since there's only one debug object the current situation isn't necessarily a problem. But what happens if you have two debug objects?

dbg1       DebugClass
dbg2        DebugClass

Both instances of DebugClass will use the same queue, and that's not normally a good idea. You want any object in your system to be as self-contained as possible. The way to do this is to put the type attribute on the queue, and declare an instance of it inside the class, using the NEW operator.

DebugClass.Construct         PROCEDURE
   CODE
   SELF.TraceQ &= NEW(TraceQueue)

Since the destructor's job is to clean up anything which the constructor (or any other method) may have created (read allocated memory for), you should now use it to dispose of the queue when the class is finished. If you don't DISPOSE() what you NEW() you'll end up with a memory leak.

DebugClass.Destruct          PROCEDURE
   CODE
   FREE(SELF.TraceQ)
   DISPOSE(SELF.TraceQ)

NEW is, well, new to most Clarion programmers. In procedural Clarion, the runtime system takes care of memory allocation automagically. The only time you actually allocate or deallocate memory is when you add records to, or delete records from, a queue. In the world of OOP, however, objects are being created and destroyed all of the time. It's possible you may never do this explicitly in your ABC-related code, but ABC does it lots, and it's part of the power of OOP.

As TraceQ demonstrates, references have a dual purpose. They can point to an existing object which is declared elsewhere, in which case they function like an alias, or they can be used to hold an object just created, in which case they may be the only reference to the object.

Just as the debug class created the queue on the fly, you can also create the debug object on the fly, if you wish. Instead of declaring your debug object like this

db DebugClass

declare it like this

db &DebugClass

By using the ampersand you're specifying that this is a reference rather than an instantiated object. Before you can use the object in your code, you'll need to create an instance like this:

db &= NEW(DebugClass)

and when you're done with the object you need to free up the memory using DISPOSE:

DISPOSE(db)

You should always DISPOSE anything you NEW or you'll end up with a memory leak.

So what's the difference? In this case, there is no practical difference. But there may be times in your programming when you want to create objects on the fly (you can have a queue of objects, for instance). And sometimes you just want explicit control over when an object is created and when it is destroyed.

The ABC Angle

The ability to create and dispose of objects, and assign references to those objects, is one of the keys to the ABC class library. ABC is continually creating and disposing objects, and working with object references.

Now as useful as all of this is, if you didn't have anything more, well, you'd really just have something like Visual Basic. And there's a lot more to Clarion and ABC. In the next article in this series I'll look at inheritance, another of the keys to understanding the ABCs of Clarion OOP.

  • No labels