Copyright
This documentation is Copyright (c) 1991-2001 by Anton van Straaten.
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.1, with no Invariant Sections, with no Front-Cover Texts, and with no Back-Cover Texts.
A copy of the license can be viewed by clicking on the following link:
GNU Free Documentation License
2 Introduction to Object Orientation
Object Orientation and Clipper 5
Object orientation and object-oriented programming (OOP) is at present revolutionizing the programming landscape. Until relatively recently, however, this subject would not have been of immediate interest to Clipper programmers, since neither Clipper 5.01 nor Clipper 5.2 provide the features necessary to implement OOP. Most importantly, while Clipper provides four predefined object classes (TBrowse, TBColumn, Get and Error), it does not provide the ability to create one's own object classes, or user-defined objects (UDOs). Because of this, for Clipper programmers, OOP has been more of a dream to which we could only aspire, than an attainable goal.
Fortunately, the outlook for OOP in Clipper changed with the introduction of Class(y) (pronounced "CLASSY"). Class(y) actually extends the Clipper language, providing a comprehensive range of features designed to support true object-oriented programming.
Class(y) enhances Clipper by providing the ability to create and use user-defined object classes, allowing fully object-oriented programs to be developed. It provides all the key features of an object-oriented language, including encapsulation, inheritance and polymorphism.
Class(y) combines the best object-oriented features of C++ and Smalltalk with the database power of Clipper, and adds some uniquely powerful capabilities of its own. As a result, Clipper with Class(y) stands alongside C++ as one of the very few object-oriented languages capable of being used for practical commercial application development.
How does Class(y) work? In the Clipper library CLIPPER.LIB, there is a module which is used internally to create the predefined object classes. This module is also responsible for handling messages sent to these objects. Class(y) replaces this module completely. The replacement module provides object-oriented capabilities not found in standard Clipper, such as the ability to create user-defined classes and objects, as well as other features designed to support true object-oriented programming. The replacement module is written in C and assembly language, and is optimized for high performance.
The new features work totally transparently, allowing the creation of user-defined classes which can then be used in exactly the same way as Clipper's predefined classes.
To use Class(y) effectively, an understanding of the concepts involved in object orientation is required. In the next section, we will examine object orientation in general. This will be followed by practical examples using Class(y).
Overview of Object-Orientation
"After years of relative obscurity, object orientation appears to be entering the mainstream of commercial computing for both software developers and end users. A shift to object orientation is occurring simultaneously across a wide range of software components, including languages, user interfaces, databases, and operating systems. While object-oriented programming is no panacea, it has already demonstrated that it can help manage the growing complexity and increasing costs of software development".
- from Object-Oriented Software by
Winblad, Edwards & King
This quotation is an excellent summary of what is happening in the world of computing today. Although exciting research and development is taking place on many fronts, no single software topic currently enjoys as wide a scope or impact as object orientation. Some of the most advanced and powerful software products available today incorporate object orientation as a central concept: languages such as Smalltalk, C++, and Actor; leading edge minicomputer databases such as Ontos and Servio Logic's Gemstone; expert system development tools such as Neuron Data's Nexpert Object and Level 5 Object from Information Builders; and graphical user interfaces (GUIs) such as Microsoft Windows, as well as UNIX GUIs such as Open Software Foundation's Motif, and Sun Microsystems' Open Look.
Although object orientation applies in slightly different ways in each of the areas mentioned above, the same basic concepts are being applied in each case. Because of its broad scope, the term is often misused, especially in marketing claims; indeed, articles have been written on this subject, such as "My Cat is Object-Oriented", by Roger King of the University of Colorado.
This abuse often arises from the fact that object orientation in user interfaces is not easy to define clearly, and it is through user interfaces that end users encounter object orientation, usually without realizing it. Some vendors assert that their products are object-oriented merely because they use screen windows - the windows are objects, the argument goes, and therefore the program is object-oriented.
This is perhaps an extreme of misrepresentation, but the situation is complicated by in-between products such as Microsoft Windows. At both the user interface and programming level, Windows is object-oriented in many ways, but in other ways falls far short of being "fully" object-oriented.
The aspect of object orientation which Class(y) addresses is that of object-oriented languages. The features required of an object-oriented language are well defined, and existing language products set something of a standard in this area. Once familiar with the principles of object-oriented languages, it becomes much easier to differentiate between true and false claims about object orientation in other areas.
One of the main driving forces for the adoption of OOP is likely to be the need to produce programs that run under graphical user interfaces such as Microsoft Windows. This means that changing from procedural to object-oriented programming may involve changing not just the language being used, but the operating environment, resulting in an extremely steep learning curve.
While GUIs promise to make life easier for the end user, they will only make it harder for the programmer, unless we are prepared to change our programming style. Writing programs with a completely object-oriented architecture simplifies development for GUIs, since the program architecture reflects the architecture of the underlying environment.
Although we cannot write Microsoft Windows applications using standard Clipper just yet, we can prepare ourselves by starting to develop object-oriented programs. This will allow us to climb the learning curve gradually, rather than suddenly being forced to learn a new programming style as well as the complexities of event driven programming in a GUI.
We'll start our climb of the learning curve with a brief look at the history of object-oriented languages, followed by an introduction to object-oriented concepts.
Brief History of Object-Oriented Languages
The concept of an object class and inheritance, central to object-oriented languages, was first implemented in the language Simula 67, an extension of Algol 60 designed in 1967 by Ole-Johan Dahl and Kristen Nygaard from the University of Oslo and the Norwegian Computing Center (Norsk Regnesentral). Although Simula, as it is now called, is a general purpose programming language, it is not in wide usage.
A major milestone in the development of object-oriented languages was the Smalltalk research project at the Xerox Corporation's Palo Alto Research Centre (PARC). Starting in the early 1970s, the Smalltalk project, initiated by Alan Kay, had as its goals more than just the development of a programming language; rather, a complete integrated environment was the goal, including an object-oriented language, development tools, and a graphical interface. The standard components of modern graphical user interfaces, such as windows, icons, and the mouse, were pioneered at Xerox PARC.
The Smalltalk language itself was the first 'true' object-oriented language in that it dealt exclusively with objects. All subsequent object-oriented languages have been based on the concepts used in Smalltalk. Smalltalk was important, not just for its language, but for the development tools available in the Smalltalk environment. These include class browsers and object inspectors. A class browser is a very powerful tool which allows program code to be edited in a much more convenient and structured way than with conventional editors. Because of the inherently well-defined structure of object-oriented programs, the class browser is capable of displaying a given program's class hierarchy in graphical form, allowing the user to 'point and shoot' to select a particular method (procedure) to be edited. Many programming tasks become menu driven, such as the creation of new classes, modifying the structure of the inheritance tree, and modifying the structure of a class. These operations are more complex and tedious when performed in a traditional editing environment.
Tools such as these are an integral part of the promise of object-oriented technology. They can simplify a programmer's life, reducing development time and costs. Although they are a rarity in the DOS world at present, as the move toward object-oriented technology grows, and as we move towards GUIs like Microsoft Windows, these tools will become more commonplace.
What is an Object?
One of the fundamental reasons that object orientation is enjoying such success as a programming paradigm is very simple: the real world is made up of objects. An invoice is an object. A stock item is an object. A balance sheet is an object. An entire company is an object. Objects can contain other objects (this is called composition); and in this way complete systems can be constructed using objects.
But what is an object from a programming point of view? Simply put, it is a collection of related data, which is associated with the procedures which can operate on that data.
By this definition, most well-structured programs could be said to contain objects. This is another contributing factor to the confusion surrounding the definition of object orientation.
It is in fact possible to write programs in an object-oriented way in many traditional procedure-oriented languages. However, without the support provided by an object orientated languages, many compromises have to be made.
An object-oriented language formalizes the relationship of the data within an object to the program code which can operate on that data, by requiring that the compiler or interpreter be informed which procedures are allowed to operate on an object's data.
Before we can clarify our definition of an object further, we need to explore a few other concepts.
Classes, Instances and Instance Variables
In any given system, many objects will exist. Many of these objects will be very similar to each other: for example, you might have thousands of invoice objects stored on disk. Although each one is different, they all share a common set of attributes. The same operations are valid on all of them. There is a term to describe such a collection of similar objects: it is called a class.
A class can be thought of as a template, or specification, for creating an object. The class itself consists of details specifying what the structure of its objects should be. The class can also be said to 'contain' the program procedures which are permitted to operate on objects of that class.
For example, an Invoice class would contain procedures for printing and updating invoices. It would also contain details of the structure of an invoice object, for example that each invoice object must contain variables named
date, customer, amount, etc.To look at it another way, we can define an object as an instance of a class. A given class may have many instances of itself (objects) in existence at any one time. Each of these instances has a structure determined by the class to which it belongs, but they are distinguished from each other by the data within that structure, which is stored in instance variables. The term instance variable distinguishes a variable that belongs to an object class from the ordinary stand-alone variables that we are used to. Instance variables are contained within objects, and are not directly accessible outside of those objects, although they can be accessed by sending messages to their objects.
Messages and Methods
Earlier, an object was defined as a module containing both procedures and data. An object's procedures are known as methods. This terminology helps to distinguish them from procedures which are not associated with an object, since there are fundamental differences. In a fully object-oriented system such as Smalltalk, there are no procedures, only methods. In a hybrid system such as C++, or Clipper with Class(y), both methods and procedures can coexist.
Methods are not called in the same way that procedures are. Rather, messages are sent to objects, which respond by executing the appropriate method. All valid operations on or using the data in an object are defined by its methods, so all operations on an object are accomplished by sending messages to the object. Because of this, it is not necessary for other objects to access, or even know about, the internals of foreign objects. Objects behave like black boxes: send them a message, and they respond by executing the appropriate method. Send the
print message to an invoice object, and it will respond by printing itself. This black box approach is known generally as encapsulation, and while it is possible to achieve in procedural systems, object-oriented systems actively encourage, support and enforce it.Inheritance - Superclasses and Subclasses
Common properties among groups of classes can often be combined to form a parent class, or superclass. For example, it might make sense for a Quotation class, an Order class, and an Invoice class to all share the same superclass, a Sales Document class. The Quotation, Order, and Invoice classes are thus subclasses of the Sales Document class. This is known as inheritance. The subclasses inherit all the properties of their superclass, and may add unique, individual properties of their own. This concept can be extended further, with subclasses of subclasses. Such class hierarchies are a common feature of object-oriented systems.
Inheritance is one of the most powerful features of object-oriented programming, since it allows reuse of existing code in new situations without modification. When a subclass is derived from a superclass, only the differences in behavior need to be programmed into the subclass. The superclass remains intact and will usually continue to be used as is in other parts of the system, while other subclasses are using it in different ways.
Polymorphism
The term polymorphism in this context refers to the fact that the same message, such as
print, can result in different behaviors when sent to different objects. Sending the print message to a graph object has a different effect than it would on a balance sheet object. With a traditional procedural approach, the programmer is forced to differentiate procedure names, using names like PrnBalSheet and PrintGraph. In an object-oriented language, this differentiation is unnecessary, and in fact unwise.Polymorphism has benefits for the programmer in that the same name can be used for conceptually similar operations in different classes, but its implications go deeper than that. It means that a message can be sent to an object whose class is not known. In a procedural system, given a variable of unknown type, a
CASE statement would typically be required to test the type of the variable and pass it to the correct procedure for the desired operation. In an object-oriented system, a message is sent to the object with no testing, and the object responds accordingly.This has important implications for inheritance, since it means that methods belonging to classes near the root of the inheritance tree do not need to know details of the subclasses which may be inherited from them. By sending messages with standardized names to the objects with which it deals, generic methods can be written which can later be used with any class which supports the required messages.
3 Using Class(y)
The concepts described in the previous chapter should become clearer when applied in an actual program. To this end, we will now follow the process of creating a simple, real life class; and at the same time familiarizing ourselves with Class(y), enabling us to move on to a more complex example in the next section.
First, some detail about Class(y). It consists of a library, CLASSY.LIB, and a header file, CLASS(Y).CH. Object-oriented Clipper programs can be written and compiled in conjunction with the header file, then linked with the library.
Class(y) makes use of, and extends, Clipper's limited built-in object-oriented capabilities, specifically the send operator, or colon. In standard Clipper, a message is sent to an object as follows:
object:message( <parameters,...> )
Class(y) does not change this syntax. In fact, program modules which use objects, without defining classes themselves, can be compiled without any special header file, since they are standard Clipper programs in all respects.
The include file, CLASS(Y).CH, must be included (using
#include) when defining a new object class. A user-defined class is usually defined in a separate program module. It can then be compiled into an object module (.OBJ), and linked into an executable file, or added to a library file (.LIB).The use of the preprocessor and user-defined commands for class creation means that if and when Computer Associates release a version of Clipper with the ability to create user-defined classes, it should only be necessary to change the Class(y) header file, rather than any of the code implementing specific user-defined classes.
Using Class(y), it is possible to mix and match programming styles. On the one hand, object classes can be developed and used in normal procedural programs, in the same way that the built-in TBrowse and Get classes are used in standard Clipper.
At the other extreme, complete object-oriented systems can be developed, in which no stand alone procedures exist, other than a startup procedure. The startup procedure initializes the system and from then on, all program execution is effected by passing messages between objects.
Any desired mix of these two approaches may be used, allowing developers to gradually familiarize themselves with the benefits of object-oriented programming.
Creating a Class
Perhaps the most fundamental feature required of an object-oriented programming language is the ability to create new object classes. With Class(y), a class consists of a class specification, which describes the overall structure of a class, and the method definitions, which contains the actual code for the methods of that class. These two components usually appear in the same module (.PRG file).
The Class Specification
We will use the creation of a simple rectangle class as our first example. This class will consist of little more than the coordinates of the rectangle, along with a method to set these coordinates. Here is the class specification:
// rectangle.prg
#include "class(y).ch"
CREATE CLASS Rectangle
VAR top, left
VAR bottom, right
EXPORT:
METHOD init
METHOD set
METHOD width, height
METHOD area
END CLASS
This code is used by Class(y) to create the specified class. It is actually a type of function, which is usually called whenever instances of the class need to be created. It should be placed at the beginning of the module (.PRG file) in which the code (methods) for the class are defined.
The first statement, CREATE CLASS, ;is straightforward. We are creating a class called Rectangle. All following statements up to the END CLASS statement are Class(y) class declaration statements, the purpose of which is to declare the structure of a class.
VAR is one such statement. It is followed by one or more instance variable names. In this example, we are saying that the Rectangle class contains four instance variables:
top, left, bottom and right.Following the VAR statements is the EXPORT: statement. This statement causes any methods and variables defined subsequently to be made accessible to any user of this class. By default, methods and variables are considered hidden, and can only be accessed from the methods of that class. This is how encapsulation is enforced in Class(y).
The METHOD command is used to declare the names of the methods defined in this class. It is followed by one or more method names. In class Rectangle, we are declaring that five methods will be defined:
init, set, width, height and area.Using the Class
Before we cover the actual definition of the methods, we will look at how the class might be used.
// testrect.prg
LOCAL x := Rectangle():new(5, 10, 15, 40)
? 'The dimensions of Rectangle x are:'
? ' width:', x:width
? ' height:', x:height
? ' area:', x:area
? ' top:', x:top
? ' left:', x:left
? ' bottom:', x:bottom
? ' right:', x:right
// eof testrect.prg
This program creates a new instance of the class Rectangle and assigns it to the local variable
x. The expression:Rectangle():new( 5, 10, 15, 40 )
can be understood as sending the
new() message to the Rectangle class, which has the effect of creating an instance of the Rectangle class with the specified dimensions.The
Rectangle() function in the above example is referred to as a class function, since it returns an object (a class object) which refers to the entire Rectangle class, rather than to a single instance of Rectangle.The method named
new is a predefined system method which creates a new object and initializes it. The new method is a predefined system method. It causes the appropriate initialization to be performed by sending an init() message to the newly created object, along with the parameters originally received by the new method. Each class should have its own, specialized init method. The job of the init method is to initialize the new object according to the specified parameters.The
new method, which is responsible for creating and initializing new objects, is known as a constructor method. The init method, which new uses to initialize new objects, is known as an initializer method.Once we have created our object, we can use it just as we would any other Clipper object, by sending it messages. The statement
x:width, for example, sends the width message to the Rectangle x. In this particular case, this has the effect of invoking the width method, which calculates and returns a result.Running this program produces output similar to the following:
C:\>testrect
The dimensions of Rectangle x are:
width: 31
height: 11
area: 341
Error CLASS(Y)/41 Scope violation (private): RECTANGLE:TOP
Called from obj:TOP(0)
Called from TESTRECT(10)
C:\>
Uh-oh! We have an error. Looking at line 10 of TESTRECT.PRG, we see that we have tried to print the value of
x:top. What is wrong with that? Looking back to the specification of class Rectangle, we notice that the instance variable top is declared before the EXPORT: statement. Because of this, top is considered a hidden instance variable, which means it can only be accessed by methods belonging to class Rectangle. Our program, TESTRECT.PRG, is not a Rectangle method, and so is prevented from accessing this variable.Hidden instance variables are an important benefit of object-oriented programming. A hidden instance variable is only accessible within the methods of its class. Making an instance variable hidden ensures that only a relatively small set of routines (the methods of that class) can change it. If the variable's value is incorrect, we know exactly which routines to examine.
In a well-designed object-oriented system, it is often desirable to prevent the values of instance variables from being changed outside of their class. However, it is often necessary to access (but not change) an instance variable's value outside of the class. To facilitate this, instance variables can be declared as read-only, using the keyword READONLY in the VAR command. Let's redefine the Rectangle class to allow external routines to read our instance variables:
CREATE CLASS Rectangle
EXPORT:
VAR top, left READONLY
VAR bottom, right READONLY
METHOD init
METHOD set
METHOD width, height
METHOD area
END CLASS
Notice that we have placed the instance variable declarations after the EXPORT: statement. This makes them accessible to any program, but the READONLY clause in the VAR command prevents other programs from assigning to these variables, in other words changing their value. Naturally, the methods of class Rectangle can still assign values to these variables - otherwise they would be useless.
Running TESTRECT now gives us the results we expect:
C:\>testrect
The dimensions of Rectangle x are:
width:
31
height:
11
area:
341
top:
5
left:
10
bottom:
15
right:
40
C:\>
Method Definitions
The method definitions for a class consist of program code for all the methods specified for that class. This code is usually placed after the class specification, in the same module (.PRG file).
When writing methods, you need to be aware of how a method differs from a normal function or procedure. A method, unlike a normal function or procedure, is not called directly, but rather is invoked as the result of a message sent to an object. A method cannot be invoked without an object being associated with it. A method acts on the object receiving the message; but to do so, it needs to be able to access it. To this end, a local variable called
self exists in all methods. self refers to the object which received the message that is being acted on. Instance variables and methods belonging to that object are accessed via the self variable. In the set method in the code below, for example, the line:self:top := nTop
assigns the value of the parameter
nTop to the instance variable top of the rectangle which received the set() message.The names of parameters and the names of the corresponding instance variables in this example have purposely been made different. Although the statement
self:top := top would work correctly - since there is currently no ambiguity between instance variable names and ordinary Clipper variable names - in future versions of Clipper, this may change.Here are the method definitions for class Rectangle:
// rectangle.prg continued...
METHOD init( top, left, bottom, right ), ()
self:set( top, left, bottom, right )
RETURN self
METHOD set(
nTop, nLeft, nBottom, nRight )
IF nTop <> NIL
self:top := nTop
END
IF nLeft <> NIL
self:left :=
nLeft
END
IF nBottom <>
NIL
self:bottom := nBottom
END
IF nRight <> NIL
self:right := nRight
END
RETURN self
METHOD
width
RETURN self:right - self:left + 1
METHOD height
RETURN
self:bottom - self:top + 1
METHOD area
RETURN self:width * self:height
// eof
rectangle.prg
The code is mostly self-explanatory. However, the METHOD command is being used differently from the way it was used inside the class specification.
When used after a class specification, the METHOD command begins the definition of a method, just as the Clipper statements FUNCTION and PROCEDURE are used to begin the definition of a user-defined function or procedure.
A method declared with the METHOD command must return a value, just like a function. If you need to declare a method which does not return a value, you can use the METHOD PROCEDURE command.
It has become standard practice to return
self from methods which do not otherwise need to return a value. This allows message sends to be chained. For example, if our Rectangle class contained a message called draw(), we might write an expression such as:oRect:set(5,4,10,15):draw()
which would set the coordinates for the Rectangle object referred to by
oRect, then send the draw() message to that object. This is only possible if the set() method returns self.The first method defined above,
init, makes use of a special feature of the METHOD command. Let's take a look at it.METHOD init( top, left, bottom, right ), ()
After the parameter list, there is a comma, followed by a pair of parentheses. It is actually a second parameter list, which in this case is empty. The reason for its existence will remain a mystery until the next section, when we discuss inheritance. For the moment, it is sufficient to know that you should always include this second parameter list when defining the
init method.As discussed in the previous section, new objects are usually created by sending the
new() message to a class object. For example, to create a new instance of the Rectangle class, we use code such as this:obj := Rectangle():new( 5, 10, 15, 70 )
This statement sends the
new() message, with the specified parameters, to the Rectangle class. The new() method responds by creating a new, empty Rectangle object, and sending the init() message to it to initialize it. A reference to the newly created and initialized Rectangle object is returned, and in the code above is stored in the variable obj.Whenever a new object is created, by sending a
new() message to a class, the initializer for that class is called. The initializer's job is to initialize the new object, and it can take parameters as necessary to facilitate this. By default, the initializer method for a class is called init.A class does not have to have an initializer, but it is highly recommended. In some cases, a class will inherit its initializer from a parent class (inheritance is described in the next section).
If an initializer is not defined for a particular class, and if it does not inherit an initializer from a parent class, that class will still accept the
new() message. Instead of returning a properly initialized object, though, it will return an empty object - an object with all its instance variables set to NIL. This is not usually very useful or desirable.Why is creating completely empty objects not desirable? One of the ways in which correctness can be ensured in object-oriented programs is by ensuring that objects are always in a valid state. An empty object is unlikely to be in a valid state. For example, a Rectangle object with its corner coordinates set to
NIL cannot be drawn and will not respond correctly to many messages. An object like this is in an invalid state.Now that we have finished with RECTANGLE.PRG, we have the makings of a complete system and can actually test it. The system can be compiled as follows:
clipper rectangle /n/w/a/b
clipper testrect /w/a/b
rtlink fi testrect,
rectangle lib classy
This should create the file TESTRECT.EXE which can then be run. Note that the /b switch is used, to include debug information. It might be instructive to trace through the program in the debugger (CLD TESTRECT) to get a feel for what is happening.
Inheritance
An object-oriented language is not complete if it does not support inheritance. Inheritance is the mechanism which allows existing code to be reused in different circumstances without modification.
As an example, we will declare a class called ScreenRect which can draw a rectangle on the screen and manipulate it. Since we already have a Rectangle class, we can save a lot of time by inheriting from that class.
The code below defines the class ScreenRect. A new feature is introduced here: the double colon (::) is a shorthand notation for sending a message to the
self object in a method. This is particularly useful in a statement such as the following, adapted from the hide method below:RestScreen( self:top, self:left, self:bottom,
self:right, ;
self:screenBuf )
With the double colon, this becomes:
RestScreen( ::top, ::left, ::bottom, ::right, ::screenBuf )
which might look strange at first, but it is easy to get used to and is easier to read than the alternative.
Here is the code for the ScreenRect class:
// scrnrect.prg
#include "class(y).ch"
#include
"box.ch"
CREATE CLASS
ScreenRect FROM Rectangle
VAR screenBuf
VAR boxStyle
VAR color
EXPORT:
METHOD init
METHOD moveUp, moveDown
METHOD
moveLeft, moveRight
METHOD hide, show
END CLASS
METHOD init( top, left, bottom, right, color, boxStyle ),
;
( top, left, bottom, right )
::boxStyle := IF( boxStyle == nil, B_DOUBLE,
boxStyle )
::color := color
::show()
RETURN self
METHOD hide()
RestScreen( ::top,
::left, ::bottom, ::right, ::screenBuf )
RETURN self
METHOD show()
LOCAL oldColor := SetColor( ::color )
::screenBuf
:= SaveScreen(::top, ::left, ::bottom, ::right)
@ ::top,
::left, ::bottom, ::right BOX ::boxStyle
SetColor(
oldColor )
RETURN self
METHOD moveUp( n )
::hide()
::set( ::top - n, NIL, ::bottom - n, NIL )
::show()
RETURN self
METHOD moveDown( n )
::hide()
::set( ::top + n, NIL, ::bottom + n, NIL )
::show()
RETURN self
METHOD moveLeft( n )
::hide()
::set( NIL, ::left - n, NIL, ::right - n )
::show()
RETURN self
METHOD moveRight( n )
::hide()
::set( NIL, ::left + n, NIL, ::right + n )
::show()
RETURN self
// eof scrnrect.prg
A number of new techniques have been sneaked in above. First and most important is inheritance. The ScreenRect class has been declared by inheriting from the Rectangle class, using the statement:
CREATE CLASS ScreenRect FROM Rectangle
This means that the ScreenRect class inherits all of the instance variables and methods of the Rectangle class, in addition to its own instance variables and methods. In the
hide method, for example, the instance variables top, left, bottom, and right, which are Rectangle instance variables, have been accessed, along with screenBuf, which is a ScreenRect instance variable.Inheriting from another class raises an issue with regard to the initializer method
init: the instance variables of the parent class, or superclass, should also be initialized when a new ScreenRect object is created. Since the superclass would usually have its own initializer for this purpose, it makes sense to use it - after all, one of the benefits of object-oriented programming is supposed to be reusability. Class(y) automates this process, by allowing the parameters to be passed to the superclass to be specified in the METHOD command. In the above example, the line:METHOD init( top, left, bottom, right, color, boxStyle
), ;
( top, left, bottom, right )
begins the definition of an initializer method which accepts the specified parameters. Because of the second parameter list, its first operation is to call the initializer in the superclass with the parameters specified in the second list. The second parameter list must, of course, match what is expected by the superclass initializer, in this case that of the class Rectangle.
This
init method is typical of most initializers. It passes some of its parameters up to the initializer in its superclass to ensure that inherited variables are initialized, and then it initializes the instance variables belonging to the class in which it is defined.An interesting point here is that because the instance variables of class Rectangle were declared read-only, class ScreenRect would be prevented from directly setting these variables. The only way ScreenRect methods can affect these variables is by invoking the initializer in the Rectangle class, or using the
set method which was written for that purpose. Note that if the instance variables of class Rectangle had been declared after a PROTECTED: command, then they could be updated from the ScreenRect class, since it is a subclass, but could not be updated from other, unrelated classes.The following sample program will allow us to test the ScreenRect class:
// testscrn.prg
#include "class(y).ch"
PROCEDURE main
LOCAL i
LOCAL rect1 := ScreenRect():new(5, 5, 10, 25, 'R+/G')
// the following uses default colors:
LOCAL rect2 := ScreenRect():new(15, 60, 22, 75)
FOR i := 1 TO 10
rect1:moveDown(1)
rect1:moveRight(1)
rect2:moveUp(1)
rect2:moveLeft(1)
NEXT
RETURN
Compile this program as follows:
clipper testscrn /w/b
clipper
scrnrect /n/w/b
rtlink fi testscrn, scrnrect, rectangle
lib classy
Note that the RECTANGLE module has been linked in above. Since the ScreenRect class is inherited from the Rectangle class, the RECTANGLE module is required when linking. If it is omitted, a link error will occur referring to an undefined symbol RECTANGLE.
You can now run TESTSCRN.EXE. Again, tracing through it in the debugger is a useful exercise (CLD TESTSCRN).
Class Variables & Methods
One other fairly important concept needs to be introduced: class variables and methods. Up until now, we have talked about instance variables, and the methods we have referred to are, in a sense, instance methods in that they deal with instances of the class and are invoked by sending messages to such instances.
Every object, or instance of a class, has its own set of instance variables with its own unique values, which differentiate it from other members of the same class. Similarly, every method has implicit access to the object
self, which is the specific object, or instance, that the method is operating on.There are some situations, however, where all instances of a class need to share the same data, or where a method needs to take action which affects the class as a whole. Class variables and class methods exist to cater for these situations.
A class variable is a variable, defined in the class specification, which is shared among all instances (objects) of its class. Where instance variables can have a different value in each instance of a class, only one copy of a class variable exists for an entire class.
Similarly, a class method is a method which applies to an entire class, in that it can only be invoked by sending a message to a class object, and its
self variable will then refer to that class object. Because of this, a class method cannot directly access (via self) the instance variables defined in its class, but it can access its class variables.For example, if we were to expand our ScreenRect class into a more complete windowing system, one of the features we might want is a method that would close all windows on the screen. To implement this, we would need a class method, perhaps called
hideAll. It should be a class method since it relates to all existing instances of the class, not just a single instance. To do its job, hideAll would need access to all of the instances in existence at a given time. To achieve this, we could define a class variable that contains an array of all existing ScreenRect objects. We will call this variable activeRects.To implement this, we might modify our class specification as follows:
CREATE CLASS ScreenRect FROM Rectangle
VAR screenBuf
VAR boxStyle
VAR color
CLASS VAR
activeRects
EXPORT:
METHOD init
METHOD moveUp,
moveDown
METHOD moveLeft, moveRight
METHOD hide, show
CLASS
METHOD hideAll
CLASS METHOD initClass
END CLASS
The class variable
activeRects was declared using the CLASS VAR command. Notice that it was declared before the EXPORT: command, making it a hidden variable. There is no need for other modules to have direct access to this variable, and making it hidden prevents an external routine from mistakenly setting the variable to NIL, for example, thereby destroying our list of active objects!Two class methods were declared using the CLASS METHOD command:
hideAll and initClass. The hideAll method is the one we're trying to define; but where did initClass come from? It is there for the sole purpose of initializing any class variables. Only one copy of each class variable is shared by all instances of a class, so initializing these variables in the init method doesn't make sense, since the class variables would get reinitialized every time an object was created. We could put in an IF statement to check whether the class variables had already been initialized, but this would be a workaround at best. The initClass method is intended to take care of this issue. If a class has a method called initClass, Class(y) will automatically invoke that method only once, when the class is first created. This makes the initClass method an ideal place to initialize class variables. Note that initClass must be declared using CLASS METHOD.Here are the method definitions for
initClass and hideAll as they appear in SCRNRECT.PRG:METHOD initClass
::activeRects
:= {}
RETURN self
METHOD hideAll
LOCAL i
FOR i := 1 TO LEN(::activeRects)
::activeRects[i]:hide
NEXT i
RETURN self
For this to work correctly, we will need to make sure that the
activeRects variable is maintained correctly, by adding the following code somewhere inside the ScreenRect initializer:AADD( ::activeRects, self )
This will add each new ScreenRect object, as it is created, to the
::activeRects array.Finally, we should also ensure that ScreenRect objects get removed from the active list when they are no longer active, in other words when
hide() is invoked. We can do this using something of a sledgehammer approach, by adding the following code to the hide method:FOR i := 1 TO LEN( ::activeRects )
IF ::activeRects[i] == self
ADEL( ::activeRects, i
)
ASIZE( ::activeRects, LEN( ::activeRects ) - 1 )
END
NEXT i
This is somewhat inefficient, in that it has to scan the array of active rectangles to find
self, i.e. the one that has received the hide message. A better implementation might be to enforce a stack-based approach, so that only the most recently displayed ScreenRect can be hidden. This would also prevent problem of restoring screen areas in the wrong sequence. In fact, the sample Window class supplied with Class(y) uses a List class to achieve something very similar.4 Pull-Down Menu Tutorial
Introduction
Having covered some of the basics of creating a class and using inheritance, we'll move on to a more sophisticated example. The program we are going to discuss implements a general pull-down menu system. It has been kept fairly simple to serve as a clear illustration of OOP techniques.
Consider a typical pull-down menu:
File Window Block
Load
Edit
Save
Document
Text
We will examine the operation of a menu like this in some detail, to give us a specification to work from.
A pull-down menu system consists of a number of components, the first of which is the menu bar, running horizontally across the screen, containing a number of options. This menu behaves like a normal Clipper menu, allowing options to be highlighted with a light bar controlled by the cursor keys, or by typing the first letter of the desired option. When an option is selected by pressing the ENTER key, an action associated with that option is performed. In most cases, this action will be to display a corresponding menu, 'pulled down' from the menu bar. In the example above, the
File option has been selected, which has resulted in the Load/Edit/Save menu being pulled down. The pulled-down menu also behaves like a standard Clipper menu, separate from the menu bar. Because the menu's options are arranged vertically, the up and down arrows are used to move between options. Pressing a left or right arrow results in the pulled-down menu being closed, and its sibling to the left or right being pulled down instead. Finally, selecting an option from this menu again results in an action being executed. Sometimes, this action will consist of yet another menu - but when this happens, the new menu will not behave like a pull-down menu. Aside from not being pulled down from the menu bar, it would have no siblings and so the left and right arrow keys cannot select other menus. We will refer to this kind of menu as a pop-up menu. In the above example, the Save option has been selected from the pull-down menu, resulting in a pop-up menu containing the options Document and Text.Designing the Classes
Now, to write an object-oriented program to implement this menu system, a good first step is to decide on the classes which will make up the system. From the specification, we can see that we are dealing with three varieties of menu: the menu bar, the pull-down menu, and the pop-up menu. It would make sense to have a class for each one of these menu types. However, the different types of menu have many common characteristics. To avoid duplicating code, we need a class which has the characteristics common to all three types of menu. We will call this class BaseMenu.
A class such as BaseMenu is often referred to as an abstract class, since you would not normally create instances of that class. Instead, you would create instances of the three real menu classes (which we shall call MenuBar, PullDnMenu, and PopupMenu), which will all be inherited from the BaseMenu class. This means that all three menu varieties will have the same basic set of instance variables and methods, derived from BaseMenu. In addition, each of the classes may implement new methods and variables of their own.
The most important data which the BaseMenu class will contain are the details of the menu items themselves. But what constitutes a menu item? It must include option's label text, which is displayed on the menu, and an action associated with that option. We might also want to store the row and column at which the label should be displayed. With all these related attributes, it is worth creating a MenuItem class to group all of this information together.
So the BaseMenu class will contain an array of MenuItem objects. This array will be referred to by an instance variable called
items.

Pull-Down Menu Class Diagram
A diagram of the class hierarchy for our pull-down menu system is shown in Figure 2 above. Please note that this diagram does not completely conform to any standard diagramming methodology. Nevertheless, it serves to illustrate our example, and it shows how easily an object-oriented design can be meaningfully diagrammed.
Each class is represented by its own box. Inside the box are listed the name of the class, its instance variables, and methods. By convention, solid arrows between the classes point from a subclass to its superclass. The dotted arrow pointing from the instance variable
items in the BaseMenu class has an arrow pointing to the MenuItem class, indicating that the items variable in a BaseMenu object will contain MenuItem objects.The diagram shows that the MenuBar and PopupMenu classes have been inherited from BaseMenu, as described earlier. However, PullDnMenu has not been inherited directly from BaseMenu. The reason for this is that a PullDnMenu is very similar to a PopupMenu. A PullDnMenu does all of the things which a PopupMenu does, such as drawing a window on the screen. However, according to our specification, it has one extra behavior which PopupMenu doesn't have: it responds to the left and right arrow keys by activating the appropriate sibling menu on the menu bar. To avoid duplicating code, PullDnMenu has been inherited from PopupMenu. This means that PullDnMenu only needs to implement the code necessary for its extra, unique behavior; it inherits all its other behavior from PopupMenu in the form of methods and instance variables.
Generally speaking, in an inheritance tree (which usually has a root at the top, and branches out downwards), the classes nearer the root are always the most general classes. As you move away from the root, the classes should become more and more specialized, only implementing the extra features required, without needing to reimplement behavior that is already defined further up the tree.
Explanation of the Code
When going through the code for the pull-down menu system, don't be overly concerned if it does not make perfect sense at first. The program has been designed to illustrate many of the most important concepts in OOP in general, using a system consisting only of interacting classes.
You may notice that the program makes use of a Window class, which is included with Class(y). We do not cover the operation of this class in detail, since it is incidental to the overall structure of the program. In brief, the Window class allows windows to be created, popped up and popped down, handling screen saving and related functions transparently. Screen I/O within a window is relative to the window's borders, so when a window is active, an
@ 0, 0 SAY... command will refer to the top left corner of that window.Creating a Class - MenuItem
We will start with the MenuItem class, which is a stand-alone class, and hence quite straightforward. The source code for this is contained in the file MENUITEM.PRG on the distribution disk. As mentioned earlier, it contains instance variables for
row, column, label, and action. It also has a flag, isActive, which can be used to deactivate a particular option. Here is the class specification:// MenuItem.prg
#include "class(y).ch"
#include
"win.ch" // Class(y) sample window library header
CREATE CLASS MenuItem
VAR label
VAR action
EXPORT:
VAR row, col READONLY
VAR isActive READONLY
METHOD init
METHOD draw
MESSAGE exec TO action
METHOD nextCol
METHOD nextRow
END CLASS
METHOD init( nRow, nCol, cLabel, oAction, isActive ), ()
::row := nRow
::col := nCol
::label := cLabel
::action :=
oAction
::isActive := IF( isActive == NIL, .T., isActive
)
RETURN self
METHOD draw
IF ::isActive
@ ::row, ::col PROMPT ::label
ELSE
@ ::row, ::col SAY ::label
END
RETURN self
METHOD nextRow
RETURN ::row + 1
METHOD
nextCol
RETURN ::col + LEN( ::label )
// eof menuitem.prg
Skipping through the code a bit, we can see the definition of one of the methods,
draw:METHOD draw
IF ::isActive
@ ::row, ::col PROMPT ::label
ELSE
@ ::row, ::col SAY ::label
END
RETURN self
We have already discussed the role of the
self object within methods, but this is a good opportunity to look at how it works in a real method. Remember that the double colon (::) is shorthand for sending a message to the self object.When a statement like
item:draw() is executed, if the item variable refers to an object of class MenuItem, then the draw method in that class will be invoked. Within this method, self is automatically set to refer to the same MenuItem object referred to by the caller's item variable - the variable which the draw message was sent to. The statement above, then, will cause that MenuItem's label to display in the correct row and column, which is also stored in the MenuItem object.The initializer for the MenuItem class is quite straightforward. It accepts up to five parameters specifying the position, text, action and status of the MenuItem, and assigns these parameters to its instance variables. The initializer will be invoked automatically when a new MenuItem object is created, as in the following statement:
LOCAL item := MenuItem():new(1, 1, "Print", { || Print() })
This would create a new MenuItem object containing the specified values, and set the
item variable to refer to this object.If you trace through this code in the Clipper debugger, you will see the class functions, such as MenuItem, being called every time a new object is created. The first time such a function is called, every line will be executed, since the class is being created; on subsequent occasions, it returns immediately, passing a reference to the class back to the caller.
Other MenuItem Methods
Only two other methods are defined in the MenuItem class:
nextrow and nextcol. They are used to calculate the next available row or column after the current option. They are invoked by the addItem method in the PopupMenu and MenuBar classes respectively.There remains one unexplained declaration in the MenuItem class specification:
MESSAGE exec TO action
This statement makes use of the Class(y) feature known as message forwarding. The MESSAGE command in this context indicates that we are not defining a method for this message, but we are specifying what to do in response to the message. In this case, the "
TO action" clause specifies that when the exec message is received by a MenuItem object, the message will be forwarded, with all parameters, to the object referred to by the action instance variable. If we did not use this message forwarding feature, we would have to define a MenuItem method such as this:METHOD exec( oParent )
RETURN
::action:exec( oParent )
Using message forwarding saves code and improves performance. In this example, forwarding is used to allow MenuItem objects to receive
exec messages and respond to them by executing the action associated with that item - by forwarding the exec message to the object referred to by the action instance variable. The BaseMenu class implements an exec method which displays and executes a given menu, so if the object referred to by action is a menu, the exec message will have the desired effect. In this way, menus can be nested to any depth.At some point, though, we will want to perform an action other than displaying a menu. Many Clipper menu systems work by storing actions as code blocks, and evaluating the appropriate block when an option is selected. To cater for this, code blocks are treated as a special type of object, which can be evaluated by sending an
exec message to them. (Note: in standard Clipper, a similar but undocumented capability exists, allowing an eval message to be sent to a code block. Class(y) supports both the exec and eval messages).So with Class(y), there is more than one way to evaluate a code block:
LOCAL bSquare := { |x| x * x }
LOCAL n := 17 // value for testing
? eval(bSquare, n) // the old way
?
bSquare:eval( n ) // the undocumented Clipper way
?
bSquare:exec( n ) // the Class(y) way
? { |x| x + 2
}:exec( n ) // this also works!
By implementing an
exec method in a given class, we can use that class with this menu system without any modification to the existing menu classes. For example, we might have a Dialog Box class, which on receiving an exec() message would display itself and allow the appropriate user input. With a complete population of such classes (e.g. Report, Graph, Browse...), we would do away with code blocks (for this purpose) completely.The BaseMenu Class
Much of the functionality of the menu system is encapsulated in the BaseMenu class (code in BASEMENU.PRG). However, different specific features are required by each of its subclasses. To support this, two messages are declared in the BaseMenu class, without corresponding methods, as follows:
MESSAGE setKeys IS DEFERRED
MESSAGE clearKeys IS DEFERRED
As with
exec in MenuItem, the MESSAGE command is used to declare messages which have no corresponding method defined in the current class. The clause IS DEFERRED causes the messages to invoke a method called deferred which is defined in the system class, Object. All the deferred method does is generate an error as follows:Message should be implemented by subclass
This forces subclasses to redefine these messages. If they do not, the above error will occur. Deferred methods are often used in abstract classes such as BaseMenu. They document that the specified message must be overridden in a subclass, and they ensure that if the message is not overridden, a meaningful error will occur.
In this menu system, only the PullDnMenu class has any need to implement these methods. For the other subclasses of BaseMenu (PopupMenu and MenuBar), no action needs to be taken in response to the
setKey and clearKey messages, so the following declarations are used in those classes:MESSAGE setKeys IS NULL
MESSAGE
clearKeys IS NULL
Here, the IS clause of the MESSAGE command is used to map the message to the
null method, which like deferred, is defined in the system class, Object. The IS clause can be used to cause any method defined in a superclass to be invoked in response to the specified message. The Object class is the ultimate superclass of all classes in Class(y), and it defines methods such as deferred and null for exactly this purpose.As discussed earlier, BaseMenu contains an array of MenuItem objects stored in an instance variable called
items. If you look at the draw method in BASEMENU.PRG, you will see that it is quite simple, just looping through the item array and sending a draw() message to each item in turn. This has the effect of displaying all the options for a particular menu on the screen. Note that it does not draw a box around the items; this additional specialized behavior is implemented by the subclasses PopupMenu and MenuBar, since the one draws a box, or window, while the other merely draws a bar across the screen.The BaseMenu initializer takes an array of item/action pairs and initializes the
items instance variable by invoking the addItem method for each item pair. The addItem method creates a new instance of MenuItem and adds this to the items array.The
exec method in BaseMenu causes the menu to be displayed on the screen, using the draw method, and executes a MENU TO command to allow the user to choose an option. If an option is selected, the exec message is sent to that MenuItem object, causing the appropriate action to take place.The
newMenuPos method is invoked by the various classes on their parent menu to establish where on the screen to draw themselves.// BaseMenu.prg
#include "class(y).ch"
CREATE CLASS BaseMenu
PROTECTED:
VAR items
VAR currPos
VAR parent
EXPORT:
METHOD init
METHOD addItem
METHOD draw
METHOD exec
METHOD newMenuPos
//
declaring the following two methods as deferred
// will
force subclasses to override them.
MESSAGE setKeys IS
DEFERRED
MESSAGE clearKeys IS DEFERRED
END CLASS
METHOD init( aItems ), ()
LOCAL
i
::items := {}
::currPos := 1
IF
aItems != NIL
FOR i := 1 TO LEN( aItems )
// note: following is a bit tricky; invokes addItem
// in the subclass, which takes fewer parameters
// than BaseMenu's addItem.
::addItem( aItems[i, 1], aItems[i, 2] )
NEXT
END
RETURN self
METHOD draw()
LOCAL i
FOR i := 1 TO LEN( ::items )
::items[i]:draw()
NEXT i
RETURN self
METHOD addItem( nRow,
nCol, cLabel, oAction, lActive )
AADD( ::items, ;
MenuItem():new(nRow, nCol, cLabel, oAction, lActive ))
RETURN self
METHOD exec( oParent )
LOCAL finished := .f.
::parent := oParent
WHILE
!finished
::draw()
::setKeys()
MENU TO ::currPos
::clearKeys()
finished := ( ::currPos == 0 )
IF !finished
::items[::currPos]:exec( self )
END
END
RETURN self
METHOD newMenuPos
RETURN
::currPos
// eof basemenu.prg
The MenuBar Class
This class inherits all the behavior of BaseMenu. It adds no instance variables of its own. It adds a replacement
draw method, which highlights the bar across the top of the screen and then invokes the draw method in BaseMenu. This is done with the following statement:::super:draw()
Explicitly invoking a method in a superclass is a very common operation in OOP, and is typically used in exactly this situation - to add functionality to a method defined in a superclass. In Class(y),
super is a reserved message which all classes accept (since it is defined in the Object class). Sending the super message results in a reference to the part of the object defined by the superclass. In the above example, this results in the draw method in the superclass being invoked, instead of the one defined in the current class.The MenuBar class, having no instance variables of its own, does not need to perform any initialization of its own. Accordingly, no
init method has been defined. However, MenuBar will inherit the init method from the superclass, BaseMenu, ensuring that inherited instance variables are correctly initialized.// MenuBar.PRG
#include "class(y).ch"
#define OPTION_SPACING 4
CREATE CLASS MenuBar FROM
BaseMenu
EXPORT:
METHOD draw
METHOD addItem
METHOD newMenuPos
// override parent's DEFERRED
messages, but this class
// doesn't need to do anything
with them, so map to NULL.
MESSAGE setKeys IS NULL
MESSAGE clearKeys IS NULL
END
CLASS
METHOD addItem( cLabel, oAction, isActive )
LOCAL
nCol
// establish screen column
for new option
IF len(::items) == 0
nCol := OPTION_SPACING
ELSE
nCol := ATAIL( ::items ):nextCol() + OPTION_SPACING
END
// invoke addItem in the superclass (BaseMenu)
::super:addItem( 0, nCol, cLabel, oAction, isActive )
RETURN self
METHOD draw()
winCurrent(0) //
selects main screen
@ 0, 0 // draw the bar
::super:draw() // invoke superclass' draw method
RETURN self
METHOD newMenuPos
//
tells a child menu where to put itself
RETURN
::items[::currPos]:col
// eof menubar.prg
Overriding an Inherited Method
For inheritance to be an effective mechanism for code reuse, we need a way of modifying behavior that has been inherited from a superclass, without modifying the superclass itself. This can be done by overriding methods that have been defined in a superclass.
The newMenuPos method in MenuBar completely overrides the method of the same name in BaseMenu. Remember that
newMenuPos is invoked by a child menu and is used to determine where that menu should draw itself. When a MenuBar is the parent, the child menu must be drawn underneath the corresponding option on the bar, so newMenuPos returns the relevant column position. The corresponding method in BaseMenu is used without change by the other two subclasses.The only other method in MenuBar is
addItem, also a reimplementation of a BaseMenu method. This addItem takes only three parameters: the label for the option, the action object and the isActive flag. It decides where to put the new item, using the nextCol method in MenuItem. It then invokes addItem in the superclass with the additional row and column parameters. So in this case, although we have overridden the addItem method defined in the superclass, the overriding method still makes use of it; in this way, we add functionality rather than replacing it.Calculating the row and column like this means that the user of the MenuBar class does not have to worry about it. MenuBar's
addItem method becomes a simplified shell or interface to BaseMenu's addItem method.The PopupMenu Class
This class is similar in structure to MenuBar, since it is also inherited directly from BaseMenu. Like MenuBar, it refines the
draw and addItem methods. In this case the refinements have to do with drawing a box, or window, around the menu. It adds two instance variables for this purpose, window to refer to the window object and width which is used in its addItem method to determine the window's required width. In addition, it has to redefine the exec method, so that it can remove the window after execution is complete.Two other methods are defined:
menuTop and menuLeft. These are needed to make the window drawing behavior more general, since PullDnMenu is inherited from PopupMenu and it has slightly different window drawing requirements. In PopupMenu, these methods return coordinates based on the parent menu's position, assuming that the parent menu is a PullDnMenu or another PopupMenu.// PopupMenu.PRG
#include "class(y).ch"
#include "win.ch"
CREATE CLASS PopupMenu FROM BaseMenu
PROTECTED:
VAR window
VAR width
METHOD menuTop, menuLeft
EXPORT:
METHOD init
METHOD
draw
METHOD addItem
METHOD
exec
// override parent's
DEFERRED messages, but this class
// doesn't need to do
anything with them, so map to NULL.
MESSAGE setKeys IS
NULL
MESSAGE clearKeys IS NULL
END CLASS
/*
init()
The ::width variable must be initialized before
superclass'
initializer is invoked, so we aren't use the
extended
METHOD syntax below. Instead, the superclass'
initializer
is invoked explicitly with the statement
::super:init(...).
*/
METHOD init( aItems )
::width :=
0
::super:init( aItems )
RETURN
self
METHOD draw()
LOCAL bottom, right
IF ::window == NIL
bottom := ::menuTop + len(::items) + 1
right :=
::menuLeft + ::width + 1
::window := Window():new(
::menuTop, ::menuLeft, ;
bottom, right, SNGLBORD )
END
::super:draw()
RETURN self
METHOD menuTop
RETURN winTop() + ::parent:newMenuPos()
METHOD menuLeft
RETURN winLeft() + 2
METHOD addItem( cLabel, oAction, isActive )
LOCAL nRow
// establish
screen row for new option
IF LEN( ::items ) == 0
nRow := 0
ELSE
nRow := ATAIL( ::items ):nextRow
END
::super:addItem( nRow, 0, cLabel,
oAction, isActive )
::width := MAX( ::width, LEN( cLabel
) )
RETURN self
METHOD exec( oParent )
// invoke the exec method in the superclass (BaseMenu)
::super:exec( oParent )
::window:kill()
::window := NIL
RETURN self
// eof
popupmen.prg
The PullDnMenu Class
PullDnMenu is inherited from PopupMenu, and adds no new instance variables of its own. It overrides the
menuTop and menuLeft methods with its own, which return position values which assume that its parent menu is a MenuBar (since PullDnMenus are always pulled down from a menu bar).PullDnMenu also defines
setKeys and clearKeys, which you may recall are specified as deferred methods in BaseMenu, and called by BaseMenu's exec method. setKeys sets the left and right arrow keys to refer to the moveLeft and moveRight methods, also defined in PullDnMenu. clearKeys clears these settings.Notice here that some foresight was required to actually put the calls to these deferred methods in BaseMenu. In practice, you might find when defining a class lower down in the hierarchy, that you need to modify a class further up the tree to make it more generic. In this case, declaring and calling the deferred key-setting methods in BaseMenu have made it more generic, allowing inherited classes to remap the keyboard as desired, just by defining two methods.
Increasing generality is one of the only valid reasons to modify an existing class when inheriting from it. If instead we modified BaseMenu by making it more specific, for example by drawing a window around its menu, all the subclasses would inherit this behavior, or be forced to override it, which would be inefficient. Classes near the top of the inheritance tree should be, and are, more general, and they get more specific as the tree expands downwards.
// PullDnMenu.PRG
#include "class(y).ch"
#include "win.ch"
#include
"inkey.ch"
CREATE CLASS PullDnMenu FROM PopupMenu
PROTECTED:
METHOD menuTop, menuLeft
EXPORT:
METHOD moveLeft, moveRight
METHOD setKeys,
clearKeys
END CLASS
METHOD menuTop
RETURN winTop() + 1
METHOD menuLeft
RETURN winLeft() +
::parent:newMenuPos()
METHOD moveLeft
KEYBOARD CHR(K_ESC)
+ CHR(K_LEFT) + CHR(K_ENTER)
RETURN self
METHOD moveRight
KEYBOARD CHR(K_ESC) + CHR(K_RIGHT) + CHR(K_ENTER)
RETURN self
METHOD setKeys
SET KEY
K_LEFT TO ::moveLeft
SET KEY K_RIGHT TO ::moveRight
RETURN self
METHOD clearKeys
SET KEY
K_LEFT TO
SET KEY K_RIGHT TO
RETURN self
// eof pulldnme.prg
The Menu Demonstration
The demonstration program, MENUDEMO.PRG, is a normal procedural program which uses the menu objects to display a menu. In practice, you would probably want to make a routine like this data-driven, so that the details of the menu layout are read in from a configuration file. However, the purpose of this demonstration program is merely to show how the menu objects can be created and used.
Pull-down menu systems tend to be nested structures by their very nature, and the code in MENUDEMO reflects this. Using the methods in the classes we have written, there are various ways in which a menu could be defined. On the one hand, an empty menu could be created, after which items could be added to it one by one. This approach is demonstrated in the comment near the end of the program. At the other extreme, you can nest your calls to the menu constructor as deep as you like, for example:
oMenuBar:addItem(" File ", ;
PullDnMenu():new( { ;
{ " Load ", { ||
LoadFile() } }, ;
{ " File ", { || FileOpt() }
}, ;
{ " Save ", ;
PopupMenu():new( { ;
{ "Document ", { ||
SaveDoc() } ,;
{ " Text ", { || SaveText()
}},;
}) ) )
...but this gets rather hard to read! In MENUDEMO.PRG, we have taken a middle path, defining one menu at a time and assigning them to variables until they are needed. This means that the menu has to be implemented in reverse, defining the deepest menus first (a bit like Reverse Polish Notation on a Hewlett Packard calculator).
The nested arrays and code blocks do make the code look a bit icky, but again, data-driving the menu would do away with such problems.
As discussed earlier, if this demonstration program were more object-oriented, it would not use code blocks at all; instead, all the actions would be objects of some kind. The entire system could then consist of objects, each with their own specific task, and in this case all tied together by the menu.
Tracing Execution
To get an idea of what happens during actual execution, let's ignore the creation of the menu objects, which in principle we should already understand, and look at what happens when the statement
oMenuBar:exec() is executed.Because MenuBar does not directly implement an exec method, the exec method in the superclass, BaseMenu, is invoked. Once into the menu loop, the first thing this method does is to draw the menu by sending the
draw() message to the self object with the statement ::draw().It is important to realize that even though we are now executing a BaseMenu method, the
self object at this point is not an instance of BaseMenu, but rather an instance of MenuBar, as referred to by the variable oMenuBar.Because of this, the
draw method that now gets invoked is MenuBar's draw method; we are now back down in the MenuBar class. But after this method has drawn a bar across the top of the screen, it explicitly invokes draw in the superclass - and we're back in a BaseMenu method!Once BaseMenu's
draw has executed and displayed all the menu items, it returns to where it was invoked, which was MenuBar's draw; this in turn returns to BaseMenu's exec, where execution continues.We could continue describing this flow of control for some time, but we have illustrated the basic point, which is how execution switches up and down the inheritance tree depending on where methods have been defined.
This can be tricky to come to grips with; again, tracing through the code in the debugger can help a great deal.
5 Inheriting From TBrowse
In this chapter we will look at an example which involves inheriting from Clipper's TBrowse class. The programs discussed in this chapter can be found on the distribution disk in the files DBROWSE.PRG and DBROWDEM.PRG.
Executing the batch file MAKEALL.BAT in Class(y)'s SOURCE\SAMPLE directory will compile and link all of the sample programs, including the browse demonstration, DBROWDEM.EXE. It will have debug information included, and is interesting to trace through in the Clipper debugger.
The pull-down menu example in the previous chapter is highly object-oriented, consisting of five interacting object classes. This provides an interesting view of how such a system can be structured. But for various reasons, not all of us are in a position to begin writing completely object-oriented Clipper systems right now - although many Class(y) users are successfully doing so.
This chapter discusses a more immediately practicable example of using user-defined classes in a Clipper program. We are going to inherit a new class from the built-in Clipper class, TBrowse, resulting in a class which is more complete, easier to use, and without sacrificing flexibility.
This example is not intended to be an "ultimate" browsing class. Instead, it is based on a piece of code which will be familiar to many people: the Clipper sample program TBDEMO.PRG, which has been reworked as a new class, called dBrowse, inherited from TBrowse.
TBDEMO.PRG can be found in the subdirectory SOURCE\SAMPLE in the directory in which Clipper is installed. This text refers to the version supplied with Clipper 5.01a. The sample program was based on the version supplied with the original Clipper 5.0, so there are some discrepancies in the way things are implemented in DBROWSE.PRG, when compared to newer versions of TBDEMO.PRG.
TBDEMO uses the TBrowse class to implement a fairly simple database browse, with editing. The majority of the code in TBDEMO is devoted to setting up and using a TBrowse object. Essentially, TBDEMO specializes the behaviour of TBrowse, by providing a specifically database oriented browsing capability.
It is not a coincidence that this exact kind of specialization is a major characteristic of any inherited class in a well designed object-oriented system.
What most of the code in TBDEMO tells us, quite plainly, is that it should be implemented as a subclass of TBrowse. It makes little sense to revert to procedural programming, just when we could most use object orientation to manage the complexity of a general class such as TBrowse.
The Code
To start with, we have declared the dBrowse class as follows:
CREATE CLASS dBrowse FROM TBrowse
VAR appendMode
EXPORT:
METHOD autoFields
METHOD exec
METHOD goBottom
METHOD goTop
METHOD skipper
END CLASS
So the dBrowse class inherits all of the instance variables and methods of the TBrowse class. In addition, a new instance variable and five new or replacement methods are added.
Cargo is a Kludge!
The
appendMode instance variable makes an important difference to the code. It stores the current append mode - ie. whether records are currently being appended. As an instance variable, it is available to all methods in the dBrowse class.In the original TBDEMO, the
cargo variable in the TBrowse class is used to store the state of the append mode. To retain readability and maintainability, the preprocessor was used to hide the underlying instance variable name, as follows (taken with comments from TBDEMO.PRG):// These #defines use the browse's "cargo"
slot to hold the
// "append mode" flag for the
browse. The #defines make it
// easy to change this
later (e.g. if you need to keep
// several items in the
cargo slot).
#define TURN_ON_APPEND_MODE(b) (b:cargo :=
.T.)
#define TURN_OFF_APPEND_MODE(b) (b:cargo := .F.)
#define IS_APPEND_MODE(b) (b:cargo)
This is an obvious workaround which highlights the fact that the entire concept of the
cargo slot, in all four of Clipper's predefined classes, is a workaround for the fact that Clipper itself does not support user-defined classes and inheritance.Overall Program Structure
The original TBDEMO code consists of ten functions. We will examine some of the key functions: TBDemo(), MyBrowse(), Skipper(), and DoGet(). We will examine how these functions were modified to become part of our dBrowse class.
Before we do that, though, there is one change which was made repeatedly throughout the file which we should examine. In the original TBDEMO, there are over forty occurrences of a message send to
browse, which is the variable containing the TBrowse object. This variable gets passed around as a parameter among the ten functions.But with a class-based structure for TBDEMO, the TBrowse object is no longer a foreign entity, since TBrowse is dBrowse's superclass. This means that all TBrowse methods needed in dBrowse can be invoked by sending a message to
self. So all occurrences of browse: in the dBrowse methods were replaced with the double colon (::), the Class(y) shorthand for sending a message to self.The very fact that so many messages were being sent to the TBrowse object in TBDEMO was another strong hint that those functions should have been methods in the class or a subclass, such as dBrowse.
TBDemo()
This function demonstrates the use of the MyBrowse() function and does not require much explanation. A separate module has been created for this purpose, called DBROWDEM.PRG. The important lines from this module look something like this:
LOCAL dBrow := dBrowse():new(5, 5, 15, 70)
dBrow:autoFields()
dBrow:exec()
We have implemented the browse in three lines here. What each of these lines do is explained more fully below.
MyBrowse()
This is the main browsing function. It performs three major functions:
· A TBrowse object is created and some of its instance variables are initialized. In a class, this is usually the job of the initializer, so we have extracted this code to the
· In
StockBrowseNew(), a loop is used to create a TBColumn object for each field in the currently selected database table. This object is then added to the TBrowse object using addColumn(). To avoid making the dBrowse class so specific that it is only capable of browsing all fields in the current DBF, this section has been made into a separate method, called autoFields. It is then under the caller's control whether the columns to be browsed are taken from the current DBF or from some other source.· Finally, an event handling loop is entered which stabilizes the browse while waiting for a keystroke. When a key is pressed, a
CASE statement in the function ApplyKey() is used to execute the appropriate action. For dBrowse, the event handling loop has been made into a method called exec, since it is when this method is called that the browse is actually activated.Skipper()
This function controls moving the record pointer through the data. This has been made into a dBrowse method, also called
skipper(). The second parameter, browse, is no longer required - it is replaced by self which is automatically available inside a method.DoGet()
This function handles the editing of individual cells, or fields, in the browse. This has been made into a method, also called
doGet(). Again, the browse parameter is not required, since it is replaced by the implicit self.Where Did the Code Blocks Go?
Two methods in the dBrowse class have not yet been mentioned, since they do not have direct equivalents in TBDEMO. These are
goTop and goBottom. They are replacements for methods of the same name in the TBrowse class. This has been done more as an interesting exercise than because it is necessary. In the TBrowse class, these methods operate by evaluating the corresponding code blocks contained in the instance variables goTopBlock and goBottomBlock. This technique was needed to enable TBrowse to browse data from different sources, where the code to move to the top or bottom of the data would vary with the data source. Here again, as with the use of the cargo instance variable described earlier, a workaround has been used to compensate for Clipper's lack of an inheritance capability - in this case by using instance variables containing user-supplied code blocks. Using inheritance, these blocks become unnecessary, and the goTop and goBottom methods were reimplemented without using code blocks.Unfortunately, the same approach could not be taken with TBrowse's
skipBlock instance variable, since there is no single corresponding method which evaluates it. Rather, skipBlock is evaluated directly in a number of places inside TBrowse. This forces us to use a level of indirection, using the skipBlock code block merely to invoke our skipper method.How to Use the Class
The demonstration program, DBROWDEM.PRG, shows how a browse can be created and activated in only three lines. This could easily be done in one line, but the advantage of the former approach is that we can manipulate the dBrowse object in other ways, before starting the event loop using the
exec message. After all, dBrowse is a subclass of TBrowse, and as such has all of TBrowse's methods available to it. For example, instead of invoking the autoFields method, we could easily write a loop using the addColumn() message to create calculated fields, or to obtain data from multiple related DBFs.What Next?
What has been done so far provides little more than a start towards a powerful yet easy to use browsing class. The changes made to TBDEMO were deliberately kept to a minimum to illustrate how easy it can be to convert to a class-based design. As a result, our dBrowse class is not that much more useful than the original TBDEMO. The crucial difference is that it should be easier to expand and enhance - as we will now see.
First of all, dBrowse actually goes too far, too quickly. In one step from TBrowse, we have a class that implements an event loop, does field editing, and is tied to a database source. However, the event loop is quite a general piece of code, which we could use in, say, an array browsing class. The obvious solution here is to have an intermediate class, which we will call GenBrowse, as illustrated in the following class hierarchy diagram:

With this class structure, maximum expandability is achieved, with minimum rewriting of code. An important point to remember when dealing with inheritance is that you should only have to program the exceptions and additions in an inherited class - if you end up duplicating code in different classes, there is probably something wrong with the design. By moving the
exec method up into the GenBrowse class, we avoid such duplication.To take this one step further, it can easily be seen that at the bottom of the above tree we will need to inherit a new class from GenBrowse for each different data source we wish to browse. This is not totally sensible; after all, we are trying to develop a browsing class, not a data providing class - but the latter would in fact provide an excellent solution. If we had a general purpose data providing class, with standard methods such as
goTop, goBottom, and skip, we could have a subclass of this class for any type of data we might wish to handle - even, for example, SQL or Paradox data. Hmm... I think we've just invented RDDs - Replacable Database Drivers! A general purpose data providing class would give us the capability to implement a single general purpose browsing class which accepted a data-providing object as a parameter. We could then browse any type of data using the same browse class. In addition, we would not have to repeatedly implement data-providing methods in in every class which requires a data source - we would just pass data-providing objects around.Event Handling
Another area which could stand improvement is the key handling in the
exec method's event loop. At present, the key mappings - which keys are handled and what they do - cannot be changed without overriding the method in a subclass. To allow more flexibility, we could add an instance variable to the class to contain an array of paired values - a key value and a code block. By setting this instance variable to a suitable array, the keyboard mapping can be changed at will by the caller.Field Editing
There is one last slightly more mundane, but useful, enhancement which could easily be made to our browsing class system, relating to editing of fields in the browse. The code for editing a field in TBDEMO and dBrowse is fairly complex - but what if we want a read-only browse? We could override or otherwise disable the editing behaviour, but the unused code would remain in the class. Thinking about it, a browsing class is not the right place to put field editing code, anyway.
A more logical place to implement an
edit method is in the TBColumn class (or a subclass), since that is where the contents of individual cells is decided.So let's inherit two new classes from TBColumn - call them ROColumn and RWColumn - and implement an
edit method in each. ROColumn (read-only column) would have a dummy, or null, edit method which would do nothing, or perhaps just generate a beep. RWColumn would have a fully functional edit method, similar to the doGet method currently in the dBrowse class (which would now become redundant). In the generic event loop in the GenBrowse class, we could specify that hitting the ENTER key, say, would cause an edit message to be sent to the currently selected column object, which could be either an ROColumn or an RWColumn object. This would look something like the following:CASE nKey == K_ENTER
columnObj:edit
Editing would then take place, or not, depending on what type of column object had been inserted when the browse object was set up. No IF statement is necessary here. During setup, read-only and editable columns could easily be mixed in the same browse.
Summary
The system proposed here would provide a set of classes which would meet the requirement for a powerful, easy to use, and flexible browsing system. It would be easy to use because it would have default behaviours, requiring very little code to implement in a calling program. It would be flexible because other behaviour could be implemented by inheriting new classes from the existing ones. No functionality has been lost, because methods all the way up the tree to TBrowse can still be used as required. Having an object-based solution also makes it easy to implemement multiple simultaneously active browses - something much harder to do with a procedural approach (without simulating objects).
The combination of all of these capabilities is undeniably very powerful, and is a good example of the kind of benefits which can be achieved with object-oriented programming.