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:
|