![]() |
|
Published 1997-07-01 Printer-friendly version
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.
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.
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.
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:
| 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,
|
| 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.
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.
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.
Copyright © 1999-2008 by CoveComm Inc. All Rights Reserved. Reproduction in any form without the express written consent of CoveComm Inc., except as described in the subscription agreement, is prohibited.
Clarion Magazine ISSN 1718-9942
One year: $189
(includes all back issues since '99)
Renewals from $139
Two years: $289
Renewals from $239