OOP in Clarion 4

by Mike Hanson

Published 1997-07-01    Printer-friendly version

Download the code here

For many years you've been hearing the hype about object oriented programming. OOP is supposed to make your programs smaller, and easier to write and maintain. Of course, we all know that Clarion programmers are already more productive than other developers, so what's the big deal?

Well, we were given a small taste of OOP with the CW 2.0 language. However, very few of us looked into these new features, because the templates did most of our work and they weren't using OOP. When we did write code, it was little snippets that wouldn't benefit from OOP. Also, Clarion's implementation was missing several key elements that made it difficult to fully apply the ideology.

Well, Clarion 4 has made up for these shortcomings. First, they filled the holes in the implementation. Next they rewrote sections of the template as a class library, and changed the templates to use those. Like it or not, these changes are going to drag us kicking and screaming into the world of object oriented programming.

What is OOP?

In a nutshell, object oriented programming is different from procedure oriented programming in one specific way: the data and procedures are joined into objects. For example, in procedure oriented programming (POP), you would have some data (either data files or memory variables). You would also have some procedures and functions that processed this data. There wasn't, however, any specified connection between the two. It was up to the developer to make sure he kept his application well designed.

From a maintenance perspective POP could be a nightmare. Data was strewn all over, with no sense of what would happen to it. And a given procedure could be accessing any data in the system. Ultimately, POP is naturally disorganized. In contrast, with OOP your data and the methods that operate on that data are declared in the same place. You know what accesses what and where. All the guesswork is gone.

What is an object? We can think of a "Person" object as everything about a person, and everything that can happen to that person. This grouping of data and methods is called Encapsulation. A "Person" is just an abstract concept that we understand at a high level without worrying about the low level details. The details and complexity are hidden within the object, which is know as Abstraction.

Next comes a crucial OOP concept: Inheritance. As you know, many different entities have similar properties. For example, a person has a name and address. A business is just a person with a business name. A supplier is just a business with some associated products. Why don't we just declare the root object, then have each new object borrow all of the attributes and functionality from the inherited object. At each level we'll add some attributes (data) and operations (procedures and functions), which make the object unique.

Sometimes newly inherited objects require a modified version of a function or procedure. This can be done by Overloading. A function that has already been defined is redefined. If it has the same name but different parameters, then both will be available. Depending on which parameters are passed, one function or the other will be called. This is called Polymorphism. Clarion has always used polymorphism in its own commands, as in SET(File) vs. SET(Key).

If a function has the same parameters but is declared at a different inheritance level, one or the other will be called, depending on the type of object being used or Instantiated. If the original object is being utilized, then the original procedure will be called. If the new object is utilized, then the new procedure will be called. In other words: "Do this, until I tell you to something else, then do that, until..."

Sometimes these overloaded functions will call each other. For example, you have anAnimal object, which is inherited by theMammal object, which is inherited by theHuman object. At each level we'll have aDescribe() method. Each level uses its parent's description first, followed by its own. This way the description of a human automatically includes that of an animal and a mammal.

There is a specific type of overloaded method called a Virtual. We'll describe this in more depth later.

Clarion's version of OOP

Now that you have a basic understanding of the OOP concepts and common buzzwords, let's look at how Clarion implements them at the language level. First, objects are classes. Here is the simplest possible class:

Useless         CLASS
              END

This class has no data or methods, which is why it's useless. Let's look at a more practical example:

Animal          CLASS
Name            STRING(30)
Describe        FUNCTION,STRING
              END

This object has data (a Name) and a method (return your Description). We could use this class in the following program:

!OOP1.PRJ
  PROGRAM
  MAP.

Animal       CLASS,TYPE
Name           STRING(30)
Init           PROCEDURE(STRING N)
Describe       FUNCTION,STRING
             END

MyAnimal LIKE(Animal)

  CODE
  MyAnimal.Init('Bacteria')
  MESSAGE(MyAnimal.Describe())

Animal.Init PROCEDURE(STRING N)
  CODE
  SELF.Name = N

Animal.Describe FUNCTION
  CODE
  RETURN('The animal is a ' & CLIP(SELF.Name) & '.')

This looks like a lot of work to display a simple message, but let's take a look at it. First you should notice theAnimal class declaration. It says that each animal will have a name, and it can be initialized and described. Notice theTYPE keyword. This indicates that it is only a description of the class, and not an actual occurrence or "instance" or "instantiation". The instance is declared as MyObject, which copies the definition from theAnimal class. Instead of usingLIKE(Animal), you could do this:

MyAnimal  Animal

Next we initialize our object with the animal's name. Notice the "dot" notation. It's alwaysObject.Variable orObject.Procedure. This is different from what we're used to with Clarion usingPre:Field. Finally, we display the animal's description in a message box.

At the bottom are the definitions for the procedures and functions that we are declaring in theCLASS structures above. TheInit procedure simply sets the name. Notice the "Animal." preceding the procedure name. You could have also used:

Init PROCEDURE(Animal)

Regardless of the syntax that you choose, the first parameter is always a hidden reference to the class. Remember this if you use theOMIT() function.

Take note of the "SELF." keyword. Any time you want to refer to data and procedures within yourself, you need to do it in this way. This enables the compiler to distinguish the scope of your variable.

At this point you're probably shaking your head and saying that OOP is more difficult than it's purported to be. However, the base classes are usually defined separately from your program. All you need to do is use them. That means the program could be as simple as:

  PROGRAM
  MAP.
  INCLUDE('ANIMAL.INC')

MyAnimal    Animal

  CODE
  MyAnimal.Init('Bacteria')
  MESSAGE(MyAnimal.Describe())

All of the complexity is hidden in the class library. Now let's look at simple inheritance:

!OOP2.PRJ
  PROGRAM
  MAP.
Animal       CLASS,TYPE
Name           STRING(30)
Init           PROCEDURE(STRING N)
Describe       FUNCTION,STRING
             END

Mammal       CLASS(Animal),TYPE
Legs           BYTE
Arms           BYTE
Init           PROCEDURE(STRING N, BYTE L, BYTE A=0)
Describe       FUNCTION,STRING
             END

MyAnimal     Animal

MyMammal     Mammal

  CODE
  MyAnimal.Init('Bacteria')
  MyMammal.Init('Dog', 4)
  MESSAGE(MyAnimal.Describe())
  MESSAGE(MyMammal.Describe())

Animal.Init PROCEDURE(STRING N)
CODE
  SELF.Name = N

Animal.Describe FUNCTION
  CODE
  RETURN('The animal is a ' & CLIP(SELF.Name) & '.')

Mammal.Init PROCEDURE(STRING N, BYTE L, BYTE A=0)
  CODE
  PARENT.Init(N)
  SELF.Legs = L
  SELF.Arms = A

Mammal.Describe FUNCTION
  CODE
  RETURN(PARENT.Describe() & '|' & 'It has ' & SELF.Legs & ' legs and ' & SELF.Arms & ' arms.')

Now our object has the benefits of theAnimal base class and the additional features of theMammal inherited class. Notice that both classes contain aDescribe function. If we are instantiating anAnimal, it will call theAnimal.Describe() function. If, instead, we are instantiating aMammal, it will call theMammal.Describe() function.

Notice that theMammal.Describe() function callsPARENT.Describe(). It knows that it's not the base class, so it borrows what it can from its ancestors before doing its own thing. This is where the benefits of OOP start becoming apparent.

For comparison, let's see how the program might have looked with non-OOP methods:

!POP.PRJ
  PROGRAM

Animal     GROUP,TYPE
Name         STRING(30)
           END

Mammal     GROUP,TYPE
Name         STRING(30)
Legs         BYTE
Arms         BYTE
           END

MyAnimal   LIKE(Animal)

MyMammal   LIKE(Mammal)

  MAP
    InitAnimal(*Animal A, STRING N)
    DescribeAnimal(*Animal A),STRING
    InitMammal(*Mammal M, STRING N, BYTE L, BYTE A=0)
    DescribeMammal(*Mammal M),STRING
  END

  CODE
  InitAnimal(MyAnimal, 'Bacteria')
  InitMammal(MyMammal, 'Dog', 4)
  MESSAGE(DescribeAnimal(MyAnimal))
  MESSAGE(DescribeMammal(MyMammal))

InitAnimal PROCEDURE(*Animal A, STRING N)
  CODE
  A.Name = N

DescribeAnimal FUNCTION(*Animal A)
  CODE
  RETURN('The animal is a ' & CLIP(A.Name) & '.')

InitMammal PROCEDURE(*Mammal M, STRING N, BYTE L, BYTE A=0)
  CODE
  M.Name = N
  M.Legs = L
  M.Arms = A

DescribeMammal FUNCTION(*Mammal M)
  CODE
  RETURN('The animal is a ' & CLIP(M.Name) & '.¦' & 'It has ' & M.Legs & ' legs and ' & M.Arms & ' arms.')

Notice how theMammal group has to redefine theName variable, because it is not inheriting theAnimal class. Also notice that each procedure name in theMAP must be unique. (e.g.; We have to rememberInitMammal, instead of justInit.) Also, the object being operated on must be passed to the procedure or function as a parameter. This is automatically done for you with OOP.

Notice howInitMammal must assign the name itself, rather than being able to depend onInitAnimal for all common elements. The same applies toDescribeMammal(), which must repeat the same formula thatDescribeAnimal() has already defined. At this point, you should see how the object oriented code is cleaner.

OOP in Clarion versus C++

Many of you may be familiar with OOP programming under C++. There are many similarities to Clarion, and a few differences. Lets start with the following table:

C++ Clarion
class CLASS
new NEW
delete DISPOSE
private PROTECTED / PRIVATE
public (implicit, unless PRIVATE is specified)
friend PRIVATE
this SELF, PARENT
static (define as module data in class module)
#define EQUATE
typedef TYPE
const -enum EQUATE + TYPE
union OVER

Of course not all elements are directly transferable. There are also OOP features in C++ (like multiple inheritance) that Clarion does not have. For the most part, however, these missing features are somewhat esoteric and are not necessary for normal OOP work with Clarion.

A couple of the items above deserve extra explanation. The private, public and friend access rights are quite different. In C++ if something is "private" then nothing but the class itself can access it, including derived classes. Clarion does not have an equivalent to this. It does have thePROTECTED access, which means that only the class and its derived classes have access. This is similar to "friend" in C++, except that Clarion won't allow an explicit friend relationship if they are not children. In C++ if something is "public" then everything can access it everywhere where it is in scope, which is the default for Clarion. If something is marked asPRIVATE in Clarion, then only procedures in the class' module can access it. There is no equivalent to this in C++. The bottom line for Clarion is this:

Keyword Access

PROTECTED Current and derived classes
PRIVATE Current and derived classes, and other functions in the same module
<nothing> Anyone with the same scope

Clarion has constructors and destructors. They cannot take parameters like C++, though. This, of course, means that you cannot have overloaded (multiple based upon parameter lists) constructors and destructors. TopSpeed suggests that you don't use these automatic constructors and destructors, and instead create your own Init and Kill functions that you explicitly call at the appropriate time. Based upon my own experience, I agree with this strategy.

The enum data type in C++ is really just a fancy method for #defining macros and typedefs concurrently. This can be simulated with CW using a structure with the TYPE attribute, plus a series ofITEMIZEdEQUATEs.

C++
enum ignition_parts { distributor = 1, cap,
points, plug, condenser, coil, wires, done }
Clarion
Ignition_Part   SHORT,TYPE
Ignition_Parts  EQUATE(SHORT) !Alternative
Ignition_Parts1 ITEMIZE,PRE(IP)

Distributor     EQUATE(1)
Cap             EQUATE
Points          EQUATE
Plug            EQUATE
Condenser       EQUATE
Coil            EQUATE
Wires           EQUATE
Done            EQUATE

Your program might look like this:

  PROGRAM
  MAP.

MyPart Ignition_Parts

  CODE
  MyPart = IP:Distrubutor

A Clarion CLASS must have all its functions and procedures defined in the same module. Although this doesn't leave must flexibility for organization, it's normally not a big problem. As with all OOP programming, you should strive to make your classes as small as possible.

Clarion's OOP Usage Today

If you are new to OOP, I expect that your head is swimming after these last two sections. You are probably thinking that this has nothing to do with creating menus, browses and forms for your database applications. Well ... you're right!

There's a very important saying: "Use the right tool for the right job". OOP is great when it's working on objects. However, customer update windows and invoice reports are not objects: they are business oriented tasks. Sure they're simply procedures working with data, but that doesn't mean they should be object oriented. Sure, we could force ourselves to do it that way, but the learning curve would be enormous, and the benefits might not be enough to warrant the change.

Instead, why don't we take a smaller step and make all of our interface widgets into objects. At the high level you're creating a Browse window. That's the important part. To accomplish this, this window will need a browsing list box. You could have the template write all of the code into the procedure every time you use it (the CW 2.0 method), or you could have a Browse object that is called by many browses and houses its code in a separate module (the Clarion 4 method). The development environment will look essentially the same, but the generated code will be far more efficient.

Clarion 4 has done a wonderful job of implementing this approach. If you are cruising around and looking at your windows and templates in AppGen, it doesn't look that much different from 2.0. If you look at the generated module, however, you will find that it's much smaller (e.g.: browse procedures are sometimes 75% smaller). When you have 100 or more browses in an application, these savings can really add up. Normally the overall savings for an average APP are around 60%.

The benefit for you is that you don't need to learn everything about OOP to start using Clarion 4 today, since the IDE isn't much different. You can get comfortable with the concepts, look at the generated code, and learn new techniques as they're required.

There are two elements of OOP that Clarion 4 uses that I haven't really mentioned yet. They will have a large impact on how you work, though. The first isn't really OOP-specific, but it's used extensively in the new ABC templates that come with Clarion 4. A Reference variable is a handle to another variable, and it can be treated almost like the original variable itself. For example, a reference is used by the Browse class to access the View structure that was generated into your browse procedure. In that case, it would be an &VIEW reference. The View itself is passed into the object during the initialization, and it handles access from there. Sometimes the object won't know the type beforehand, so it uses an &ANY reference. This is a handle to any and all variable types. There is only one situation where you need to understand these, which brings us to the more significant element: Virtual Functions.

For some reason, this is one of the hardest concepts for most people to grasp when they are learning OOP. Let's consider our animal example from before. We've decided to add the option to destroy animals. We have three methods at our disposal: drowning, freezing and burning. (I'm sorry if this seems a little gruesome for any of you.) TheAnimal class contains theDestroy procedure, and it needs to check to see which of these methods will work on the animal before proceeding. TheAnimal base class doesn't know anything about the inherited classes, so it provides a Virtual function. It's normally an empty function in the base class. In our cases, nothing will destroy a base class animal.

Let's inherit a new class calledBacteria. It can be burned, but cannot be drowned or frozen. It creates a new version of theCanBurn() virtual function saying, "Yes, I can be burned". When theAnimal base class needs to destroy the animal, it automatically says "At this point I'm a Bacteria, so can I be burned?" It does the same for theMammal class, which can be drowned, frozen and burned (and has three virtual functions to prove it). Essentially, a virtual function allows you to override something that the base class does directly. The example for this is in OOP3.PRJ.

This technique is used in Clarion 4 with the Filter embed on Browses and Reports. Just in case some extra code needs to be called as a filter, theFileManager base class calls an empty, virtualValidateRecord() function, which always returnsRecord:Ok. If a browse or report wants to override this, it just inherits the base class and creates another virtual function with an alternative bunch of code. When theFileManager class callsValidateRecord(), it calls the new one, not the empty one. This works regardless of the number of inheritance levels.

Now, back to references... CW 2.0 provided aValidateRecord routine, which could be used with code like this:

IF NOT (the thing I want)
  EXIT
END

The expression could reference any global or local variables in the procedure. In Clarion 4 your code is in a procedure separate from the main procedure. If there are local variables from your own procedure that you need to access in theValidateRecord() virtual function, you need to set them up as a Local Variable Reference. This is done on the Extension's "Classes" tab. After you have done this, prefix your variable name in your code with "SELF.". For example, your CW 2.0 code might have looked like this:

IF Inv:Date <> Loc:Date
  EXIT
END

In C4 it would look like this:

IF Inv:Date <> SELF.Loc:Date
  RETURN(Record:Filtered)
END

You'll also notice that we'reRETURNing a value rather thanEXITing. Again, we are no longer in aROUTINE, but rather aFUNCTION. Clarion has done a good job of describing how to do this conversion without too many headaches. If their examples don't cover your situation, then you are probably a non-beginner and will be able to figure it out without too much trouble.

Conclusion

OOP is not something you can learn in a day. It's like a magic wand that can do miracles, but you have to learn the skills and practice using it before you see appreciable results. Fortunately, Clarion 4 comes to the rescue giving us real benefits from OOP, while allowing us to learn the concepts at our own pace. Don't be afraid to look behind the curtain ... you'll be impressed with what you see.

Printer-friendly version

 
 

Search

 

Advanced Search
Topical Index

Related Articles

Subscribe to
ClarionMag

One year: $189

(includes all back issues since '99)

Renewals from $139

Two years: $289

Renewals from $239

More Info

Subscribe Now!

ClarionMag Blog

RSS Feeds

Updates via Email

Enter your Email


Powered by FeedBlitz

Quick Links