Working with objects and classes Foundation
In the following chapters basic programming tasks and features of the Xbase++ object-oriented programming model are explained. However, object-oriented programming is more than just using particular features. OOP rather is a new way of thinking with regard to the process of analyzing problems and developing solutions.
Keep in mind: in object-oriented programming, a program is considered to be a set of entities which are loosely interconnected. These entities are named objects (or instances) and each object is responsible for specific tasks.
A class defines the objects that perform specific tasks within a program. The class defines the instance variables and methods for objects of the class. All objects of a class have the same set of instance variables and can execute the same methods. Two objects of the same class can have different values in their instance variables, but they execute the same methods. When a new object of a class is created, the process is called creating an instance. An object is an instance of a class. The following is an example of a class declaration:
This declaration defines the cursor class. Objects of this class are designed to control the screen cursor. Each object of the class has three instance variables (declared by the VAR statement) which contain the cursor coordinates and cursor shape. Each object also has two methods (declared by the METHOD statement) which "show" or "hide" the cursor. The class defines the interface that the programmer must use when he works with objects of the class. It defines names for the instance variables and methods of each object belonging to the class.
The keyword EXPORTED: specifies that access to the instance variables of the class is permitted from outside the program code of the methods of the class. The methods of each object created from the example class can also be executed from anywhere in the program (more information about visibility of methods and instance variables can be found in the description of EXPORTED:, PROTECTED: and HIDDEN: in the reference documentation).
A class is defined by the declaration CLASS <ClassName> ... ENDCLASS. This declaration instructs the compiler to create a function with the name <ClassName> and to designate this function as a class function. The return value of a class function is the class object representing the declared class. The class object includes the method :new() which is used to create objects of the class. New objects have the instance variables and methods specified in the class declaration. The following code shows an example of creating a class object and an instance of this class:
The class object declared by CLASS Cursor is returned by the class function Cursor(). When the class object executes the method :new(), it returns a new object of the Cursor class.
The fact that a class object is itself an Iobject needs to be emphasized since the terminology is confusing. The class object is an independent part of the class. One of the tasks of the class object is to generate new objects (instances). The class object always includes a method called :new() and a method called :className(). The class object can execute additional methods and can have additional member variables (class variables) if they are specified in the class declaration.
In order to be precise, it is necessary to distinguish between "instance object" and "class object". Both are objects in the sense of data type. However, the term "object" is generally used as a shortened version of "instance object" or to identify a variable that contains a value of the "object" data type.
After declaring a class using CLASS...ENDCLASS, the declared methods must be programmed. Programming methods is very similar to programming user-defined functions, except that the declaration METHOD must be used instead of the declaration FUNCTION. Within a source code file (PRG file) the name for a method may be used only one time per class. When additional classes are programmed in the same PRG file, methods within the file can have the same name if they exist in different classes. The following code shows an example of the program code for methods:
The example shows that the methods are first declared within CLASS...ENDCLASS using the keyword METHOD. Then the METHOD declarations appear at the start of the program code for each of the declared methods. The name of the class where the method is declared can optionally be placed in front of the method name. This allows several small classes to be programmed in a single file even if different classes have methods with the same name. However, it is good programming style to preceed a method's name with the name of the class it belongs to, even if a single class is programmed in one file. Furthermore is it necessary that all methods declared in a class be implemented in the same PRG file where the class declaration is located. Otherwise the compiler will generate an error message.
In order to access the value of an instance variable in an object, the instance variable name must be sent to the object using the send operator : (the colon). The following is an example:
An object reference is contained in the variable oCursor. This object's instance variable named nRow is accessed using the send operator. The process can be viewed as sending a message containing the name of an instance variable to the object. The object answers the message by returning the value of the corresponding instance variable. Programming with objects resembles a conversation where messages are sent to objects, the object returns an answer and the program continues.
One special aspect of objects is that they can return not only the value of an instance variable but also the RETURN value of a method in response to a message. Within a method any program code can be executed. Methods are different from user-defined functions in that the object executing the method is always visible within the context of the method. This means that all member variables of an object can be accessed within a method. Generally, instance variables of the object are processed within the program code of a method. An object executes a method when it receives the name of the method followed by parentheses () as a message via the send operator. Examples of how methods are called are shown in the following code:
The message :Show() instructs the object to execute the program code of the show method declared using the keyword METHOD in the oCursor class definition.
As already outlined earlier in this chapter, a message is passed to an object (receiver) which then carries out an action by executing a method or accessing an member variable. In situations when it is necessary to access the receiver object within a method body, a "pseudo-variable" SELF is used. SELF is like an ordinary variable, only it need not to be declared and cannot be modified. SELF can be used inside of methods as if it refers to the object the message is sent to.
This behavior is shown in the code below. The message bark is send to an object of class Output. The object receives the message and decides which method is to be executed. In this case, the method :Bark() of the class Output is executed. The method :Bark() in turn uses SELF:Say(...) to send the message say to the same object the message bark was sent to. In other words, SELF in method :Bark() references the same object instance as the variable oOutput in the procedure Main().
The operator :: is an abbreviated way of writing SELF:. It is used to send messages only to the object SELF. The :: operator and SELF can both be used only inside a method implementation.
Immediately after an object is created, all its instance variables contain the value NIL. In most cases these instance variables should be assigned default values. The optional method :init() can be specified in the declaration of a class. If it is included, this method is automatically executed right after an object is created. The example of the cursor class is expanded in the code below to include an :init() method:
Within the method :init(), the instance variables of the previously generated object are initialized with default values. The characters :: are used as an abbreviation for self:. This means the code for the :init() method could also be written as:
A LOCAL variable with the name self implicitly exists in the program code of all methods. This variable contains a reference to the object executing the method. Any attempt to redeclare the variable self or assign a value to this variable leads to a syntax error during compiling.
The term inheritance describes a very important mechanism in OOP. Using inheritance, a new class can be declared based on previously defined classes. The new class is derived from the existing class and assumes the entire structure of the existing class, including its member variables and all the program code of its methods. The new class has access to the program code of the existing class. This code does not have to be available as source code but can be in a compiled form in a LIB or DLL file. The following two class declarations show the basic mechanism of inheritance:
The class ClassB is derived from the class ClassA and inherits its :show() method and the :iVar instance variable. ClassB has its own :init() method which initializes the inherited instance variable with a different value than the value assigned in the :init() method of ClassA. Objects of ClassB can execute the method :show(), even though the corresponding program code is in ClassA. The following program code illustrates this relationship:
ObjectB executes the :show() method of ClassA, because ClassB is derived from this class. In ClassB the :init() method is redefined and it is this method that is executed after objectB is created, rather than the :init() method from ClassA. The :init() method from ClassA has been overridden in ClassB. The :iVar in objectB contains the character "B" which is output on the screen using the method :show(). Since ClassB inherits the methods of ClassA, objects of ClassB can also execute the :init() method of ClassA as shown below:
When inherited methods are overridden, the method in the superclass can be executed if the corresponding superclass name is sent to the object before the name of the method. In the code above, ClassA is the superclass of ClassB as shown in the earlier examples.
Sending the name of the superclass to an instance is called a cast. A cast changes the calling context of a method call to one of the superclasses of the instance. A cast is not a member variable. Attempting to save a cast implicitly reverts the cast back to the original instance:
Because the cast is created for a specific context, it is usable in this context only. Saving casts is prevented to avoid unpredictable behavior if executed in a different calling context.
In addition to SELF, Xbase++ provides another "pseudo-variable" named SUPER. Like SELF, SUPER refers to the receiver of the message for the method currently being executed. However, using SUPER for sending messages is different than using SELF. A message passed to SELF locates the corresponding method or member variable within the class of the receiver. A message passed to SUPER, on the other hand, invokes the method or member variable declared in a superclass if one exists.
The following code shows the usage of SUPER and SELF. In the Init() method of the class Dog, SELF:Name is used to access the member variable Name. Similarly, SELF is used to access the same member variable in the method Say(). However, Say() also uses SUPER:Say() to send the message Say to the baseclass. Because the class Dog is derived from Animal and Output, the message sent using SUPER is automatically routed to the Output class and its Say() method implementation. This example illustrates how functionality inherited from a baseclass can be used to augment the behaviour of a derived class. In this case, the method Say() in the class Output (which knows how to output text) is used within a method overridden in the derived class Dog.
In addition to the SUPER "pseudo-variable", there also is a SUPER statement which is shown in the code snippet below. Using the SUPER statement, the current message is automatically passed to the baseclass, along with all its parameters. In this case, the Say() method in the baseclass Output is executed. Note that the message name is automatically inserted by the compiler. Using this approach, the code becomes more robust against parameter list changes. Likewise, relocating a method from one baseclass to another does not affect the implementation in derived classes. Therefore, the SUPER statement should be preferred to using explicit casts such as SELF:baseclass:methodname(). Because of its independence from the message name, using SUPER is also recommended over sending messages directly via the send operator (:) as in the following statement: SUPER:methodname().
In contrast to other dynamic languages, Xbase++ does not actually create classes dynamically by default. Instead, class declarations are processed by the compiler which creates the corresponding class objects and the class functions for accessing the classes at runtime. The advantage of this approach is a performance gain because many calculations required for accessing member variables or methods can be done at compile-time.
To create a class at runtime, the function ClassCreate() has to be used. This function creates a class object based on definitions gathered at runtime. Behavior is added to dynamic classes using code blocks. Once created, a dynamic class object and its instance objects behave exactly like a compile-time class and its instances. Because classes behave the same irrespective of how they were created, class names can be considered a type name with a guaranteed behavior and state.
For more details see the function ClassCreate().
The example below implements the user-defined function DbRecord() which creates a class object for an open table. Objects of the class have instance variables whose names match with the field names of the database. The program code for the methods :init(), :get() and :put() is provided by code blocks which call STATIC functions. There, the instance variables of an object are accessed using the macro operator.
Even though objects with arbitrary state and behavior can be defined through a dynamic class created with ClassCreate(), the state and behaviour of an object cannot be changed once its class is created. In other words, it is not normally possible to have different kinds of object instances without first creating a class.
However, that is exactly what DataObjects are designed for: they are objects without a class. Even though DataObject instances are created the same way as other instances (by executing the :new() method of a DataObject class object), each instance can have its own state and behavior. The following sections discuss several usage scenarios for DataObjects. Also demonstrated is a powerful new way of dynamic object-oriented programming which is made possible by the unique properties of the DataObject class.
Adding new instance variables
Adding instance variables to a data object instance is done simply by assigning a value to a member. Instead of raising a runtime error due to the access of a non-existing member, data objects create the corresponding instance variable the first time a value is assigned. This behavior is analogous to the behaviour of dynamic variables such as privates or publics.
Accessing a member variable which does not exist always returns NIL. No runtime error is raised in this case.
The dynamic member mechanism of DataObjects fully supports reflection. Therefore, IsMemberVar() and :ClassDescribe() reflect dynamic members. Likewise, dynamic members are visible in the debugger and the macro execution engine. Most importantly, there is no performance penalty compared to ordinary/declared members after the dynamic member is created with the first assignment.
In order to add functionality (behavior) to an existing DataObject, :defineMethod( <cMessage>, <cFunction>|<bCodeblock> ) must be used. Behavior can be implemented using either a code block or a function. In both cases, the first parameter passed is the DataObject to which the message is sent. Existing behavior can be redefined in the same fashion.
Sometimes, objects with the same function and member definition need to be created, for example, if DataObjects are used to store records of a table or cursor. To create objects which are identical in terms of initial state and behavior, the method :Copy() can be used. In this case, the data object instance which defines the desired set of members and methods is also referred to as a prototype. In addition to being identical at the time of creation, data objects created from a prototype also retain the same structure throughout their lifetime. For example, if a new member is added to one of the objects, all other data object instances simultaneously support that member, too. A redefiniton of an existing method or the addition of a new method also affects all other object instances created using the :Copy() method. The following example outlines this behavior.
Working with DataObjects is like using objects of any other class. However, DataObjects do not necessarily share the same state and behavior as it is the case with ordinary class-based objects. Data objects therefore do not give the same guarantee regarding their exported interface a classes does. However, due to their dynamic nature data objects are perfectly suited for being used as a universal data container in Xbase++ applications.
In object-oriented programing languages reflection allows the inspection of classes, interfaces, members and methods at runtime without knowing the names of these entities at compile-time. Reflection also allows to create new object instances and to execute methods.
Xbase++ as a dynamic language implements reflection using dedicated methods and functions, but also with language features such as macro expansion and code block evaluation.
Getting information about methods and members
Using the function IsMemberVar(), it is easy to test if an object defines a certain member variable. Similarly, the function IsMethod() determines if a certain method is defined in a particular object instance.
More details about an object can be retrieved using the method :ClassDescribe(). This method retrieves a complete description of a class. For example, :classDescribe() can be used to retrieve an array with the names of all exported member variables defined in the class.
Class names and inheritance
The method :className() is used to retrieve the name of the class an object is an instance of. Using :isDerivedFrom() allows to test whether an object is an instance of a sub-class of a given class. :isDerivedFrom() method is generally used to test for interface guarantees.
Dynamically creating object instances
Using the function ClassObject(), the class object for a class can be queried. An alternative way for retrieving a class object is by using the macro operator to execute the corresponding class function. Once the class object is known, an instance of the class can be created by sending :new() to the class object as shown in the following example.
Macros, codeblocks and objects
The object-oriented programming model fully supports macros and code blocks. The following code illustrates different usage patterns.
If you see anything in the documentation that is not correct, does not match your experience with the particular feature or requires further clarification, please use this form to report a documentation issue.