Object Oriented Programming 

The one-page crash course on Turbo Pascal OOP (in work)
Last Updated 29-August-1997

Annotated 10-July-2003

After thinking that this document was lost forever, I stumbled across it on an old hard drive. Wow.

I originally wrote this while I was still hanging out and wasting time on the Borland Pascal USENET group. Since some people actually found it useful, and because I think it was a fairly decent introduction to Object Oriented principles, I will dust it off and put it up on my current site.

I don't use DOS or Borland (Turbo) Pascal anymore. But, amazingly, the code presented on this page compiles without errors with the Free Pascal Compiler for Linux. Even more amazing is that it executes in a gnome-terminal essentially identically to the way it did on my old DOS text screen. Ahh! Once again I have ASCII cats blinking around my screen...

In the interest of historical accuracy, I'm posting it here essentially unchanged from it's original form. I've annotated the document with some comments, which can be easily identified by purple color.

This is hardly a comprehensive guide to OOP, nor is it intended to be. It's purpose is to introduce object oriented principles to programmers and to familiarize them with what is involved in using objects. To that end, I hope that it is easy to understand. I will gladly recieve your comments or suggestions! I can be contacted by e-mail at look4me@airmail.net. (Go ahead, I dare ya! I left this email address long ago. I will NOT receive anything that you attempt to send to it. Because of the present scourge, I do not have an email address that I am willing to post on a web page, unfortunately.) Also, if you are left with any questions about OOP, do direct those questions to me and I will try my best to answer them in a timely manner. The answers to such questions might also be integrated into this page. 

This is to be a sort of preamble to another (yet unmade) tutorial on Turbo Vision. (Don't hold your breath.)

Since it's inception, Pascal has been a procedural language. Procedures and functions were static. By this I mean that a procedure would be written and compiled and that was that. When the program called the procedure, there was no question what procedure was being called. Very static. To someone not familiar with this new paradigm, OOP can seem like a mysterious netherworld, incomprehensible and daunting.

However, if we are going to use Turbo Vision then it is essential that we are comfortable with object oriented principles. Let's begin.

In Turbo Pascal, an object is declared much the same way as a record. Take a look at the following record definition:

{1} Type
{2}   TPoint = Record;
{3}     X,Y: Integer;
{4}     End;

By making this record, we are saying that X and Y have something to do with each other. This is why we have encapsulated them into a single record. They are no longer seperate integers, but combined. Where ever one goes, the other will be there also. Together, they have a name: TPoint. We can use this encapsulation advantageously. A procedure to assign values to X and Y might be:

{5} Procedure Assign(Var Point: TPoint; X,Y: Integer);
{6}   Begin
{7}   Point.X := X;
{8}   Point.Y := Y;
{9}   End;

But now we have a problem. The procedure works fine. However, the procedure was intended to assign values only to variable of type TPoint. It has no other purpose. In a sense, Assign is a part of TPoint, but nothing in the definition of the record suggests that Assign really belongs to it. Our encapsulation has been somewhat diminished.

Now an object...

I used the word "encapsulation" in the last section for a reason. It is our first object oriented vocabulary word. In the same way that records encapsulate data, objects encapsulate data and code. In an object, data and the methods used to manipulate that data are inextricably bound. Not separate integers and procedures, but combined. Now, we will make an object out of our work:

{1} Type
{2}   TPoint = Object
{3}     X,Y: Integer;
{4}     Procedure Assign(AX,AY);
{5}     End;

You will notice that an object definition is very much like a record definition with a couple of notable exceptions. The first is in line {2}. The TPoint is an object and not a record. Second, and most interestingly, the TPoint now includes a procedure in line {4}. Now Assign is bound together with X and Y. All that remains is to define what Assign does:

{ 6} Procedure TPoint.Assign(AX,AY);
{ 7}   Begin
{ 8}   X := AX;
{ 9}   Y := AY;
{10}   End;

Pay close attention to the way we defined this procedure. First (line {6}) the declaration is TPoint.Assign instead of simply Assign. Since the procedure is defined outside of the bounds of our object type declaration, we must specify what object it belongs to. Otherwise, the compiler would make a normal procedure out of it and not an object. Also, more than one object might have a procedure called Assign, and the compiler must know which object this definition is a part of. Now look at lines {8} and {9}. Notice that when assigning values to X and Y, TPoint.Assign does not need to specify TPoint.X and TPoint.Y. The procedure and its data are in the same scope.

Now the object is complete. It has data and it has a method of dealing with the data. Method will be our second vocabulary word. A procedure or function which is part of an object is generally referred to as a method. Let's take our object for a quick spin:

{11} Var P: TPoint;

{12} Begin
{13} P.Assign(0,0);
{14} WriteLn(P.X,P.Y);
{15} P.Assign(5,7);
{16} WriteLn(P.X,P.Y);
{17} End.

Notice that a variable can be an object as easily as it can be a record (line {11}). Notice also in lines {13} and {15} how obvious it is what is being assigned. Properly used encapsulation significantly improves the readability of code.

Encapsulation is nice, but...

If you are beginning to think that an object is just a record with procedures and functions built in then you should stop thinking that way now. If that were true, then there would be no reason for the new object keyword; we could just expand the functionality of a record and thus call it a record. Encapsulation is a nice thing, but an object is so much more.

Consider an object which has no methods, only data. You may be tempted to think that, surely, this is merely a record. Again, you couldn't be farther from the truth. Objects have a feature that records only dream of: Inheritance. The concept of inheritance is fairly simple. We use it all the time when speaking of something new in terms of something already known. For example, we could say that a zebra is like a horse but with stripes.* Thought of this way, a zebra will inherit all the attributes of a horse and have an additional attribute, stripes.

Inheritance in objects can be thought of the same way. An object that inherits attributes from another is called a descendant and an object that is inherited from is called an ancestor. In Turbo Pascal, an object may have only one direct ancestor, but may have many descendants. The domain of an object refers to any object plus all of its descendants.

{1} Type
{2}   TPoint3D = Object(TPoint)
{3}     Z: Integer;
{4}     Procedure Assign(AX,AY,AZ: Integer);
{5}     End;

Take a look at line {2}. Here we are declaring that TPoint3D is a descendant of TPoint. It will be just like TPoint except where we specify differently. (For this declaration to be legal, the TPoint object must be already declared. Of course, it may be in a unit or just declared earlier in the same program.) In line {3}, a new integer variable is added. This means that a TPoint3D contains 3 integers: X and Y inherited from TPoint and the new Z. Also, our TPoint.Assign is not appropriate for TPoint3D, so line {4} declares a new Assign which will override the old Assign in the new object. We will have to write the body of the new procedure:

{ 6} Procedure TPoint3d.Assign(AX,AY,AZ: Integer);
{ 7}   Begin
{ 8}   Inherited Assign(AX,AY);
{ 9}   Z := AZ;
{10}   End;

Line {8} contains the Inherited key word, which is new to Turbo Pascal 7. This means that we want to call the Assign method from the direct ancestor. We could have just rewritten the assignments for X and Y, but inheritance allows us to call upon the ancestor to do the job it has always done (namely, assigning values to X and Y). This might not seem like a big deal now, but you will be thankful when it saves you from rewriting hundreds of lines of code.

Also, this economizes somewhat on code. Since the ancestor's assign method is actually called to do the work, there is no new code generated except the call to the ancestor's method. If TPoint had a thousand different descendants, they would all use the same code to assign X and Y.

Users of previous versions don't have the Inherited key word available to them, and will need to specifically call out the method to do the inherited work:

{ 8}   TPoint.Assign(AX,AY);

In line {9}, we finish the new Assign's work by assigning the value for Z.

Now we want to try out our new object:

{11} Var
{12}   P1: TPoint;
{13}   P2: TPoint3D;

{14} Begin
{15} P1.Assign(4,6);
{16} P2.Assign(9,8,7);
{17} WriteLn(P1.X,P1.Y);
{18} WriteLn(P2.X,P2.Y,P2.Z);
{19} End.

Inheritance allows us to create simple objects and later build on them to make powerful objects which do complex tasks. No need to re-invent the wheel every time we write a new program. We only need to inherit the aspects to previous code which we want to keep, and make changes necessary to the specific new task. Almost all of the Turbo Vision traces it's ancestry back to one simple object.

Polymorphism

Polymorphism is probably the most difficult to understand of the three aspects of objects. But without it, objects would just be a clever way to associate data with procedures and functions. The real power of objects can only be realized with the use of polymorphism.

Those not familiar and comfortable with the use of pointers should understand them before continuing. The use of objects is seldom exclusive of the use of pointers. A pointer review page is available for familiarization.

Late Binding

For people familiar only traditional structured programming, the concept of late binding might be hard to grasp. Everything about the data used in a program is traditionally known at compile time. Variables are bound with variable names when the program is compiled. This is known as early binding. For example:

Var
  A: Integer;
  B: String;

In the variable declaration above, A is declared as an integer and B is a string. When the program is compiled, memory is set aside for these variables. This is early binding. When the program runs, we know what variables are being used and we know what type they are. Pointers allow us a limited form of late binding, in that we might not know what they will point to at run time. Depending upon user input, time of day, or any number of other unpredictable circumstances, a pointer can be made to point to any number of types of variables, records, arrays, or other pointers.

Objects in Turbo Pascal extend this concept of late binding by use of what is know as the Virtual Method Table, or VMT. The VMT keeps track of objects with virtual methods and makes sure that the correct methods are called for each object. Since this may be unclear, let's try an example:

{01} {$X+} {Enable extended syntax}
{02} Unit Cat1;

{03} Interface

{04} Type
{05}   PCat = ^TCat;
{06}   TCat = Object
{07}     X,Y: Byte;
{08}     Constructor Init(AX,AY: Byte);
{09}     Destructor Done; Virtual;
{10}     Procedure Draw; Virtual;
{11}     Procedure Hide; Virtual;
{12}     Function SetCoords(AX,AY: Byte): Boolean;
{13}     Procedure Move(DX,DY: ShortInt);
{14}     End;

We are putting this object into a unit because we will use it again later, and to demonstrate how libraries of objects typically come in units.

Notice in line {05} that we first declare a pointer to our object. Get used to this. It is a good idea to always make pointers to objects. Also, it is important that you name your pointers and types consistantly. In my code, a pointer to a type always begins with P and the type always begins with T. This follows Borland's method of type declaration and I find it to be concise and readable. However, there is no law which states that you must follow this standard.

In line {06}, we see that TCat is a base object (no ancestors). Our future objects will use TCat as their ancestor, either directly or indirectly.

Line {08} and {09} contain new key words. A constructor is used to initialize an object which has virtual methods. A destructor disposes of such an object. More on this later.

Lines {10} and {11} declare virtual methods, indicated by the Virtual key word. By making these virtual, we are saying that the implementation of these might change in descendant objects, and we want the correct method to be called for the type of object being used.

{15} Implementation

{16} Uses Crt;

{17} Constructor TCat.Init(AX,AY: Byte);
{18}   Begin
{19}   X := 1; Y := 1; {Default to upper-left corner of screen}
{20}   SetCoords(AX,AY);
{21}   Draw;
{22}   End;

As stated earlier, a constructor initializes objects with virtual methods. This is very important. All objects with virtual methods must have a constructor! A constructor is just like a procedure except that it updates the VMT with information about the object's virtual methods.

In line {20}, we call the SetCoords method to put the TCat where we want it. Here we are using extended syntax and ignoring the result of SetCoords. We don't actually care if SetCoords succeeds, which is why we pre-initialize X and Y in line {19}. This will make more sense when you see the declaration of SetCoords later.

Line {21} calls the virtual Draw method to put our TCat on the screen. Because Draw is virtual, the call isn't actually to it, but to the Draw method pointed to by the VMT. This is an important distinction which will become clearer when we make our descendants.

{23} Destructor TCat.Done;
{24}   Begin End;

The destructor performs the task of removing the object's virtual methods from the VMT. Since that's all we want it to do, it is empty (Begin End;).

{25} Procedure TCat.Draw;
{26}   Begin
{27}   GotoXY(X+1,Y);
{28}   Write('_  /|');
{29}   GotoXY(X+1,Y+1);
{30}   Write('\o.O''');
{31}   GotoXY(X,Y+2);
{32}   Write('=(_ _)=');
{33}   GotoXY(X+3,Y+3);
{34}   Write('U');
{35}   End;

{25} Procedure TCat.Hide;
{26}   Begin
{27}   GotoXY(X+1,Y);
{28}   Write('     ');
{29}   GotoXY(X+1,Y+1);
{30}   Write('     ');
{31}   GotoXY(X,Y+2);
{32}   Write('       ');
{33}   GotoXY(X+3,Y+3);
{34}   Write(' ');
{35}   End;

Our Draw and Hide methods are very simple: Go to the appropriate location on the screen and Write the picture. There is no provision for saving the part of the screen that is being overwritten. Perhaps a descendant could demonstrate such an enhancement. Hide is almost a copy of Draw except that it writes spaces over the picture. In case you are wondering what the TCat looks like:

{36} Function TCat.SetCoords(AX,AY: Byte): Boolean;
{37}   Begin
{38}   SetCoords := False;
{39}   If (AX < 1) OR (AX > 73) Then Exit;
{40}   If (AY < 1) OR (AY > 22) Then Exit;
{41}   X := AX; Y := AY;
{42}   SetCoords := True;
{43}   End;

This function first checks to see that the coordinates are within the bounds of the 80x25 text screen (accounting for the size of the picture). If not then just exit and return false leaving the X and Y coordinates as they were. This is why the constructor initialized these values with 1; if the coordinates passed to Init were invalid, the TCat would just appear in the upper-left corner.

{44} Procedure TCat.Move(DX,DY: ShortInt);
{45}   Begin
{46}   Hide;
{47}   SetCoords(X + DX, Y + DY);
{48}   Draw;
{49}   End;

{50} End.

Moving a TCat is pretty simple. Just call the Hide method, reset the coordinates, and call the Draw method. If the new coordinates are out of bounds then the TCat won't move, so once again we don't care if SetCoords succeeds or not. Remember that this is a unit, so line {50} finishes the unit up. Now a quick test program:

{01} Program CatTest;

{02} Uses Crt, Cat1;

{03} Var
{04}   Litter: Array[1..10] Of PCat;
{05}   Loop: Integer;

{06} Begin
{07} ClrScr;
{08} For Loop := 1 To 10 Do
{09}   Litter[Loop] := New(PCat, Init(Random(70) + 1, Random(20) + 1));

In line {09}, we see that, when using New to allocate for an object, you can pass the constructor as a second parameter. This is very handy. With this extension, we can completely instantiate an object in a single line. Memory allocation and object initialization are contained in the call to New. Instantiate is another word you should be familiar with. Dynamic objects are instantiated. In lines {08} and {09}, we create ten TCat instances.

{10} Repeat
{11}   Delay(1000);
{12}   Litter[Random(10) + 1]^.Move(-9 + Random(19), -4 + Random(9));
{13}   Until KeyPressed;

Not too mind-boggling what's going on here. Every second or so, move a random TCat a random direction using the built-in Move method until a key is pressed.

Finally, we must clean up after our TCats. The syntax of Dispose has been extended similar to New. Now you can dispose of the allocated memory and call the object's destructor in a single statement:

{14} For Loop := 1 To 10 Do
{15}   Dispose(Litter[Loop], Done);
{16} End.

Now, what's all this "late binding" stuff about? Let's make a TCat descendent and see. One obvious enhancement to the TCat is adding color. The TColoredCat will illustrate this enhancement:

{01} Unit Cat2;

{02} Interface

{03} Uses Cat1;

{04} Type
{05}   PColoredCat = ^TColoredCat;
{06}   TColoredCat = Object(TCat)
{07}     Color: Byte;
{08}     Constructor Init(AX,AY,AColor: Byte);
{09}     Procedure Draw; Virtual;
{10}     End;

Not a bad start. As in our inheritance example, we are stating that our descendant TColoredCat will be just like TCat except for some new details. Specifically, a new byte variable to hold the color attribute (line {07}), The constructor has been replaced (line {08}), and the virtual Draw method needs to be changed (line {09}). First, the constructor:

{11} Implementation

{12} Uses Crt;

{13} Constructor TColoredCat.Init(AX,AY,AColor: Byte);
{14}   Begin
{15}   Color := AColor;
{16}   Inherited Init(AX,AY); { TP 6 must use 'TCat.Init(AX,AY)' }
{17}   End;

The first thing you should notice about this is how little of it there is. Inheritance works great. All the constructor behaviors that we want are already in the old Init, so we only need to add one line! Now, the virtual Draw method:

{18} Procedure TColoredCat.Draw;
{19}   Var OldAttr: Byte;
{20}   Begin
{21}   OldAttr := TextAttr;
{22}   TextAttr := Color;
{23}   Inherited Draw;
{24}   TextAttr := OldAttr;
{25}   End;

{26} End.

The Draw method merely saves the original text attribute byte from the Crt unit, assigns the color of the TColoredCat, calls the inherited method, and restores the saved text attribute. Simple.

That's it! The only difference between the old TCat and the new TColoredCat is four variable assignments ({15}, {21}, {22}, {24}). Now that's conservation of code.

Now, about late binding and virtual methods... Take a look at TColoredCat.Init. You will see that it does not call the Draw method. The inherited TCat.Init calls the Draw method. This is a problem if procedural bindings occur at compile time (early binding). Here's what would happen if the early binding model were applied to this situation:

  1. The Cat1 unit would be compiled.
  2. The TCat.Init constructor's call to Draw would be bound to TCat.Draw. In other words, TCat.Init would call TCat.Draw.
  3. The Cat2 unit would be compiled.
  4. The TColoredCat.Init calls TCat.Init (line {16}) which has already been compiled.
  5. Now TCat.Init will call TCat.Draw which will not draw a colored TCat.
As you can see, TColoredCat.Draw will never be called by an inherited call to Draw, so overriding the Draw method does not do much good. Late binding means that the call to Draw is not resolved until run time and will be properly bound to the inherited instances that are actually making the call.

In order to make a test program for the new TColoredCat, we need only change two lines in CatTest:

{02} Uses Crt, Cat1, Cat2;

And,

{09}   Litter[Loop] := New(PColoredCat, Init(Random(70) + 1, Random(20) + 1, Random(16)));

The only changes needed are the inclusion of Cat2 in the Uses clause and to change the statement that initializes the array elements with new TCats so that it initializes them with new TColoredCats instead. (The colors are chosen at random from the first 16 which will always produce a black background. If this were changed to Random(256) then an assortment of foreground and background colors would be used for each TCat, including possible blinking TCats.)

Another Note On Inheritance And Polymorphism

You may have noticed that line {04} of CatTest did not need to be changed, and that in line {09} we are assigning new PColoredCats to array elements which were declared as PCats. In other words, the following is OK:

Var Cat: PCat;

Cat := New (PColoredCat, Init(1,2,3));

This is perfectly legal. Any pointer to an object may also point to any object which descended from that object. This is why the domain of an object is important. In our CatTest program, we could have made some of the Litter into TCats while at the same time others could be TColoredCats. This is a key element in event driven programs (Turbo Vision is event driven). We can declare pointers without knowing what, specifically, they will point to at run time. Events such as user input might determine which object descendants will be used. More on this when we get to TurboVision.

Be aware that this does not work the other way. A PColoredCat cannot point to a TCat.

More To Come...

(Not likely. Like Robert Frost's "The Road Not Taken," I've long since moved on. Alas, Pascal was a fun language. Maybe someday.)

*Apologies to Neil J. Rubenking for stealing his illustration.