Brian Long (www.blong.com)
This month Brian Long looks at the Object Pascal support for building your own objects in Kylix applications.
This article first appeared in Linux Format Issue 30, August 2002.
Click here to download the files associated with this article.
If you find this article useful then please consider making a donation. It will be appreciated however big or small it might be and will encourage Brian to continue researching and writing about interesting subjects in the future.
Last month we had some fun doing some simple animation. In the example provided we had a single layer of animation drawn across a background. It is very possible to use additional bitmaps to build extra animation layers, just as with cartoon animation.
We might come back to this area later in the series and see what we can achieve, but this month we focus our attention on OOP (Object Oriented Programming) support in Object Pascal.
Object oriented programming is really a means to allow programmers to mirror the real world a little more closely than may be possible otherwise. If you have the ability to create a model in your program that closely mirrors what you are trying to represent then the theory goes that your job will be easier and the resultant code will be easier to maintain.
Kylix is an OOP tool, which means it allows you to write classes that exhibit the three so-called cornerstones of OOP: encapsulation, inheritance and polymorphism. We'll try and get an understanding of each of these principles this month whilst developing some sample classes. Additionally we'll see how properties are defined in order to provide "storage-with-side-effects".
To start off we should get a few basics out of the way. An object is a programmatic data entity that contains everything required to represent something. What the "something" is completely dependent on the application and the object, but is often something from the real world that is being modelled to a greater or lesser extent.
In the real world any arbitrary object (say, for example, a pencil) has various attributes (such as the length, mass, density, lead hardness and bluntness) and behaviour (such as the ability to make a mark on something when the tip makes contact). To represent a pencil in a program you would first define a class type.
A class is a data type that defines the template that all pencils should follow. It defines the attributes as data fields and/or properties and the behaviour as methods. Once the class has been defined you can create instances of the class; each instance is called an object.
A component in Kylix is an object that has built-in behaviour to allow it to interact with the Kylix IDE (notably the Form Designer and Object Inspector). Components can be stored in form files when you save your project, which are compiled into your executable during the compile/link cycle. This allows the form that you set up at design-time to be recreated as needed when the program is running.
Nevertheless, components are objects and so follow the definition of an object. For example, a TEdit component represents an edit widget. It has properties to reflect the widget attributes such as Text, Top, Left and Color. It also has methods to surface the widget behaviour such as Clear, PasteFromClipboard and Hide.
When you design a form using the Form Designer, Kylix defines a form class to allow programmatic access to the form's attributes (including all the components you drop on it) and behaviour (the event handlers you set up for the form and the components on it). A form is actually a special type of component.
Kylix comes with many classes that do not meet the criteria to be components or forms and so are not component classes; they are simple object classes. Since component classes add in extra complications we will concentrate on simple object classes in this foray.
Let's start our look at class-writing syntax by defining a simple class that represents a car. Encapsulation allows us to define the data attributes and any related routines that typically act upon that data in the same class, rather than declaring global variables and global routines.
The basic type definition looks like this and would be found in the interface section of a unit (unless you didn't want other units to access it, in which case it would be in the implementation section):
This defines a basic class called TCar, but which is empty. Note the convention of prefixing any type definitions with T (to imply it's a type). We'll add data attribute to represent the speed of the car and whether it's parked and a couple of methods that let the car be parked and driven. The method implementations must be placed in the implementation section of the defining unit, regardless of which section the class is defined in.
This is a simple example and there are various potential integrity problems with it.
It is generally accepted that when writing classes you should employ data hiding to protect the integrity of data items. Take the class above; there is nothing to stop any code in an application changing the value of the Speed field to a ridiculously high value. Also, there is nothing to stop some code directly changing Parked to True, even if Speed is set to 100.
In other words, when data fields are directly exposed from a class, there is no protection against their values being modified in error. To avoid this kind of error requires a mechanism to hide the data items from code that uses objects of your class type. Instead, code outside the class must use routines to access the data. There is a routine to read the data value (the getter routine) and another to update the value (the setter routine).
The setter can employ validation code to ensure no invalid value is assigned to the underlying field. The getter typically just returns the underlying field value, but can run any additional code you feel may be necessary when returning the value.
There are various levels of access restriction you can assign to your class members. Of interest right now are the private and public access levels. There are two other levels, but we will look at those later.
Defining something as public allows code outside the class to access it, whereas marking it private prevents any code outside the unit that defines the class from accessing it. Following this idea we might modify the class to look like this:
Note that a convention is being applied here where private data fields start with an F (for field).
With this implementation no outside code can change the FParked value without going through the SetParked routine. Similarly nothing can change the car speed without going through SetSpeed. Admittedly, SetSpeed does no validation (such as limiting its top speed) but we'll add that in soon. The important thing is that code has to execute in the class to change the value of a class field.
One downside to this data hiding approach is that you have two different routines to call to read and write a data member of a class (such as SetSpeed and GetSpeed). Object Pascal simplifies this issue by introducing support for properties.
A property is a mechanism that exposes an item from a class that look just like a data attribute, but which is defined in terms of what happens when it is read from or written to. You can define a property to call a getter and setter routine when it is accessed, as in:
This would allow you write code like this:
instead of this:
You can read and write using a single identifier (the property) rather than calling two routines (the getter and setter).
However, if the getter would simply return a private data field, you can specify that the property returns it directly and save the trouble of a pointless routine. Similarly if you need no validation when setting the value, you can specify that the property writes directly to the data field.
You can also define read-only and write-only properties and there is also support for array properties (implemented with the array indexer passed to the getter/setter as an extra parameter) and indexed properties (where several properties are implemented with the same getter/setter, which is passed an index value to identify the property).
We can rewrite the class like this (the method implementations that remain are exactly the same as before):
Our car class may well do a simple job of representing a car, but things are more interesting and slightly more realistic if we introduce some classes that represent particular types of car. We can simplify this task by using inheritance.
When you hear someone being passionate about OOP you can be sure the word "reuse" will crop up at some point. Reuse is enabled by inheritance which allows a new class to be defined as a superset of an existing class. If you can locate a class that provides a proportion of what you need from a new class, you can make your new class inherit the existing class, which represents your new class's starting point. The old class is called the ancestor of the new class; the new class is a descendant of its ancestor.
In a rich class library (such as CLX) there will be many classes that inherit from other classes. When designing a class hierarchy for an application, careful thought should be put to what classes exists and what inherits from what. Any common code should be implemented in a class, and the various specifics can be implemented in descendants of the common ancestor. For example, all components inherit from the TComponent class, which defines all the functionality and properties necessary to be worked with at design-time.
All Object Pascal classes you define must inherit from TObject, either directly or by inheriting something that is already inherited from TObject.
TObject is a base class of any Object Pascal class hierarchy - it is the ultimate ancestor of every class and so sits right at the top of the hierarchy (or at the very bottom, depending which way up you picture it). It provides various simple services required by the Kylix RTL (BaseCLX). For example every class has a ClassName method inherited from TObject that returns the class name as a string. So, for example, Button1.ClassName returns the string TButton.
Figure 1 shows a representative illustration of the inheritance that leads to the TBitBtn class.
Figure 1: A representation of the inheritance present in a TBitBtn
The classes we have looked at so far have not specified an ancestor and so implicitly inherit directly from TObject. You can make this explicit by changing the class definition to:
Note that Object Pascal implements single inheritance, which means a class can inherit directly from a single class, not from multiple classes as you can in C++, which supports multiple inheritance.
We can build a small hierarchy that allows specific cars to be implemented using TCar as a base class and inheriting its generic car behaviour. Each descendant will introduce the specifics of that type of car.
Additionally we can, if we wish, define custom exception classes inherited from Exception. These exceptions can be raised to represent any error conditions. So we can re-implement CarError and TCar.SetParked as:
With an understanding of inheritance we can now properly cover the data access levels. These are defined in terms of how they affect a class writer and an object user. A class writer is at liberty to inherit from any class you define and an object user can create an instance of your class and try to access its members.
You declare something as private if you wish to exclude it from class writers and object users, meaning it is only for your use. Data fields are commonly declared private.
You declare something as protected if you wish to exclude it from object users, but still leave it available for class writers. Property getters and setters whose implementations need to be extended or modified by descendant classes will typically be declared protected.
You declare something as public if you wish to allow class writers and object users to have access to it. Anything you want accessible at runtime is typically declared public.
You declare a component property as published if you wish to exhibit the same behaviour at runtime as if it were public, but wish it to appear on the Object Inspector at design time. Since we are not writing components here, we have no need for this access level.
Note that to be perfectly accurate, private and protected items can be accessed from other code in the same unit as the class, however it is typical that the class writer's class or object user's code would be in a different unit to your class.
Before implementing the descendants let's extend the base class a bit more. Since any given car has a maximum attainable speed we should make this information available (but not modifiable) and also adhere to it (i.e. it should employ some validation). Also the car should start its life in a parked state.
We can achieve this by setting the top speed and the parked value in the constructor, which is a special method called to initialise a freshly created object. Every Kylix class has a constructor called Create; the one inherited from TObject takes no arguments but many classes redefine the constructor to accept appropriate arguments to initialise the object. Additionally, a read-only property can make the top speed available.
Notice in the code below the constructor calls the inherited constructor to ensure that any initialisation code inherited from the ancestor is not lost.
Also note that the SetTopSpeed protected method is there to allow class descendants to still get a value assigned to FTopSpeed. If a TCar descendant were defined in some other unit, it would not have access to the FTopSpeed private data field.
When the object is destroyed by a call to its Free method another special method, the destructor, is called to do any necessary tidying up. Every Kylix class has a destructor called Destroy.
Constructors and destructors are examples of methods that should definitely call their inherited versions to ensure important setup/tidy-up code is still executed. For other methods, whether you call the inherited version depends on the circumstances. If you do call the inherited code, you will be extending the behaviour of the ancestor's method, otherwise you will be changing it.
We can now implement the descendant classes, which will set the top speed in their constructor and implement custom driving behaviour.
With these classes complete we can test them. A test application is supplied on this month's disk called ObjectsEg.dpr that has the following event handlers set up for the form's OnCreate and OnDestroy events and the OnClick events of a couple of buttons:
As you can see, test car objects are created (and set to an unparked state) when the form is created and destroyed when the form is destroyed. Each button sets the speed of the associated car to a random permissible value and then drives it. The results of this test are quite successful (see Figure 2), but there are circumstances where the behaviour won't be as predictable and correct.
Figure 2: A sample object executing a method
One of the consequences of inheritance is that we get a class hierarchy and in that hierarchy are many branches. In any given branch there will be a base class and various descendants that are all supersets of that base class. Rather than treat instances of all these classes (of which there may be lots) using the correct types at all times, it can often be more convenient to treat them all like their common base class.
Our application involves a small class hierarchy branch with a common base class of TCar. If we wanted to represent lots of car objects we might declare an array of TMorrisMinor objects and another array of TPorsche911 objects. Any operation we wanted to perform against all cars would need to be executed in two loops, one for each array. If any new TCar descendants were added to the hierarchy we would need to add a new array and go back to each place that we looped through the two arrays and add in another loop for the new array.
That would pose a maintenance problem, so it is usually better to declare a single array of TCar objects and just add all car objects to the single array. Anything inherited from TCar can be treated like a TCar (as it is a superset of one) so it doesn't matter how many new classes are defined - the single array will suffice.
So we could add in a new data field (a TCar array) to the form, and initialise it when the form is created:
We can then add two more buttons to the form that share an OnClick event handler. If we set the Tag property of each button to identify an element of the array then the event handler can read the Tag of the pertinent button, then index into the array to access the TCar descendant.
The idea is that either the Morris Minor or the Porsche will be represented by the Car object reference, so the appropriate top speed will be used as an upper limit for the random speed, and the car will then be driven.
Unfortunately the plan doesn't quite work out as you can see in Figure 3. Despite pressing the new button that should drive the Porsche, the Porsche driving message is not displayed (and neither is the Morris Minor's when you press that button). This is because we have not used polymorphism.
Figure 3: A lack of polymorphism makes generic coding a problem
Unless otherwise instructed, the compiler will work out what the target address of a call to any routine or method at compile time from the information it has to hand (the source code). In this case the compiler finds a TCar reference and so translates the call to Car.Drive into a jump to TCar.Drive. This is the natural behaviour of a compiler and is sometimes called early binding (routine calls are bound to target addresses before the program is executed).
In order to allow this generic representation of objects to be a useful technique we need the correct implementation of Drive to be executed, based upon which object is actually found at runtime. So if the Car object is actually a TMorrisMinor we need TMorrisMinor.Drive to be executed and the same applies for the Porsche. For this to work we need the compiler not to do early binding for this method; instead we need it do late binding, which means deciding which address to jump to at runtime.
Polymorphism (or late binding) allows you to call the same method on any object in a given branch of the class hierarchy (no matter how the object is being represented) and for the correct behaviour to ensue.
Since this behaviour is not the compiler's default we need to specifically mark any methods that require polymorphism. When you add a new method to a class that needs to be polymorphic you must use the virtual directive. If you need to redefine a polymorphic method, in order to either change or extend it, use the override directive.
So SetTopSpeed and Drive are marked as new polymorphic methods in TCar. The two descendant classes override the Drive implementation, but keep the inherited SetTopSpeed implementation. With these changes the program goes back to working just fine.
Note that the constructor is not marked polymorphic; polymorphism typically results from treating different object instances through the same type, such as TCar. A constructor is called through the class itself, not through an instance, so there is generally little point in marking it virtual.
Borland's annual conference (BorCon 2002) was held this year in May in Anaheim, California. Of interest to Kylix developers (and also developers who like C++, but don't use Kylix) was information about the next version of Kylix that was given in the Product Address session.
It seems that Kylix 3 (codenamed Cortez) will include the Object Pascal language support that has been present in Kylix 1 and Kylix 2, but will also include the first version of Borland's C++ compiler for the Linux platform. This means that Kylix developers will have the choice of writing CLX applications either in Object Pascal or C++.
Additionally, Linux programmers who are familiar with the Linux and/or X windows APIs can write non-CLX applications as they have done previously, but in a fully functional integrated development environment. Kylix 3 is expected to be released later this year, so we'll keep you up to date with news about it. At this stage there has been no information on what will be offered in the Open Edition of Kylix 3, but we'll keep our ear to the ground.
[Of course the release of Kylix 3 has since been and gone (July 2002), and there is indeed a Kylix 3 Open Edition]
Next month we'll look at more OOP support in Kylix. In the meantime, if there is something about Kylix Open Edition you want to see covered here, drop us an email and we'll try our best to incorporate it into a future instalment.
Brian Long used to work at Borland UK, performing a number of duties including Technical Support on all the programming tools. Since leaving in 1995, Brian has spent the intervening years as a trainer, trouble-shooter and mentor focusing on the use of the C#, Delphi and C++ languages, and of the Win32 and .NET platforms. In his spare time Brian actively researches and employs strategies for the convenient identification, isolation and removal of malware.
If you need training in these areas or need solutions to problems you have with them, please get in touch or visit Brian's Web site.
Brian authored a Borland Pascal problem-solving book in 1994 and occasionally acts as a Technical Editor for Wiley (previously Sybex); he was the Technical Editor for Mastering Delphi 7 and Mastering Delphi 2005 and also contributed a chapter to Delphi for .NET Developer Guide. Brian is a regular columnist in The Delphi Magazine and has had numerous articles published in Developer's Review, Computing, Delphi Developer's Journal and EXE Magazine. He was nominated for the Spirit of Delphi 2000 award.
Back to top