![]() |
|
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.
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.
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
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.

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

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