Object-oriented programming in Clarion - the basics

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

One way to get started with OOP is the DevRoadmaps Clarion Library (DCL), our open source repository of Clarion code. There's a lot of terrifically useful code in the DCL, and you'll need some basic OOP concepts under your belt to make full use. 

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. So do procedures, of course. But there's a key difference: in a class you can have data at the method (procedure) level, and you can also have data at the class level.

Classes: like DLLs, only way better

In that respect classes are a lot like small DLLs - they are their own little bundles of procedures with (optionally) shared data, and that shared data can be public or private. 

So why not just use DLLs instead of classes? Because DLLs really can't do all that much; they only give you a tiny fraction of what a class can do. 

So what can you do with classes that you can't do with DLLs? Here are a few things:

  • 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

Yes, you can sort of do some of these things with DLLs. Virtual methods are somewhat like callback procedures. And you can use TYPEd procedures to do something almost like interfaces. 

But after you've been doing OOP for a while you'll find that those procedural techniques are shadows of what you can do with objects. There's a reason the programming world has so fully embraced OOP (which is about half a century old now):  objects are incredibly flexible and useful. 

OOP: Not the end of programming

 As good as object-oriented programming is, it isn't the solution to every problem. For instance, in recent years there's been a surge in interest in aspect-oriented programming, which addresses some of OOP's shortcomings. AOP isn't a replacement for OOP, however; it's generally used in conjunction with OOP.

Creating an instance of a class

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. 

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. 

An example

Here's some sample code that checks for a string that ends with the string 'xyz':

AProcedureThatDoesSomething procedure(string s)
 
str DCL_System_String                ! Create an instance of the string class


  CODE
  str.Assign(s)                      ! Assign the passed value
  if str.EndsWith('xyz')
    ! do something
  else
    ! do something else
  end

How would you go about writing the EndsWith code? Here's how the class does it:

DCL_System_String.EndsWith      procedure(string s)!,byte
thislength                      long
otherlength                     long
    CODE
    s = upper(clip(s))
    IF NOT Self.Value &= NULL
        if s = ''
            return FALSE
        end
        thislength = len(clip(self.value))
        otherlength = len(s)
        if otherlength > thislength
            return FALSE
        end
        if SUB(upper(self.value),thislength-otherlength+1,otherlength) = s
            return TRUE
        end         
    END
    return false

That really isn't code you'd want to embed somewhere in your app, at least not if you figured on using it more than once. 

You could put it in a function library - although the above code assumes the Self.Value property has been set somewhere, there's no particular reason you couldn't also pass in the string. 

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:

! 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 using the string class's Count method:

! 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. That is, after all, the point of embeds, right? 

Maybe that's was once the case, but not any more. 

The point of embeds is to give you a place to hook in your code. They are absolutely not there to contain your code. At least not the vast majority of your code. 

In particular, when you put business logic inside an embed point, 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 truly awful way to test business logic. 

On the other hand, business logic in classes can be tested much more easily. I use ClarionTest for this (included in the DCL) and I'll have some docs up soon under the DCL page

Reusability

Embed code isn't reusable - it just sits at that embed point. (Okay, if your embed code is a routine or a local class, it's reusable within that procedure. But not elsewhere.)

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

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:

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

! 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 (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, but the implementation can be either source or binary code (e.g. a standalone LIB or LIB+DLL). 

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. 

 

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