The ABCs of OOP - Part 2

by Dave Harms

Published 1999-05-17    Printer-friendly version

In the first article in this series I reviewed the basic concepts of object-oriented programming, including inheritance, encapsulation, polymorphism, and composition. In this article I'll focus on inheritance and encapsulation, which will lay a foundation for the critical area of virtual methods.

Inheritance is a terrific idea, and not just if you have a rich uncle. The more code you've written in your life, the more times you've probably written the same code over and over, only slightly differently each time. Clarion has traditionally used its template technology to take care of this problem, generating oft-used code for you and tailoring it to your needs. This doesn't exactly reduce the code being created, but it does make the programmer's job a bit easier.

Inheritance uses a different approach than the templates to code reuse (although in Clarion the ABC templates and classes both contribute vital pieces of the solution). Rather than rewriting the code with your changes, inheritance allows you to selectively replace or supplement the original code.

A Derived Class

Previously I demonstrated a small class which could be used to assist in debugging applications. Listing 1 shows the class declaration and Listing 2 the class implementation.

Listing 1. The DebugClass declaration (DEBUG.INC).
TraceQueue     QUEUE,TYPE
Text              STRING(200)
               END

DebugClass     CLASS,TYPE,MODULE('DEBUG.CLW')
TraceQ            &TraceQueue
Construct         PROCEDURE
Destruct          PROCEDURE
ShowTrace         PROCEDURE
Trace             PROCEDURE(STRING Text)
               END
Listing 2. The DebugClass implementation (DEBUG.CLW).
MEMBER

   MAP
   END

   INCLUDE('DEBUG.INC')

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

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

DebugClass.ShowTrace         PROCEDURE

window WINDOW('Debug Messages'),AT(,,331,206),|
     FONT('MS Sans Serif',8,,,CHARSET:ANSI),SYSTEM,GRAY,DOUBLE
       LIST,AT(5,5,320,180),USE(?List1),HVSCROLL,|
     FONT('Courier New',8,,FONT:regular,CHARSET:ANSI),|
     FROM(self.TraceQ)
       BUTTON('Close'),AT(150,190,,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)

This class can also be diagrammed as shown in Figure 1.

Figure 1. The DebugClass class diagram.

abcoop_fig1.gif (2015 bytes)

The class notation used in Figure 1 is fairly standard. The class is drawn as a box with the top area containing the name. Below it are two optional areas. The first holds properties (in this case the queue called TraceQ) and below that are the methods. I'll be using this kind of class diagram in explaining inheritance.

Let's say you've been using the DebugClass to create a trace log of messages while debugging one of your applications. You now decide that you want to modify the format of the trace log to display a timestamp at the beginning of each line. One possibility is to add FORMAT(CLOCK(),@T4) & ' ' to the beginning of the line of text. But if you make that change to DebugClass then you've altered your original code, and there still may be times when you don't want the timestamp.

If you sometimes want a feature in your code and other times don't, procedural code gives you two options. One is to add a switch to the code so you can turn the feature on and off. The other is to make a complete copy of the code, then change the copy. The latter is a fairly ugly solution to this problem but when you're dealing with more complex logic it's sometimes the only solution.

In OOP you can solve this problem with inheritance. Simply create a class derived from DebugClass and replace the Trace method with the one you want to use. Listing 3 shows a declaration for a derived DebugTimedClass, and Listing 4 shows the method source (or one possible version thereof).

Listing 3. The DebugTimedClass declaration (DEBUGTIMED.INC).
   include('debug.inc')


DebugTimedClass   CLASS(DebugClass),TYPE,MODULE('DEBUGTIMED.CLW')
Trace                PROCEDURE(STRING Text)
                  END

DebugTimedClass is now the child of DebugClass, and DebugClass is the parent of DebugTimedClass. Children are derived from their parents (you knew that).

Note that the form of the class declaration has now changed. Whereas DebugClass had a CLASS attribute with no parameters, meaning it was a base class, DebugTimedClass has the CLASS(DebugClass) attribute. This means that DebugTimedClass is identical to DebugClass except where DebugTimedClass contains replacement or new methods. In this case you can see that there is a Trace method which has the identical declaration to that in DebugClass. The result is that DebugTimedClass will be have exactly as if you had created it by making a copy of the DebugClass declaration and implementation (.INC and .CLW files) and replacing the Trace method.

As with DebugClass, the declaration is kept in an include file, and that file also needs an INCLUDE statement that points back to DEBUG.INC so that the compiler knows where to find the parent class definition.

A method in a derived class which has the same name and parameter list as a method in the parent class is said to override the parent method. Overriding is just an OOPish term for replacing.

NOTE: Although you can override methods in Clarion you cannot override data. Variables and structures declared in a derived class may not have the same name as those in the parent class.

DebugTimedClass's MODULE statement is also different from that of DebugClass: it points to the DebugTimed.CLW module which contains the source for the new Trace method. (You can have multiple classes in the same module if you wish, though this raises some scoping issues. More on that later.)

Inheriting code is considerably more efficient than copying code because it avoids source duplication, leaving you only one place where you need to modify/maintain any given block of source code. It can, however, make the code slightly more difficult to read, in that you need to know what code is in the parent class. A class browser can be a big help when you're working with derived classes.

Listing 4 shows a possible implementation of the new Trace method.

Listing 4. A Trace method with a timestamp.
DebugTimedClass.Trace      procedure(string Text)
   code
   self.TraceQ.Text = clock() & ' ' & Text
   add(self.TraceQ)

The Trace method in Listing 4 will work, but it isn't as efficient as it could be. It's an almost complete duplication of the original Trace method except that it adds the timestamp to the log string, so if the name or structure of TraceQ should change, you'd have to change the code in the derived class as well. Listing 4 also assumes that TraceQ, which is a property of the parent class, can be accessed by the derived class, and there may be good reasons why this shouldn't be allowed to happen.

It so happens that in this case TraceQ is public data and can be accessed by the derived class, so there won't be a problem. But often a class will restrict access to some of its data so that it can guarantee the state of that data.

Restricting Data Access

One of the goals you should be aiming for in OOP is to make each class as self-contained (or encapsulated) as possible. That means that not only does the class have everything it needs to function (it doesn't rely on global data, for instance), it also means that outside forces can't make the class do something it shouldn't do. And usually the way you force a class to do something it shouldn't do is by changing some of its data (from outside the class) to values that the class can't properly deal with.

Some class data can safely be exposed. If you have a BYTE field which is used as a True/False flag, and your code consistently interprets a value of zero as False and all other values as True, then it doesn't matter what a programmer (or user) sets the flag to because your class can deal with the situation. If on the other hand you have a string field which can only contain the values YES, NO, SOMETIMES or MAYBE then you don't want anyone or anything setting that field to a value like HARDLY EVER. You do want to mediate access to the variable through one or more methods.

TIP: It's a convention in Clarion OOP to prefix a method name that gets a variable's content with Get and a method that sets a variable's content with Set.

The same principles apply to methods. You may have some methods that are only for the class's own use, and others that can be safely used by derived classes or other code.

You can restrict access to data and methods by using the PRIVATE and PROTECTED attributes.

PRIVATE And PROTECTED

The PRIVATE attribute means that the method or property can only be used by the class in which the method or property is declared. Use this attribute when you don't want any derived class or any other code whatsoever to be able to use a method or property. (There is one exception to this rule - classes declared in the same source file can use all of each others methods and properties. This is Clarion's version of "friends," or what David Bayliss calls "controlled encapsulation leakage.")

If for instance the TraceQ property in DebugClass were private, the code in Listing 4 would result in a compiler error because the derived class wouldn't have access to the queue.

If you want a property or method to be available to only derived classes, use the PROTECTED attribute.

TIP: One way to assess your class design is to put the PRIVATE attribute on all your class data, changing to PROTECTED where necessary to allow derived classes to compile. This isn't a hard and fast rule, but generally the more PRIVATE/PROTECTED data you have in a class, the better the design because any access to the data is mediated by methods which can control the actual values that may be set.

Asking Your Parents

Even though DebugClass's TraceQ is public, the code in Listing 4 isn't a great idea because it duplicates existing code, and one of the aims of OOP is to improve code reuse, not provide more opportunities for duplication. In Listing 5 the reference to TraceQ is replaced by a call to the parent class's trace method.

Listing 5. A better Trace method.
DebugTimedClass.Trace             PROCEDURE(STRING Text)
   CODE
   PARENT.Trace(CLOCK() & ' - ' & Text)

Just as SELF refers to the current object/class (see the previous article in this series), the PARENT keyword refers to the parent class. Figure 2 diagrams the relationship between the two classes.

Figure 2. A diagram showing the parent and child classes.

abcoop_fig2.gif (2888 bytes)

The triangle between the two classes shows the parent/child relationship. The top of the triangle is linked to the parent, and the base is linked to one or more child classes. Although you appear to be dealing with just one class in your code (DebugTimedClass) you're actually working with a composite of the base and derived classes. This is one of the things that can make OOP code look confusing at first. The class you're dealing with, if a derived class, contains much more code than is immediately apparent. Every method that's available in the parent class can be called from the derived (child) class, and every non-private property in the parent class can be used by the child class.

This doesn't mean that you need to use PARENT whenever you're working with a parent class method or variable. If a method exists in only the parent class then the compiler knows you mean PARENT even if you don't specify it. And if the method exists in both the parent and derived class, then the compiler always assumes you mean the derived method unless you use the PARENT keyword.

Listing 5 is a considerably more efficient design than is Listing 4 because it introduces the minimum amount of new code. As well, you can now make TraceQ private, and DebugTimedClass will still compile because it doesn't access the queue directly.

The end result of all of this is a small class which extends the functionality of the Trace method. That may seem like a lot of work for little effect, but then this is just a simple demonstration. In ABC, for example, there is a base class that defines a number of methods that are used to control the core behaviour of edit-in-place (EIP) controls. As new EIP controls are introduced (spin boxes, drop lists etc) classes are derived from the base EIP class, and these new classes add the unique functionality the control requires while making use of all of the common code.

Summary

Inheritance and encapsulation together make it possible to design self-contained blocks of code (classes) which efficiently reuse code. As a result the amount of generated code in ABC applications is a small fraction of that generated for legacy applications. ABC procedures implement classes which are derived from ABC classes, and which add just the code needed to perform procedure-specific tasks.

The kind of inheritance I've described in this article is what I think of as simple inheritance. One class is derived from another and adds some functionality. But there's much more to inheritance than this. Another much more powerful aspect is something called virtual methods, without which the combination of the ABC classes and templates simply would not work. That's coming up in the next installment.

Note: The debugging class used in this article is a rudimentary form of the cciDebugClass which is part of the Clarion Open Source Project and available by download.

An example application with a working DebugClass is also available for download.

Read Part 3


David Harms is an independent software developer and the editor and publisher of Clarion Magazine. He is also co-author with Ross Santos of Developing Clarion for Windows Applications, published by SAMS (1995), and has written or co-written several Java books. David is a member of the American Society of Journalists and Authors (ASJA).

Printer-friendly version

Reader Comments

To add a comment to this article you must log in.

 
 

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