Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

OOP really isn't that difficult. Once you learn the basics fundamentals 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.

...

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):

...

As you can probably tell by looking at the code, classes (usually) contain both code and data. So do procedures, of course. But there's a key difference here: in a class you can have data at the method (procedure) level, and you can also have data at the class level.

...

  • Create classes that contain other classes (composition)
  • Supplement existing classes with your own code (inheritance)
  • Insert your own code in place of existing class code (virtual methods)
  • Create plugin architectures (interfaces)
  • Take a component- or building block-based approach to software development

...

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 on that 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 is the case with any procedure call - procedure data, unless marked as static, only exists for the duration of the call). As soon as the Assign method completes, the stringLength variable goes away. 

...

But for something like a string class, having all the methods in one place has a number of advantages:

  • A class makes it easier to do multiple operations on the same string

...

  • .
  • Code completion shows you all of the available methods for that object so you don't have to go hunting through the docs.
  • When you need to add some new functionality, you know exactly where to go: the class definition. And

...

  • your changes are automatically available anywhere that class is used. 

Here's another example. 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:

...

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

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

...

In particular, when you put business logic inside an embed point, then the only way you can test that logic is to execute the program up to the point where your logic is exercised. And usually that means someone has to actually run the program, navigate to a particular screen, and probably enter some data and click a button. 

That is a terrible truly awful way to test business logic. 

...

Classes, however, can be reused in all kinds of interesting ways, and often in ways you never imagined when you first wrote the class 

Info
titleThe classic bits

 No discussion of OOP is complete without at least a mention of the following terms:

Encapsulation

Briefly, encapsulation means that a class contains code and data.

Inheritance

With 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.

Composition

Composition lets you lump several (or many) classes together into a functional unit.

In my own work I find that composition is responsible for the vast majority of the code reuse in my applications. I do use inheritance at times, and virtual methods are often a vital part of that strategy. But composition is my bread and butter.

 

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)Image Removed

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)Image Removed

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 OOPclass. 

Instantiating classes

Before you can use a class you have to create an instance of the class. This is called instantiation (creating an instance) and it can happen automatically or under program control.

The previous examples both used automatic instantiation, like this:

Code Block
! In the data section
Str    DCL_System_String 

By declaring a variable (Str) with the class's type (DCL_System_String) you'll get a Str object that's created automatically when this code comes into scope. If you declare Str at the global level then it will be created when the program runs and will exist until the program terminates. If instead you declare Str in, say, a procedure's data section then Str will be created when the procedure is called and will be cleaned up when the procedure terminates. 

You can also declare classes this way:

Code Block
! In the data section
Str    &DCL_System_String 

Note the & in front of the DCL_System_String type. That indicates that Str is a reference to an instance of DCL_System_String. A reference is like a pointer, except it can only point to something of the specified type. 

If you attempt to use a reference before anything has been assigned your program will GPF. So you need to create an instance with the NEW operator. There are two forms:

Str &= new(DCL_System_String)

and

Str &= new DCL_System_String

Both accomplish the same thing in exactly the same way. 

Once you're done with any explicitly created object you need to clean it up with Dispose; failure to do so will result in a memory leak, your program will hold onto that memory but will be unable to use it. 

Dispose(str)

Unlike New, Dispose has only one form. 

Locating the class header

Before the compiler can use any class declaration, it has to be able to find the declaration. 

Clarion classes, at least those designed for reuse, are usually contained in a pair of files, one with the .INC extension and the other with the .CLW extension. The .INC file contains the class declaration (see the above example); the .CLW file contains the actual methods. 

Separating the class header I(the declaration) from the implementation makes it easier to distribute classes in binary form, either for convenience or to protect intellectual property. To use a class you always need the class declaration in source form. 

Using classes

You now have enough information to start using classes others have created. Feel free to download the DevRoadmaps Clarion Library and try out some of the classes. Probably the easiest place to start is with the DCL_System_String class. 

 

Info
titleThe classic bits

 No discussion of OOP is complete without at least a mention of the following terms:

Encapsulation

Briefly, encapsulation means that a class contains code and data.

Inheritance

With 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.

Composition

Composition lets you lump several (or many) classes together into a functional unit.

In my own work I find that composition is responsible for the vast majority of the code reuse in my applications. I do use inheritance at times, and virtual methods are often a vital part of that strategy. But composition is my bread and butter.