Web Tech Training & Development
Article: OOP | Author: Rick Spence

 

 

Object Oriented Programming in Delphi
A Guide for Beginners

Object Pascal, Delphi’s underlying language, is a fully object oriented language. Simply, this means that the language allows the programmer to create and manipulate objects. In more detail, this means that the language implements the four principles of object oriented programming:

  • Data Abstraction
  • Encapsulation
  • Inheritance
  • Polymorphism

As you’ll see, these are complicated names for pretty simple ideas.

In teaching 100s of Delphi programmers, I’ve found that getting to grips with Object Oriented programming is the difference between just getting by with Delphi, and really making the most of the product. In this article and the next, I’ll introduce Delphi programmers to the Object Oriented features in Object Pascal, and show how to take advantage of them in your own applications. Even if you’ve used Delphi for a while, you may find these articles a useful review - it’s amazing how much you can do with Delphi without really understanding the principles of the language.

This article starts of with a couple of simple definitions. It starts by describing object-oriented programming in general terms, then precisely defines two terms you’ve no doubt heard – object and class. It then proceeds to look at the language mechanisms you use to work with objects, and shows how to ensure your objects are released correctly (i.e. that your program does not have any resource leaks). Following this, the article discusses, in detail, the syntax you use to create classes; you’ll see a real-world class you could immediately put to use in your applications.

What is OOP? What’s an Object? How about Class?

Object Oriented Programming (OOP for short), is all about writing programs that manipulate objects. Delphi, along with C++ and Java, is a fully object oriented language. As you’ll see, the principles of object oriented programming are the same in all these languages, though of course the syntax is different. Once you’ve learned the principles, however, no matter which language you learn them with, you’ll find that knowledge transfers easily to other languages. Concepts such as inheritance and data abstraction are the same in C++, Java, and Delphi – it’s just the language syntax that differs.

Whether you’ve used an object oriented language or not, you’ve probably heard the terms object and class thrown around. A class is a programming construct developers use to specify and implement new data types. All languages come with predefined data types – such as integers and strings. Object oriented languages allow programmers to create their own data types, such as students, accounts, and menus. Since these data types are not built-in to the language or the underlying computer hardware, we call these abstract data types.

The language mechanism programmers use to do this is called a class. A class, then, is a specification and implementation of an abstract data type. If you’ve never used an object oriented programming language before, this concept will be new to you. Look at it like this. Whatever language you have used before, you’re used to using that language’s data types – strings, integers, reals, booleans, etc. A class allows a programmer to create his or her own data types which they can proceed to use just like the predefined data types. Delphi’s VCL (Visual Component Library) is simply a collection of classes written by Borland / Inprise staff. Things you’ve no doubt used in your Delphi applications – such as forms, tables, queries, radio buttons, check boxes etc. are all classes defined in the VCL.

As you’ll see, you can create your own classes and use them in the same way. Later in this article we’ll cover the syntax in detail. For now, consider the following class declaration:

Type
TStudent = Class
FLName : Integer;
FFName : Integer;
FTel : String;
End;

This declaration declares a new data type called TStudent. The TStudent data type is represented by 3 pieces of information – the last name (FLName), the first name (FFName), and the telephone number (FTel). Your program can now proceed to declare variables of type TStudent:

Var
Student1 : TStudent;
Student2 : TStudent;

The only difference between this, and declaring variables with types that are declared in the VCL, as in:

Var
StringList1 : TStringList;
IniFile1 : TIniFile;

is that with the former you have declared your own data type (TStudent), whereas with the latter the VCL declared the data types (TStringList and TIniFile).

In order for the compiler to find the declaration of the classes you use, your program must explicitly use the unit declaring the classes. If you created a program unit containing a form, Delphi will automatically generate a USES clause for you which lists the most commonly used units inside the VCL. This is why your program can refer to classes such as forms, checkboxes, and push buttons without explicitly listing the units those classes reside in. If you didn’t create a form, however, i.e. you have an empty unit, Delphi did generate this USES clause and if you attempt to reference any VCL classes you will receive compilation errors. The same rules apply to your own classes. If one unit in your application references a class declared in another unit, the first unit must list the second unit in its USES clause.

We’ve discussed classes, but what’s an object? That’s easy. An object is simply an instance of a class – a variable whose data type is a class. Student1 is an Object. So is Student2. Likewise StringList1 and IniFile1. The term object, then, is a term used to describe any variable whose type is a class. Objects, of course can be of any class, so when describing an object you usually use its class name as well – thus you will talk about stringlist objects, form objects, student objects, etc.

Working with Objects

Before we get into writing our own classes, a quick review on how to work with classes and objects. We’ll use Delphi’s TStringList class (declared in the VCL) to illustrate our points. The first step is to declare a variable of type TStringlist.

Var
StringList1 : TStringList; // TStringList is the class –
// StringList1 the object

Where do place this declaration? It depends upon where you want to use the object (its scope), and how long you want to use it for (its lifetime). If you only want to use it inside a subroutine then declare it inside that subroutine:

Procedure Test;

Var
StringList1 : StringList;

Begin
// Work with StringList1 here...

End;

In this case, as soon as the procedure Test terminates, you cannot access the object; you can only access it from within that subroutine. If you want an object to have a wider scope and a longer lifetime you must declare it outside a subroutine. If you place it inside the unit’s implementation section, outside of any sub-routine, the object is visible throughout that unit, but only inside that unit. If you place it inside the unit’s interface section, then the object is visible both throughout this unit, and other units which use this unit.

Now, regardless of where you declare the object, your program is responsible for both allocating and releasing its memory. This is the main difference between working with objects and working with simple variables. When you work with objects you are responsible for both allocating and releasing their memory. When you work with simple variables, after declaring the variable you can use it immediately, as in:

Var
i : Integer;

Begin
i := 10;

Simply declaring the variable allocates its memory. When working with an object, however, you must first allocate its memory:

Var
Student1 : TStudent;

Begin
// Allocate memory for the object

To allocate the memory you must call a special routine called a constructor. A class’s constructor can be named anything, and indeed classes can have more than one constructor. Most classes, however, declare one constructor called Create. You’ll look at writing your own constructors later in this article when you learn how to write your own classes. For now we are using predefined classes, so we only need to be concerned with their constructor. The constructor for the TStringList class is called Create. To call the constructor you prefix the constructor name with the class name, as in:

TStudent1.Create;

The constructor is actually a function which returns a pointer to the memory it allocated. So, to allocate the memory for the object, you call Create, using the following syntax:

Var
Student1 : TStudent;
Begin
// Allocate memory for the object – note the general form:
// <Object> := <ClassName>.<ConstructorName>;
Student1 := TStudent.Create;

This is called instantiating the class. Remember the form of the call to the constructor:

<Object> := <ClassName>.<ConstructorName>;

The biggest mistake people make when getting started with Delphi’s objects is forgetting to call the class’s constructor, or calling it incorrectly.

Once you’ve allocated the memory for the class you can then access its data using the dot operator, as in:

Student1 := TStudent.Create;
Student1.FLname := ‘Spence’;
Student1.FFName := ‘Rick’;

If you try and access the data without first allocating the memory (i.e. forgetting to instantiate the class) you will receive run-time errors.

You can of course work with multiple objects:

Var
Student1 : TStudent;
Student2 : TStudent;
Begin
// Allocate memory for the objects
Student1 := TStudent.Create;
Student2 := TStudent.Create;

and each object has its own data. In this example, Student1 has three pieces of data associated with it, and so does Student2:

Student1 := TStudent.Create;
Student2 := TStudent.Create;

Student1.FLName := ‘Spence’;
Student2.FLName := ‘Brown’;

Now, you are also responsible for releasing the object’s memory. To do this you call another routine called free. However, you must prefix free with the object name, as in:

Student1.Free;

Note this is not symmetrical. You prefix the constructor name with the class name, but prefix free with the object name. In the next article you’ll see that this difference is to do with calling a piece of code to work on an object (a regular method), and calling a piece of code to work on a class (a class method).

Here’s an entire routine which declares, instantiates, and releases a TStringList object.

Procedure Test;

Var
StringList1 : StringList;

Begin
// Call the constructor to allocate the memory
StringList1 := TStringList1.Create;

// Work with stringlist1 here...

// Release its memory here
StringList1.Free;

End;

In this example we allocated and freed the memory in the same routine. This is fine in this case, as the object is only visible inside this routine. If the object were visible throughout the unit, however, you have to decide when to release its memory. It’s very common to have objects exist as long as a form. That is, a form may need to use a stringList – so you need to create the stringList when you create the form, and you need to free the stringlist when the form is freed. To do this you would instantiate the stringlist class (that is, call its constructor) in the form’s onCreate event, and release the stringList (call free) in the form’s onDestroy event.

The point is, it’s your responsibility to allocate and free the memory for the object – if you forget to release the memory you have what is called a resource leak. You’ve allocated the memory but never released it. Will you notice this in your programs? It depends upon how much memory the object requires and how often you instantiate its class. If the object requires 2K of memory and you allocate this every time the user presses a certain push button, your application will rapidly grind to a halt with a memory exhausted error and you’ll need to reboot the computer to reclaim the memory. If the object only requires a few bytes of memory and you only instantiate it a couple of times you will not notice it.

In summary, then, you are responsible for allocating and freeing your object’s memory, and it’s not quite as simple as you might think, as the next section shows

Ensuring your object’s memory is released

Consider the following code fragment:

Procedure Test;
Var
i, j : Integer;
stringList1 : TStringList;

Begin
stringList1 := TStringList.Create;
i := 10;
j := 0;
i := i div j; // Line 10 -Exception generated here
stringList1.Free; // Line 11 – never executed
End;

We intentionally generate a divide by zero exception on line 10. If you enter this code and use the debugger to single step through it, you’ll see that Delphi does not execute line 11. After the exception is detected on line 10, Delphi handles the exception and returns to the application’s event loop. Your memory is not released. Of course, this is a contrived example but in general, you must take care when allocating memory for objects that any exceptions will not prevent your calls to free from being executed. Borland / Inprise recommend - and I strongly concur - that you should always bracket your Create / Free calls inside a Try / Finally construct. The Try / Finally construct is part of standard Pascal, and here’s how it works. You use Try to denote the start of a block of code. You use Finally to denote a second block of code, then the word End to indicate the end of the entire construct, as in:

Try
<First Block of code>
Finally
<Second Block of code>
End;

If any statement inside the first block generates an exception, Delphi executes the code inside the second block before handling the exception. If the first block does not generate an exception, the second block is still executed. Thus, the second block is guaranteed to be executed regardless of whether an exception occurs or not. So, to ensure your object’s memory is released, place the call to Free inside the Finally section. Here’s the general form:

<Object> := <ClassName>.<ConstructorName>
Try
// Use the object here
Finally
<Object>.Free;
End;

Note that the call to the constructor precedes the Try. This is in case the constructor itself fails. If the constructor fails, Delphi does not allocate the memory for the object. If the call to the constructor was after the Try, Delphi would run the code inside the finally block, and you would be attempting to free an object which had no memory. By placing the call to the constructor before the Try, if the constructor itself fails the code inside the finally block is not executed.

If you need to allocate and release more than one object, your use of Try / Finally is a little more complex. Consider the following code fragment:

Var
Student1 : TStudent;
StringListl : TStringList;

Begin
// Allocate memory for the objects
StringListl := TStringList.Create;
Student1 := TStudent.Create;
Try
// Work with Student1 & StringList1
Finally
Student1.Free;
StringList.Free;
End;

Does this code guarantee that both objects are freed? No. If the constructor for TStudent fails, your program does not enter the Try block, therefore the finally block is not called, and the memory for StringList1 is not released. One solution is to have two Try / Finally blocks:

Var
Student1 : TStudent;
StringListl : TStringList;

Begin
// Allocate memory for the objects
StringListl := TStringList.Create;
Try
Student1 := TStudent.Create;
Try
// Work with Student1 & StringList1
Finally
Student1.Free;
End;
Finally
StringList.Free;
End;

This works, but is tedious; and imagine the code if you had 3 or more objects to create. One trick you can employ relies on that fact the Free will not free an object that is nil. The source code to Delphi’s Free is basically:

// Delphi’s Free
If theObjectBeingReleased <> Nil Then
ReleaseTheMemory; // Self.Destroy

Before freeing the object’s memory, Free first ensures the object is not nil. Does this mean that the following will work:

Var
Student1 : TStudent;
Begin
// Forgot to instantiate TStudent
Student1.Free;

The answer depends upon the value of Student1 when the call to Free is made. Is Student1 nil? No. In Delphi, variables are not given initial values – the actual value of Student1 depends upon what is on the processor stack in the location occupied by Student1 when the routine is called. Now, the following will work:

Var
Student1 : TStudent;
Begin
Student1 := Nil;

// Forgot to instantiate TStudent
Student1.Free;

How does this help with our problem of allocating a series of objects and guaranteeing that they are freed? Well, it means we can write the following:

Student1 := Nil;
Try
Student1 := TStudent.Create;
Finally
Student1.Free;
End;

And even if the constructor fails, the call to Free will not; Student1 was explicitly given the value Nil before the constructor fails. The constructor fails – does not allocate the memory for Student1 – so Student1 retains its value of Nil, and the call to Free does not fail.

If we extend this to the problem of allocating several objects we can write:

Var
Student1 : TStudent;
StringListl : TStringList;

Begin
Student1 := Nil;
StringList1 := Nil;
Try
StringListl := TStringList.Create;
Student1 := TStudent.Create;
// Work with Student1 & StringList1
Finally
Student1.Free;
StringList.Free;
End;

The former solution – i.e. nesting Try / Finally blocks is classically a better solution, but the latter, i.e. explicitly setting Objects to nil, and relying upon Free not to destroy objects that are Nil, is certainly more convenient.

I tend not to use too many third party products with Delphi, but there’s one type of add-on product I strongly feel every Delphi developer should use – those that check your programs for resource leaks. There are two products which fall into this category – Memory Sleuth NuMega bounds checker. Here’s how they work. They monitor your program’s use of resources - in this case the resource we’re talking about is memory, but they also monitor other lower level resources such as window handles and device contexts. When your program terminates, if it hasn’t released all the resources it allocated, these products give you a list of all such allocations, including the actual line in the source code which allocated the resource. I strongly recommend you pick up one of these products – you might be surprised at what you find...

Declaring your own Classes

As you know, a class is a programming construct you use to specify and implement an abstract data type. The class specifies the individual data elements required to store the object’s data. Previously we declared our TStudent class as:

Type
TStudent = Class
FLName : Integer;
FFName : Integer;
FTel : String;
End;

This class declaration defines the storage requirements for TStudent objects. If you create 4 TStudent objects, each one has the same three properties. That is, the structure of all TStudent objects is identical, and that structure is determined by the class declaration.

These individual pieces of data used to represent an object – in this case FLname, FFName, and FTel, are known by several names. My preference is to call them instance variables. Delphi’s documentation refers to them as fields (I find this confuses my students because you also refer to columns in database tables as fields). Other names include data members, attributes and properties, although as you’ll see later, the word property is also used in another way in Delphi’s object model.

If you’re familiar with Pascal’s Record data type, you’ll see that so far, a class is no different from a record. Both are examples of composite data types – that is, data types that contain several pieces of information. What makes a class different from a record is that you can write code to work with a class. The class can define operations, called methods, which the program can perform on objects. Example of methods for a student class might incluce "RegisterForClass", "AddtoTable", "SendInvoice". Look at Delphi’s help file to see what methods are available for the TStringList and TIniFile classes, for example.

Writing your own methods

There are two parts to writing methods for a class. The first step is to declare the method in the class declaration, much like you declare an instance variable. This tells the compiler the operations you cdan perform on the class. Methods are essentially sub routines – and as such are either functions or procedures, and can receive parameters. You must declare them in the class declaration, indicating whether they are procedures or functions, and listing any parameters they take. As an example, here’s a simple class which declares four instance variables and three methods:

Type
TSquare = Class
FX, FY : Integer;
FWidth : Integer;
Caption : String;
Function Area : Integer;
Procedure MoveLeft(dx, dy : Integer);
Procedure MoveRight(dx, dy : Integer);
End;

The next step involves writing code for these methods. We’ll get to that in a moment – first let’s look at how users of this class can call these methods:

Var
Square1 : TSquare;
SqWidth : Integer;

Begin
Square1 := TSquare.Create;
Try
Square1.Fx := 10;
Square1.Fy := 20;
Square1.FWidth := 7;

SqWidth := Square1.Area; // 49 we hope
Square1.MoveLeft(2, 3);
Finally
Square1.Free;
End;

As you can see, you call the methods by prefixing the method name with the object name, in exactly the same way you access an object’s instance variables. Now, if you have two objects in memory, and you call a method, which object’s instance variables does the method use? That is, given:

Var
Square1 : TSquare;
Square2 : TSquare;
SqWidth : Integer;

Begin
Square1 := TSquare.Create;
Square1.FWidth := 7;

Square2 := TSquare.Create;
Square2.FWidth := 5;

SqWidth := Square1.Area; // Is this 49 or 25?

What is the value of sqWidth? You’d expect it to be 49 – the value of Square1’s Width property multiplied by itself. And indeed it is – but as you’ll see there is a little magic involved to get this to work. Methods must work on the data of the object which called it. When you write

Square1.Area

the area method must work with the data of the Square1 object. You’ll see how this works in a moment.

So far you’ve seen how to declare the method, but you must also write the code to implement the method. You place the code for the method in the unit’s implementation section, and write it much like any other subroutine. When you declare the method, however, you must tell the compiler that you are writing a method rather than a stand alone sub-routine. You do this by prefixing the method name with the name of the class, as in:

Function TSquare.Area : Integer;

and:

Procedure TSquare.MoveLeft(dx, dy : Integer);

Note how the object which called the method is not explicitly received as a parameter. So how does the method – which is essentially a sub-routine - access the instance variables of the object which called it? Actually, the object is received as a parameter to the method but you don’t see it, nor do you need to declare it. When you write code such as:

SqWidth := Square1.Area; // 49

Think of the compiler actually generating the following code:

SqWidth := Area( Square1 )

Behind the scenes it is passing the object to the method as a parameter. Inside the method you can reference the object which called the method using a predefined identifier called Self. You use Self, then, to access the object’s instance variables. When you call the method with:

Square1.Area

inside the method Self refers to the Square1 object. When you call it with:

Square2.Area

Self refers to the Square2 object.

Here’s the entire Area method:

Function TSquare.Area : Integer;

Begin
Result := Self.FWidth * Self.FWidth;
End;

And here are the methods for MoveLeft and MoveRight:

Procedure TSquare.MoveLeft( dx, dy : Integer);

Begin
Self.Fx := Self.Fx – dx;
Self.Fy := Self.Fy – dy;
End;

Procedure TSquare.MoveRight( dx, dy : Integer);

Begin
Self.Fx := Self.Fx + dx;
Self.Fy := Self.Fy + dy;
End;

In most cases Self is optional - that is you can simply refer to the instance variables without using Self, s in:

Function TSquare.Area : Integer;

Begin
Result := FWidth * FWidth;
End;

This works because the compiler knows the class to which a method belongs, therefore it knows when your code is accessing instance variables whether you use Self or not. There’s one exception to this, however, and that’s when you have a local variable with the same name as an instance variable. Consider the following (admittedly contrived) example:

Function TSquare.Area : Integer;

Var
FWidth : Integer;
Begin
Result := FWidth * FWidth;
End;

Here you have both a local variable and an instance variable called FWidth. In this case the compiler will actually use the local variable and the method will not work. To correct this you would need to explicitly prefix the instance variables with the word Self.

Tip

After you have declared the methods in your class, press Ctrl Shift C – Delphi will generate the method outlines for you.

Naming Conventions

I’ve been using a couple of naming conventions which I should explicitly mention. The convention in Delphi is to name all classes starting with the letter T, standing for Type. That’s why we’ve been using TSquare and TStudent as our class names rather than simply Square and Student. You’ll notice that all the classes in Delphi’s VCL start with the letter T – thus you see classes such as TButton, TForm, etc.

Another convention I’ve been using is to prefix all instance variables with the letter F, standing for Field. You’ll see the reason for this when we look at something called properties in the next article. For now just follow the conventions blindly!

MRU List Class

There’s a lot more to object oriented programming than we’ve covered so far, but at this stage a real-world example will help cement these ideas in your mind. We will develop a class (a new data type) called a MRUList – to manage a list of most recently used strings. You’ve seen MRU lists in many applications:

  • The Microsoft Office products, for example, keep track of the most recently used files you have worked with.
  • Windows Explorer keeps track of the most recently used documents.
  • Delphi keeps track of the most recently used projects.

We’ll develop a class which users can use to track a list of most recently used strings – the user of the class could use this to track the most recently used customers, text files, deleted records, whatever. Because we haven’t covered all of Delphi’s object-oriented features yet, our class will start out simple – we’ll only use the object-oriented features we’ve discussed so far.

Let’s start by declaring the operations we want the class to support – we’ll worry about the implementation in a moment. Here’s what we need:

Query how many elements are in the list

The ability to add a string to the MRU list

The ability to ask what string is at a certain position

When you add a string to the list, it appears at the start of the list. We’ll decide on a maximum number of elements to store in the list – and when the user adds more than that the ones at the end "drop off". That’s why we call it a Most Recently Used list – we track the most recently used strings.

Here’s the first declaration of this class

Type
TMruList = Class
Function Count : Integer;
Procedure Add( s : String);
Procedure GetString( n : Integer) : String
End;

Given this class declaration, here’s how users of the class can use it:

Var
muList : TMruList;
s : String;

Begin
// Instantiate the class
mruList := TMruList.Create;
Try
// Add items to it
mruList.Add(‘Spence’);
mruList.Add(‘Jones’);

// Access some of the items – the most recently
// used unit is at zero
s := mruList.GetString( 0 );
// s now contains Jones
Finally
// Remember to free the object’s memory
mruList.Free;
End;
End;

In this simple example we instantiated the class and freed it in the same routine. If you were using this class with a form, you would instantiate the class in the form’s onCreate event, and free the object in the form’s onDestroy event.

We must now decide how to implement the class. We must decide how to store the strings, how to count the strings, and how to write the methods. The best solution is probably to use Delphi’s TStringlist to store the strings. When users add an element to the MRUList, we simply add it to the start of the stringlist. We will indeed implement this solution in a moment, but we haven’t covered all the OOP theory we need to do this yet. Here’s the problem. We can easily add a TStringList to our class declaration:

Type
TMruList = Class
FMList : TStringList;
Function Count : Integer;
Procedure Add( s : String);
Procedure GetString( n : Integer) : String
End;

but our class needs to instantiate the stringlist – i.e. we need this:

FMList := TStringList.Create;

Instantiating the MRUList class does not instantiate the TStringList class.

You could insist the user of the class instantiates the stringList after instantiating the class:

mruList := TMruList.Create;
mruList.FMList := TStringList.Create;

but this is poor design. You are requiring the user of the class to perform operations in a certain order. Furthermore, the class user must also free the stringlist before freeing the mruList class – this is too much responsibility to place on the user.

There is a solution to this – i.e. you can write the mruList class so that it instantiates and frees the stringList – but this requires the use of constructors and destructors and we haven’t covered that yet. Later in this article we’ll look at changing the class in this manner.

For now, then, we’ll use a fixed length array to store the strings, and have a separate variable which keeps track of how many elements are used at any time.

Listing 1 shows the new class declaration and the implementation of its methods.

Const
MRUMaxItems = 4;

Type
TMruList = Class
FMList : Array[0..MRUMaxItems – 1] of String;
FNumItems : Integer;
Function Count : Integer;
Procedure Add( s : String);
Function GetString( n : Integer) : String
End;

Implementation

// Return the number of elements in the MRUList
Function TMruList.Count : Integer;
Begin
Result := Self.FNumItems;
End;

// Shift all the elements in the list up by one, add new element at the start
Procedure TMruList.Add(s : String);
Var
i : Integer;
Begin
For i := max(Self.FNumItems, MRUMaxItems - 1] DownTo 1 Do
Self.FMList[i] := Self.FMList[i – 1];

Self.MList[0] := s;
Self.FNumItems := Max(FNumItems + 1, MRUMaxItems);
End;

Function GetString( n : Integer ) : String;
Begin
If (n >= 0) and (n <= Self.FNumItems - 1) Then
Result := Self.FMList[n];
End;

Listing 1 – First Version of MRUList class

Scope of Instance Variables and Methods

When we talk about the scope of instance variables and methods, we mean where they are visible – i.e. who can use them. In the classes you’ve seen so far, all the methods and instance variables were visible to the class users; this is not good. Consider the TMRUList class’s FNumItems instance variable. This a variable used internally by the class to track how many items are in the list. Because this instance variable is visible to the class user, however, there’s nothing to stop him / her from changing it directly – thus destroying the integrity of our class.

You must distinguish then, between instance variables and methods which are visible to class users, and those that should only be used internally by the class. You should only allow the class user to see the variables and methods which constitute the class’s interface – i.e. those things that are essential in order to use the class. The implementation details of the class – its inner workings – should be hidden from the class user. This allows the person writing the class to change the implementation without affecting the class user. As long as the interface to the class does not change the class user will not have to change his / her code.

This is the second principle of object oriented programming, encapsulation.

Sidebar

Principle of Encapsulation....

To hide things from the class user, the class developer separates his / her class declaration into sections. A section determines the scope of the declarations placed inside it. A class can contain up to four sections, named:

Public
Published
Private
Protected

Which section you place your declarations in determines their scope and how you can use them.

Public

The public section of the class contains things that class users can see. If you don’t explicitly name a section it defaults to public – thus the method and instance variable scope in the classes you’ve seen so far has been public.

Private

The private section of the class contains things that class users cannot see. The only code that can see private instance variables and methods are other methods of this class. Private instance variables are contained in each object, but class users cannot access them directly. Listing 2 shows a second version of our MRUList class which places numItems and MList in the private section. We didn’t list the code for the methods as those didn’t change.

Type
TMruList = Class
Private
FMList : Array[0..MRUMaxItems – 1] of String;
FNumItems : Integer;
Public
Function Count : Integer;
Procedure Add( s : String);
Function GetString( n : Integer) : String
End;

Listing 2 – Second version of MRUList class using Private instance variables

Now the user of the class cannot access either FMList of FNumItems directly. We have restricted access to these instance variables to the methods of this class. This is private data which the user of the class has no business seeing. This is encapsulation – hiding the implementation details of the class.

Protected

The Protected section has to with inheritance so I’ll hold off on a detailed description until I cover that in the next article. Briefly, instance variables and methods you place in the protected section are not visible class users; they resemble privates in this regard. The difference between protected scope and private scope is to do with sub classes. Sub class method cannot see anything a superclass declares as private, but they can see things the superclass declares as protected.

Published

The published section of the class is very similar to the public section; they both list instance variables the class user can see. The difference between the two sections is with regard to components. Components are simply classes which you can use within Delphi’s IDE, which the user can drop onto a form and manipulate visually. Radio buttons, push buttons etc. are examples of components. Not all classes are components, of course. TStringList is not a component, neither is TMRUList (yet – we will make it a component later). But all components are classes.

When your class is a component, whatever you place in the published section is available in the object inspector. This allows the user to assign values to these instance variables using the object inspector instead of making the same assignments in code. I’ll give examples of this in the next article where we look at creating components.

Writing your own Constructors & Destructors

You already know what a constructor is – it’s a special function you call to create an instance of a class. Your classes can declare their own constructors in a similar manner to the way in which you declare normal methods. The advantage to writing your own constructors is you can perform initialization when the class is instantiated.

For example, consider the following code which instantiates a TSquare class, then proceeds to set some of its instance variables:

o := TSquare.Create;
o.FX := 10;
o.FY := 10;
o.FWidth := 5;
o.FCaption := ‘First Square’;

If you wrote your own constructor for the TSquare class it could receive initial values for those parameters and allow the class user to write:

o := TSquare.Create( 10, 10, 5, ‘First Square’);

This is certainly more convenient, but it confers other advantages as well. By providing a constructor the class developer can ensure his / her object is correctly initialized. For example, consider the following use of a TSquare class which does not implement a constructor:

o := TSquare.Create;
// Calculate its area;
a := o.Area;

The code calls the area method, but the user forgot to first set the square’s width. If the class provided a constructor the user would have to pass a width value – if they forget the compiler will quickly remind them.

Yet another advantage of using constructors is they can instantiate nested objects for you. Earlier in this article we mentioned that we would prefer to have the TMRUList class use a TStringList to store the items. We said that was awkward because we needed to instantiate the TStgringList class. Well the constructor is the ideal place to do that. When the user instantiates the TMRUList class, its constructor proceeds to instantiate the TStringList class.

Now, the TStringlist also needs to be freed. When do you want it freed? When the TMRUList itself is freed. This is not automatic, however. When the TMRUList is freed, you need to execute a piece of code which will free the stringlist. Delphi’s object model provides for this with something called a destructor. A destructor is much like the inverse of a constructor – it is called when the object is destroyed. We’ll look at destructors in a moment – let’s cover the syntax for constructors first.

You declare a constructor in the class declaration, using the keyword Constructor:

Type
TSquare = Class
FX, FY : Integer;
FCaption : String;
FWidth : Integer;
Function Area : Integer;
Constructor Create( px, py : Integer;
pWidth : Integer; Caption : String;
End;

Then you write the code for it in the implementation section, again introducing it with the keyword Constructor:

Constructor TSquare.Create(px, py : Integer;
pWidth : Integer; pCaption : String);
Begin
Self.FX := px;
Self.FY := py;
Self.FWidth := pWidth;
Self.FCaption := pCaption;
End;

As you can see, all the constructor is doing is copying the parameters it receives into the instance variables. In this case, Self is optional. However, if I had given the parameters the same names as the instance variables, I would have to use Self on the left hand side of the assignment statement to force the compiler to use the instance variable rather than the parameter.

The following code shows the constructor for the MRUList class, assuming the class is going to use a StringList called FMList to store the most recently used strings:

Constructor TMRUList.Create;

Begin
FMList := TStringList.Create;
End;

The constructor simply instantiates the TStringList class and saves it in the instance variable called FMList. As we mentioned already, the class must now free the stringlist when the MRUList class itself is destroyed. You must do this in the class’s destructor.

Delphi automatically calls a class’s destructor when you destroy the object, as in:

mruList := TMRUList.Create;
Try
// Work with mruList here
Finally
mruList.Free; // This calls the destructor
End;

When your code calls Free, Delphi automatically calls your class’s destructor. You declare your destructor as part of the class declaration using the keyword Destructor. For reasons you’ll see a little later, Destructors are always called Destroy. You must also declare your destructor as Override – you’ll also see what this means in the next article – just believe me – if you don’t declare your destructor as override it will not be called! Here’s the new class declaration showing the constructor, destructor, and the stringList.

Type
TMruList = Class
Private
FMList : TStringList;
Constructor Create;
Destructor Destroy; Override;
Public
Function Count : Integer;
Procedure Add( s : String);
Function GetString( n : Integer) : String
End;

To implement the destructor you write code for it in the implementation section much like you do for a constructor. You use the keyword Destroy to introduce the method. The destructor must call a superclass method of the same name, after it has performed its jobs. You do that by using the keyword inherited:

Destructor TMRUList.Destroy;
Begin
FMList.Free;
Inherited Destroy;
End;

Note that I removed the numItems instance variable from the class– we can determine that from the stringList itself. Listing 3 shows the reworked code.

// Return the number of elements in the MRUList
Function TMruList.Count : Integer;
Begin
Result := Self.FMList.Count;
End;

// Shift all the elements in the list up by one,
// add new element at the start
Procedure TMruList.Add(s : String);
Var
i : Integer;
Begin
Self.FMList.Insert(0, s);
If Self.FMList.Count >= MRUMaxItems Then
Self.FMList.Delete( Self.FMList.Count – 1);
End;

Function GetString( n : Integer ) : String;
Begin
If (n >= 0) and (n <= Self.FMList.Count - 1) Then
Result := Self.FMList.Strings[n];
End;

Listing 3 Third Version of MRU List class using a StringList

Note how even though I completely changed the way the class worked, code which uses the class did not change at all because the interface to the class did not change. That’s what encapsulation is all about.

Summary

This was a quick introduction to Object Oriented Programming in Delphi. There’s a lot more to this topic than I’ve covered here and subsequent articles will go into more detail. Our 5 Delphi Object Oriented Programming in Delphi course starts from first principles and finishes with creating data aware components which you can integrate into Delphi’s IDE.

 

Author

Rick Spence is technical director of Web Tech Training and Development, a company with offices in Florida (hyperlink here) and the UK (hyperlink). Web Tech Training and development specialize in developing Web and database applications for other companies (link to consulting web site here), in teaching programmers.