Difference between revisions of "Programming Using Objects"

From Lazarus wiki
Jump to navigationJump to search
(code formating and category)
Line 3: Line 3:
 
Of the two OOP implementations FPC provides, the one used less often seems to be what is referred to as "Objects" which probably gets its name from the type definition syntax.  The syntax diagram for an object type declaration can be found in the FPC language reference - Chapter 5.  Basically, an '''object''' type looks like a '''record''' type with additional fields including procedure fields and also optional keywords which indicate the scope of the fields.  In fact, an oversimplified (but valid) simple object is hard to distinguish from a record structure as can be seen below.
 
Of the two OOP implementations FPC provides, the one used less often seems to be what is referred to as "Objects" which probably gets its name from the type definition syntax.  The syntax diagram for an object type declaration can be found in the FPC language reference - Chapter 5.  Basically, an '''object''' type looks like a '''record''' type with additional fields including procedure fields and also optional keywords which indicate the scope of the fields.  In fact, an oversimplified (but valid) simple object is hard to distinguish from a record structure as can be seen below.
  
<delphi>
+
<delphi>Type
Type
 
 
   MyObject = Object
 
   MyObject = Object
 
       f_Integer : integer;
 
       f_Integer : integer;
 
       f_String : ansiString;
 
       f_String : ansiString;
 
       f_Array : array [1.3] of char;
 
       f_Array : array [1.3] of char;
   end;
+
   end;</delphi>
</delphi>
 
  
 
The only difference in the above example is that the Pascal keyword '''record''' has been replaced with the keyword '''object'''.  Things start to be different when methods are added to the object.  Object methods are declared in FPC using the keywords '''procedure''' or '''function'''.  These object methods (procedures or functions) are declared the same way as normal Pascal procedures and functions; just that they are declared within the object declaration itself.  Lets create a different object; this time possibly as part of an oversimplified graphics drawing program.
 
The only difference in the above example is that the Pascal keyword '''record''' has been replaced with the keyword '''object'''.  Things start to be different when methods are added to the object.  Object methods are declared in FPC using the keywords '''procedure''' or '''function'''.  These object methods (procedures or functions) are declared the same way as normal Pascal procedures and functions; just that they are declared within the object declaration itself.  Lets create a different object; this time possibly as part of an oversimplified graphics drawing program.
  
<delphi>
+
<delphi>Type
Type
 
 
   DrawingObject = Object
 
   DrawingObject = Object
 
       x, y : single;
 
       x, y : single;
Line 23: Line 20:
  
 
Var
 
Var
   Rectangle : DrawingObject;
+
   Rectangle : DrawingObject;</delphi>
</delphi>
 
  
 
Besides the simple datatype fields providing some application specific location and size attributes, the object shown above declares an additional parameter;  a procedure called ''Draw.'' The type declaration is followed by a variable identifier called ''Rectangle'' of the type ''DrawingObject''.  Next, the ''Draw'' procedural code itself needs to be written as well as well as code for accessing and manipulating the data fields, as well as how to invoke the Draw procedure.  The following simple program shows how all this works.  It should compile and run on any system with FPC 2.2.2 and above.  Note: For Mac OS X, the -macpas compiler directive must be turned off.
 
Besides the simple datatype fields providing some application specific location and size attributes, the object shown above declares an additional parameter;  a procedure called ''Draw.'' The type declaration is followed by a variable identifier called ''Rectangle'' of the type ''DrawingObject''.  Next, the ''Draw'' procedural code itself needs to be written as well as well as code for accessing and manipulating the data fields, as well as how to invoke the Draw procedure.  The following simple program shows how all this works.  It should compile and run on any system with FPC 2.2.2 and above.  Note: For Mac OS X, the -macpas compiler directive must be turned off.
  
<delphi>
+
<delphi>Program TestObjects;
Program TestObjects;
 
  
 
Type
 
Type
Line 69: Line 64:
 
   end;
 
   end;
  
end.
+
end.</delphi>
</delphi>
 
  
 
As can be seen in the above program, the body of the ''Draw'' method (procedure) is declared after the object type declaration by concatenating the type identifier with the procedure name.  In a more realistic situation, the object would likely be declared in the '''interface''' section of a separate unit while the procedure body would be written in the '''implementation''' section of the same external unit.  In this example, only standard vanilla Pascal is used, but if actual graphic primitives are available, they can be used if desired.  The second thing to notice is that inside the ''Draw'' method (procedure), the object's data fields are referenced as if they were regular local variables.  The only difference to regular local variables is that the values of these fields will persist between calls to the ''Draw'' procedure.  
 
As can be seen in the above program, the body of the ''Draw'' method (procedure) is declared after the object type declaration by concatenating the type identifier with the procedure name.  In a more realistic situation, the object would likely be declared in the '''interface''' section of a separate unit while the procedure body would be written in the '''implementation''' section of the same external unit.  In this example, only standard vanilla Pascal is used, but if actual graphic primitives are available, they can be used if desired.  The second thing to notice is that inside the ''Draw'' method (procedure), the object's data fields are referenced as if they were regular local variables.  The only difference to regular local variables is that the values of these fields will persist between calls to the ''Draw'' procedure.  
Line 84: Line 78:
 
What follows are type declarations for: the main ''TShape'' object type (previously ''DrawingObject'') along with the new types ''TRectangle'' and ''TSquare''.  ''TShape'' now includes a new method ('''procedure''' GetParams) to obtain values for its fields.  Next, the types ''TRectangle'' and ''TSquare'' are declared.  ''TShape'' is usually referred to as an ancestor or parent object, while TRectangle is said to be a child or subobject or subclass of ''TShape'' or that it descends from ''TShape''.  This parent child, grandchild heiracry is identified by the qualifier type name following the Object keyword.  Similarly, TSquare is a child of TRectangle.  Also, ''TShape'' is not really a complete object type in itself but rather a template for other objects to inherit a common structure and behavior(s) from.  Such templates are often useful for code clarity and there are language features (explained later) which can be used to enforce certain characteristics of these template objects.  For an actual application, the ''TShape'' type would more than likely be declared differently.  Here it is used to illustrate some basic concepts.
 
What follows are type declarations for: the main ''TShape'' object type (previously ''DrawingObject'') along with the new types ''TRectangle'' and ''TSquare''.  ''TShape'' now includes a new method ('''procedure''' GetParams) to obtain values for its fields.  Next, the types ''TRectangle'' and ''TSquare'' are declared.  ''TShape'' is usually referred to as an ancestor or parent object, while TRectangle is said to be a child or subobject or subclass of ''TShape'' or that it descends from ''TShape''.  This parent child, grandchild heiracry is identified by the qualifier type name following the Object keyword.  Similarly, TSquare is a child of TRectangle.  Also, ''TShape'' is not really a complete object type in itself but rather a template for other objects to inherit a common structure and behavior(s) from.  Such templates are often useful for code clarity and there are language features (explained later) which can be used to enforce certain characteristics of these template objects.  For an actual application, the ''TShape'' type would more than likely be declared differently.  Here it is used to illustrate some basic concepts.
  
<delphi>
+
<delphi>Type
Type
+
  TShape = Object
  TShape = Object
+
    x, y : single;
      x, y : single;
+
    height, width : single;
      height, width : single;
+
    procedure GetParams;
      procedure GetParams;
+
    procedure Draw;
      procedure Draw;
+
  end;
  end;
 
  
TRectangle = Object(TShape)
+
  TRectangle = Object(TShape)
      procedure Draw;
+
    procedure Draw;
  end;
+
  end;
  
TSquare = Object(TRectangle)
+
  TSquare = Object(TRectangle)
      procedure GetParams;
+
    procedure GetParams;
      procedure Draw;
+
    procedure Draw;
 
   end;
 
   end;
  
Line 105: Line 98:
 
   Shape : TShape;
 
   Shape : TShape;
 
   Rectangle : TRectangle;
 
   Rectangle : TRectangle;
   Square : TSquare;
+
   Square : TSquare;</delphi>
</delphi>
 
  
 
Notice that ''TRectangle'' lists only  the ''Draw'' procedure while ''TSquare'' lists both the ''GetParams'' and ''Draw'' procedures.  Neither of the subobject types include any fields.  The "missing" fields and missing procedure names are said to be inherited from the fields and procedures declared in ancestor objects.  In this case, any object variables declared and instantiated of type ''TRectangle'' will inherit from the ''TShape'' type all four fields, (x, y, height, and width) and the ''GetParams'' method .  At runtine, a variable of the type ''TRectangle''  will look and behave like a ''TShape'' variable except that the ''Draw'' procedure will use different code than the procedure by the same name in an instantiated  variable of the ''TShape'' type.  Similarly, the ''TSquare'' object type will inherit all the "grandparent" fields from ''TShape'' and have its own different procedure blocks.  If desired, ''TRectangle'' and ''TSquare'' could have declared additional fields in their type definitions which would show up as additional fields (memory locations) available at runtime which instantiated parent object variables would not have or be able to access.
 
Notice that ''TRectangle'' lists only  the ''Draw'' procedure while ''TSquare'' lists both the ''GetParams'' and ''Draw'' procedures.  Neither of the subobject types include any fields.  The "missing" fields and missing procedure names are said to be inherited from the fields and procedures declared in ancestor objects.  In this case, any object variables declared and instantiated of type ''TRectangle'' will inherit from the ''TShape'' type all four fields, (x, y, height, and width) and the ''GetParams'' method .  At runtine, a variable of the type ''TRectangle''  will look and behave like a ''TShape'' variable except that the ''Draw'' procedure will use different code than the procedure by the same name in an instantiated  variable of the ''TShape'' type.  Similarly, the ''TSquare'' object type will inherit all the "grandparent" fields from ''TShape'' and have its own different procedure blocks.  If desired, ''TRectangle'' and ''TSquare'' could have declared additional fields in their type definitions which would show up as additional fields (memory locations) available at runtime which instantiated parent object variables would not have or be able to access.
Line 112: Line 104:
 
Next are the specific procedure implementations for these object types.
 
Next are the specific procedure implementations for these object types.
  
<delphi>
+
<delphi>procedure TShape.GetParams;
procedure TShape.GetParams;
+
begin
  begin
+
  write('TShape.GetParams : ');
write('TShape.GetParams : ');
+
  readln(x, y, width, height);
readln(x, y, width, height);
+
  writeln;
writeln;
+
end;
  end;
 
  
procedure TShape.Draw;
+
procedure TShape.Draw;
  begin
+
begin
    writeln('TShape.Draw');
+
  writeln('TShape.Draw');
writeln('Position: x = ', x:4:0, ' y = ', y:4:0);
+
  writeln('Position: x = ', x:4:0, ' y = ', y:4:0);
writeln('    Size: w = ', width:4:0, ' h = ', height:4:0);
+
  writeln('    Size: w = ', width:4:0, ' h = ', height:4:0);
writeln;
+
  writeln;
  end;
+
end;
  
 
procedure TRectangle.Draw;
 
procedure TRectangle.Draw;
  begin
+
begin
      writeln('TRectangle.Draw');
+
  writeln('TRectangle.Draw');
  end;
+
end;
  
procedure TSquare.GetParams;
+
procedure TSquare.GetParams;
  begin
+
begin
write('TSquare.GetParams : ');
+
  write('TSquare.GetParams : ');
readln(x, y, width, height);
+
  readln(x, y, width, height);
height := width;
+
  height := width;
writeln('making sure all sides are equal for Square');
+
  writeln('making sure all sides are equal for Square');
writeln;
+
  writeln;
  end;
+
end;
  
procedure TSquare.Draw;
+
procedure TSquare.Draw;
  begin
+
begin
      writeln('TSquare.Draw');
+
  writeln('TSquare.Draw');
  end;
+
end;</delphi>
</delphi>
 
  
 
To provide clarity, no actual (platform specific) drawing routines are used.  Rather, generic Pascal I/O routines are included to illustrate runtime behavior.  This particular hierarchy of objects, fields and methods would likely not be the best implementation for an actual shape drawing application which will become more apparent after new concepts are introduced.     
 
To provide clarity, no actual (platform specific) drawing routines are used.  Rather, generic Pascal I/O routines are included to illustrate runtime behavior.  This particular hierarchy of objects, fields and methods would likely not be the best implementation for an actual shape drawing application which will become more apparent after new concepts are introduced.     
Line 160: Line 150:
 
Here is a sample program which demonstrates the behavior of the various objects.
 
Here is a sample program which demonstrates the behavior of the various objects.
  
<delphi>
+
<delphi>Var
Var
 
 
   Shape : TShape;
 
   Shape : TShape;
 
   Rectangle : TRectangle;
 
   Rectangle : TRectangle;
 
   Square : TSquare;
 
   Square : TSquare;
 
 
begin  
 
begin  
writeln;
+
  writeln;
writeln ('Getting parameters for Shape');
+
  writeln ('Getting parameters for Shape');
Shape.GetParams;
+
  Shape.GetParams;
 
+
 
write ('Calling Shape.Draw : ');
+
  write ('Calling Shape.Draw : ');
Shape.Draw;
+
  Shape.Draw;
  
writeln;
+
  writeln;
writeln ('Getting parameters for Rectangle');
+
  writeln ('Getting parameters for Rectangle');
Rectangle.GetParams;
+
  Rectangle.GetParams;
  
write ('Calling Rectangle.Draw : ');
+
  write ('Calling Rectangle.Draw : ');
Rectangle.Draw;
+
  Rectangle.Draw;
 
 
writeln;
+
  writeln;
writeln ('Getting parameters for Square');
+
  writeln ('Getting parameters for Square');
Square.GetParams;
+
  Square.GetParams;
  
write ('Calling Square.Draw : ');
+
  write ('Calling Square.Draw : ');
Square.Draw;
+
  Square.Draw;</delphi>
</delphi>
 
  
 
The above code produces the following output.
 
The above code produces the following output.
  
<pre>
+
<pre>Getting parameters for Shape
Getting parameters for Shape
 
 
TShape.GetParams : 1 2 3 4
 
TShape.GetParams : 1 2 3 4
 
Calling Shape.Draw : TShape.Draw
 
Calling Shape.Draw : TShape.Draw
Line 207: Line 193:
 
making sure all sides are equal for Square
 
making sure all sides are equal for Square
  
Calling Square.Draw : TSquare.Draw
+
Calling Square.Draw : TSquare.Draw</pre>
</pre>
 
  
 
Tthe ''Shape'' object is initialized by calling the ''GetParams'' method and then the ''Draw'' method is called which prints out the field values.  Next, the same is done for the ''Rectangle'' object.  Notice that since there is no ''GetParams'' method for Rectangles, the compiler used the parent object's method, ''TShape.GetParams'' to carry out this action.  Finally, the ''Square'' object is initialized by calling the ''GetParams'' method which is explicitly defined for ''Squares'' and the output shows this method was called and did extra processing.  The ''Draw'' method is called and the ''Draw'' method specifically defined for ''Squares'' was executed.  Note that if the ''Draw'' procedure was left out for the ''TSquare'' type (as would be reasonable for an actual program) the last output line would look like this.
 
Tthe ''Shape'' object is initialized by calling the ''GetParams'' method and then the ''Draw'' method is called which prints out the field values.  Next, the same is done for the ''Rectangle'' object.  Notice that since there is no ''GetParams'' method for Rectangles, the compiler used the parent object's method, ''TShape.GetParams'' to carry out this action.  Finally, the ''Square'' object is initialized by calling the ''GetParams'' method which is explicitly defined for ''Squares'' and the output shows this method was called and did extra processing.  The ''Draw'' method is called and the ''Draw'' method specifically defined for ''Squares'' was executed.  Note that if the ''Draw'' procedure was left out for the ''TSquare'' type (as would be reasonable for an actual program) the last output line would look like this.
  
<pre>
+
<pre>Calling Square.Draw : TRectangle.Draw</pre>
Calling Square.Draw : TRectangle.Draw
 
</pre>
 
  
 
Now what happens if we assign a sub object variable to a parent variable as follows?  
 
Now what happens if we assign a sub object variable to a parent variable as follows?  
  
<delphi>
+
<delphi>writeln;
writeln;
+
writeln ('Assigning Rectangle to Shape');
writeln ('Assigning Rectangle to Shape');
+
Shape := Rectangle;
Shape := Rectangle;
+
writeln;
writeln;
+
write ('Calling Shape.Draw : ');
write ('Calling Shape.Draw : ');
+
Shape.Draw;
Shape.Draw;
 
 
 
writeln;
+
writeln;
writeln ('Assigning Square to Shape');
+
writeln ('Assigning Square to Shape');
Shape := Square;
+
Shape := Square;
writeln;
+
writeln;
write ('Calling Shape.Draw : ');
+
write ('Calling Shape.Draw : ');
Shape.Draw;
+
Shape.Draw;
 +
 
 +
writeln;
 +
writeln ('Assigning Square to Rectangle');
 +
Rectangle := Square;
 +
writeln;
 +
write ('Calling Rectangle.Draw : ');
 +
Rectangle.Draw;</delphi>
  
writeln;
 
writeln ('Assigning Square to Rectangle');
 
Rectangle := Square;
 
writeln;
 
write ('Calling Rectangle.Draw : ');
 
Rectangle.Draw;
 
</delphi>
 
 
The following is output.
 
The following is output.
<pre>
+
<pre>Assigning Rectangle to Shape
Assigning Rectangle to Shape
 
  
 
Calling Shape.Draw : TShape.Draw
 
Calling Shape.Draw : TShape.Draw
Line 256: Line 237:
 
Assigning Square to Rectangle
 
Assigning Square to Rectangle
  
Calling Rectangle.Draw : TRectangle.Draw
+
Calling Rectangle.Draw : TRectangle.Draw</pre>
</pre>
 
  
 
The first block of code assigns the ''Rectangle'' variable to the ''Shape'' variable.  When the ''Shape.Draw'' method is invoked, it executes the ''TShape.Draw'' code and not the ''TRectangle.Draw'' code.  But as can be seen, the ''Rectangle'' fields is what are printed out.  This behavior is called '''Static''' method inheritance in FPC.  If it is desired to instead invoke the child ''TRectangle.Draw'' method in this situation, FPC provides way to do this called '''virtual''' methods which will be covered in the next section.  Continuing on, ''Shape'' is assigned to ''Square'' and the ''TShape.Draw'' method is invoked which prints out the field values of the ''Square'' object.  Finally, the Rectangle variable is assigned the Square and the ''TRectangle.Draw'' method is invoked similarly to the behavior of the ''TShape.Draw'' methods.
 
The first block of code assigns the ''Rectangle'' variable to the ''Shape'' variable.  When the ''Shape.Draw'' method is invoked, it executes the ''TShape.Draw'' code and not the ''TRectangle.Draw'' code.  But as can be seen, the ''Rectangle'' fields is what are printed out.  This behavior is called '''Static''' method inheritance in FPC.  If it is desired to instead invoke the child ''TRectangle.Draw'' method in this situation, FPC provides way to do this called '''virtual''' methods which will be covered in the next section.  Continuing on, ''Shape'' is assigned to ''Square'' and the ''TShape.Draw'' method is invoked which prints out the field values of the ''Square'' object.  Finally, the Rectangle variable is assigned the Square and the ''TRectangle.Draw'' method is invoked similarly to the behavior of the ''TShape.Draw'' methods.
Line 263: Line 243:
 
Assigning an ancestor object to a descendent object is not allowed.  The compiler will flag the following line as an error.
 
Assigning an ancestor object to a descendent object is not allowed.  The compiler will flag the following line as an error.
  
<delphi>
+
<delphi>Rectangle := Shape;  <--- can't assign a parent to a child, does not compile</delphi>
Rectangle := Shape;  <--- can't assign a parent to a child, does not compile
 
</delphi>
 
  
 
== Objects - Virtual Inheritance ==
 
== Objects - Virtual Inheritance ==
Line 271: Line 249:
 
For a drawing application, a common task would be to refresh the display and stepping through an array or  linked list of shapes and calling the draw method.  Instead of needing a case statement inside the loop which selects one of many possible specific drawing procedures
 
For a drawing application, a common task would be to refresh the display and stepping through an array or  linked list of shapes and calling the draw method.  Instead of needing a case statement inside the loop which selects one of many possible specific drawing procedures
  
<delphi>
+
<delphi>for k := 1 to NumShapes do
for k := 1 to NumShapes do
 
 
  with ShapeRec[k] do
 
  with ShapeRec[k] do
 
   case ShapeRec[k].ShapeKind of
 
   case ShapeRec[k].ShapeKind of
Line 278: Line 255:
 
     cSquare:  DrawSquare :  (x, y, width, .height);
 
     cSquare:  DrawSquare :  (x, y, width, .height);
 
   cTriangle: DrawTriangle:(x, y, angle1, angle2, base);
 
   cTriangle: DrawTriangle:(x, y, angle1, angle2, base);
  end;  // case
+
  end;  // case</delphi>
</delphi>
+
 
 
the code would look something like this
 
the code would look something like this
<delphi>
+
 
for k:= 1 to Numshapes do
+
<delphi>for k:= 1 to Numshapes do
  Shape[k].draw;
+
  Shape[k].draw;</delphi>
</delphi>
 
  
 
Where each Shape object could be one of any sub objects descended from TShape.  Code maintenance is made easier since there is one fewer locations in code which needs to be modified.  All changes to the behavior of a particular sub object of shape is kept together in one place.
 
Where each Shape object could be one of any sub objects descended from TShape.  Code maintenance is made easier since there is one fewer locations in code which needs to be modified.  All changes to the behavior of a particular sub object of shape is kept together in one place.
Line 292: Line 268:
 
However, as seen in the last section, calling the ''Draw'' method for the Shape variable this way will not invoke the appropriate draw method of the sub object which is the behavior that is desired in this (and most) cases.  To obtain the desired behavior, the '''virtual''' keyword must be inserted after the method declaration in the type definition as follows:
 
However, as seen in the last section, calling the ''Draw'' method for the Shape variable this way will not invoke the appropriate draw method of the sub object which is the behavior that is desired in this (and most) cases.  To obtain the desired behavior, the '''virtual''' keyword must be inserted after the method declaration in the type definition as follows:
  
<delphi>
+
<delphi>Type
Type
+
  TShape = Object
  TShape = Object
+
    x, y : single;
      x, y : single;
+
    height, width : single;
      height, width : single;
+
    procedure GetParams;
      procedure GetParams;
+
    procedure Draw; virtual;
      procedure Draw; virtual;
+
  end;
  end;
 
 
   
 
   
TRectangle = Object(TShape)
+
  TRectangle = Object(TShape)
      procedure Draw; virtual;
+
    procedure Draw; virtual;
  end;
+
  end;</delphi>
</delphi>
 
  
 
Now if a Rectangle object is assigned to the Shape variable, the Draw method of ''TRectangle'' will be used.  The term often used to describe this situation is called '''overriding''' a parent method.  Although the body of main program will the same as in the previous section, the execution behavior will be different.  The '''virtual''' keyword tells the compiler to hold off fixing the specific procedure used and instead lets the binding of the method be determined at runtime dynamically.
 
Now if a Rectangle object is assigned to the Shape variable, the Draw method of ''TRectangle'' will be used.  The term often used to describe this situation is called '''overriding''' a parent method.  Although the body of main program will the same as in the previous section, the execution behavior will be different.  The '''virtual''' keyword tells the compiler to hold off fixing the specific procedure used and instead lets the binding of the method be determined at runtime dynamically.
Line 310: Line 284:
 
By adding the '''virtual''' keyword to the type declarations of TShape, TRectangle and TSquare in the last section, the latter portion of the output would look as shown below.  Note that in order to run the previous program using virtual methods, some other code needs to be added in order for the program to run.  This additional code is described in the next section.
 
By adding the '''virtual''' keyword to the type declarations of TShape, TRectangle and TSquare in the last section, the latter portion of the output would look as shown below.  Note that in order to run the previous program using virtual methods, some other code needs to be added in order for the program to run.  This additional code is described in the next section.
  
<pre>
+
<pre>Assigning Rectangle to Shape
Assigning Rectangle to Shape
 
  
 
Calling Shape.Draw : TRectangle.Draw
 
Calling Shape.Draw : TRectangle.Draw
Line 321: Line 294:
 
Assigning Square to Rectangle
 
Assigning Square to Rectangle
  
Calling Rectangle.Draw : TSquare.Draw
+
Calling Rectangle.Draw : TSquare.Draw</pre>
</pre>
 
  
 
Although it is allowed, mixing virtual methods and non virtual (static) methods in the inheritance hierarchy may result in behavior which is difficult to manage.
 
Although it is allowed, mixing virtual methods and non virtual (static) methods in the inheritance hierarchy may result in behavior which is difficult to manage.
Line 332: Line 304:
 
Here are are the Shape declarations again, this time using virtual methods and including constructors and destructors.
 
Here are are the Shape declarations again, this time using virtual methods and including constructors and destructors.
  
<delphi>
+
<delphi>Type
Type
+
  TShape = Object
  TShape = Object
+
    x, y : single;
      x, y : single;
+
    height, width : single;
      height, width : single;
 
  
      procedure GetParams; virtual;
+
    procedure GetParams; virtual;
      procedure Draw; virtual;
+
    procedure Draw; virtual;
  
      Constructor Init(xx, yy, h, w : single);
+
    Constructor Init(xx, yy, h, w : single);
      Destructor CleanUp;
+
    Destructor CleanUp;
  end;
+
  end;
  
TRectangle = Object(TShape)
+
  TRectangle = Object(TShape)
    procedure Draw; virtual;
+
    procedure Draw; virtual;
  end;
+
  end;
  
 
   TSquare = Object(TRectangle)
 
   TSquare = Object(TRectangle)
    procedure GetParams; virtual;
+
    procedure GetParams; virtual;
 
   
 
   
 
     Constructor Init(xx, yy, h, w : single);
 
     Constructor Init(xx, yy, h, w : single);
   end;
+
   end;</delphi>
</delphi>
 
  
 
The TShape object type includes fields, two virtual methods (Draw and GetParams), a constructor called Init and a destructor called CleunUp.   
 
The TShape object type includes fields, two virtual methods (Draw and GetParams), a constructor called Init and a destructor called CleunUp.   
Line 370: Line 340:
 
For the current program, the Draw method for TSquare has been removed.  It will be assumed that the (fictional) graphics code for for drawing a square is the same as for a rectangle and the Draw method can be inherited from TRectangle.  The difference in the TSquare and TRectangle object  is inherent in the Init constructor and GetParams method.  Implementation of the new constructors and destructors is shown below.
 
For the current program, the Draw method for TSquare has been removed.  It will be assumed that the (fictional) graphics code for for drawing a square is the same as for a rectangle and the Draw method can be inherited from TRectangle.  The difference in the TSquare and TRectangle object  is inherent in the Init constructor and GetParams method.  Implementation of the new constructors and destructors is shown below.
  
<delphi>
+
<delphi>Constructor TShape.Init(xx, yy, h, w : single);
  Constructor TShape.Init(xx, yy, h, w : single);
+
begin
  begin
+
  writeln('TShape.Init');
writeln('TShape.Init');
+
  x := xx;
x := xx;
+
  y := yy;
y := yy;
+
  height := h;
height := h;
+
  width := w;
width := w;
+
end;
  end;
 
 
    
 
    
  Destructor TShape.CleanUp;
+
Destructor TShape.CleanUp;
  begin
+
begin
writeln('TShape.CleanUp')
+
  writeln('TShape.CleanUp')
  end;
+
end;
  
 
+
Constructor TSquare.Init(xx, yy, h, w : single);
  Constructor TSquare.Init(xx, yy, h, w : single);
+
begin
  begin
+
  writeln('TSquare.Init');
writeln('TSquare.Init');
+
  x := xx;
x := xx;
+
  y := yy;
y := yy;
+
  height := h;
height := h;
+
  width := w;
width := w;
+
  if height <> width then
if height <> width then
+
    height := width;
height := width;
+
end;</delphi>
  end;
 
</delphi>
 
  
 
Again, the implentation of constructors look like regular methods except the keywords '''constructor''' and '''destructor''' are used instead of the keywords '''procedure''' and '''function'''.  Consider the following main program.
 
Again, the implentation of constructors look like regular methods except the keywords '''constructor''' and '''destructor''' are used instead of the keywords '''procedure''' and '''function'''.  Consider the following main program.
  
<delphi>
+
<delphi>begin
   begin
+
   writeln;
writeln;
+
 
 
+
  Shape.Init(1,2,3,4);
Shape.Init(1,2,3,4);
+
  Rectangle.Init(11, 22, 33, 44);
Rectangle.Init(11, 22, 33, 44);
+
  Square.Init(111, 222, 333, 444);
Square.Init(111, 222, 333, 444);
+
 
+
  writeln;
        writeln;
+
  write ('Calling Shape.Draw : ');
write ('Calling Shape.Draw : ');
+
  Shape.Draw;
Shape.Draw;
 
  
write ('Calling Rectangle.Draw : ');
+
  write ('Calling Rectangle.Draw : ');
Rectangle.Draw;
+
  Rectangle.Draw;
 
 
write ('Calling Square.Draw : ');
+
  write ('Calling Square.Draw : ');
Square.Draw;
+
  Square.Draw;
 
 
writeln;
+
  writeln;
writeln ('Assigning Rectangle to Shape');
+
  writeln ('Assigning Rectangle to Shape');
Shape := Rectangle;
+
  Shape := Rectangle;
writeln;
+
  writeln;
write ('Calling Shape.Draw : ');
+
  write ('Calling Shape.Draw : ');
Shape.Draw;
+
  Shape.Draw;
 
 
writeln;
+
  writeln;
writeln ('Assigning Square to Shape');
+
  writeln ('Assigning Square to Shape');
Shape := Square;
+
  Shape := Square;
writeln;
+
  writeln;
write ('Calling Shape.Draw : ');
+
  write ('Calling Shape.Draw : ');
Shape.Draw;
+
  Shape.Draw;
  
writeln;
+
  writeln;
writeln ('Assigning Square to Rectangle');
+
  writeln ('Assigning Square to Rectangle');
Rectangle := Square;
+
  Rectangle := Square;
writeln;
+
  writeln;
write ('Calling Rectangle.Draw : ');
+
  write ('Calling Rectangle.Draw : ');
Rectangle.Draw;
+
  Rectangle.Draw;
 
 
writeln;
+
  writeln;
 
 
Shape.CleanUp;
+
  Shape.CleanUp;
Rectangle.CleanUp;
+
  Rectangle.CleanUp;
Square.CleanUp;
+
  Square.CleanUp;
end.
+
end.</delphi>
</delphi>
 
  
 
which produces the output below.
 
which produces the output below.
  
<pre>
+
<pre>TShape.Init
TShape.Init
 
 
TShape.Init
 
TShape.Init
 
TSquare.Init
 
TSquare.Init
Line 475: Line 439:
 
TShape.CleanUp
 
TShape.CleanUp
 
TShape.CleanUp
 
TShape.CleanUp
TShape.CleanUp
+
TShape.CleanUp</pre>
</pre>
 
  
 
Notice that all three calls to the destructor ''CleanUp'' result in the using the inherited destructor of TShape since TRectangle and TSquare did not override the ''CleanUp'' constructor.  The virtual ''Draw'' method was overridden for each sub object type and the output reflects this even when the sub objects were assigned to the parent object.  Finally, since none of the sub objects implemented destructors, all calls to the CleanUp destructors used the one declared for the parent TShape object.  If any of the destructors were declared separately in a sub object, they would have overridden the destructor for TShape since all constructors and destructors are virtual by default.
 
Notice that all three calls to the destructor ''CleanUp'' result in the using the inherited destructor of TShape since TRectangle and TSquare did not override the ''CleanUp'' constructor.  The virtual ''Draw'' method was overridden for each sub object type and the output reflects this even when the sub objects were assigned to the parent object.  Finally, since none of the sub objects implemented destructors, all calls to the CleanUp destructors used the one declared for the parent TShape object.  If any of the destructors were declared separately in a sub object, they would have overridden the destructor for TShape since all constructors and destructors are virtual by default.
Line 492: Line 455:
 
Next some assignments with objects are made and the Draw method to see how the objects were affected.  As can be seen, the results of the assignment operations differ depending whether or not the assignments were done with the pointer variables themselves or whether the pointer was dereferenced.  The results are the same for manipulating and dereferencing  pointers to objects just as they are for any other type of data structure.  Finally, the '''dispose''' procedure is called for all the created objects.  Just as there are three ways to use the new procedure, there are three ways to use the dispose procedure.  All three ways are shown.   
 
Next some assignments with objects are made and the Draw method to see how the objects were affected.  As can be seen, the results of the assignment operations differ depending whether or not the assignments were done with the pointer variables themselves or whether the pointer was dereferenced.  The results are the same for manipulating and dereferencing  pointers to objects just as they are for any other type of data structure.  Finally, the '''dispose''' procedure is called for all the created objects.  Just as there are three ways to use the new procedure, there are three ways to use the dispose procedure.  All three ways are shown.   
  
<delphi>
+
<delphi>Type
Type
+
  TShape = Object
  TShape = Object
+
    x, y : single;
      x, y : single;
+
    height, width : single;
      height, width : single;
+
    procedure GetParams; virtual;
      procedure GetParams; virtual;
+
    procedure Draw;
      procedure Draw;
+
    Constructor Init(xx, yy, h, w : single);
      Constructor Init(xx, yy, h, w : single);
+
    Destructor CleanUp;
      Destructor CleanUp;
+
  end;
  end;
+
 
 
+
  PShape = ^TShape;
  PShape = ^TShape;
+
  PRectangle = ^TRectangle;
  PRectangle = ^TRectangle;
+
  PSquare = ^TSquare;
  PSquare = ^TSquare;
 
  
 
Var
 
Var
Line 513: Line 475:
  
 
begin
 
begin
    
+
   Shape1 := new (PShape, Init(1, 1, 1, 1) );
Shape1 := new (PShape, Init(1, 1, 1, 1) );
+
  Shape2 := new (PShape, Init(2, 2, 2, 2) );
Shape2 := new (PShape, Init(2, 2, 2, 2) );
 
 
 
new (Rectangle, Init(11, 22, 33, 44) );  
 
  
new(Square);
+
  new (Rectangle, Init(11, 22, 33, 44) );  
Square^.Init(111, 222, 333, 444);
 
  
writeln;
+
  new(Square);
 +
  Square^.Init(111, 222, 333, 444);
  
Write ('1) Shape1 : ');
+
  writeln;
Shape1^.Draw;
 
  
Shape1^ := Rectangle^;
+
  Write ('1) Shape1 : ');
Write ('2) Shape1 : ');
+
  Shape1^.Draw;
Shape1^.Draw;
 
  
Rectangle^.x := 77;
+
  Shape1^ := Rectangle^;
Write ('3) Shape1 : ');
+
  Write ('2) Shape1 : ');
Shape1^.Draw;
+
  Shape1^.Draw;
  
Write ('4) Shape2 : ');
+
  Rectangle^.x := 77;
Shape2^.Draw;
+
  Write ('3) Shape1 : ');
 +
  Shape1^.Draw;
  
Shape2 := Square;
+
  Write ('4) Shape2 : ');
Write ('5) Shape2 : ');
+
  Shape2^.Draw;
Shape2^.Draw;
 
  
Square^.y := 88;
+
  Shape2 := Square;
Write ('6) Shape2 : ');
+
  Write ('5) Shape2 : ');
Shape2^.Draw;
+
  Shape2^.Draw;
  
writeln;
+
  Square^.y := 88;
 +
  Write ('6) Shape2 : ');
 +
  Shape2^.Draw;
  
dispose(Shape1);
+
  writeln;
dispose(Shape2, CleanUp);
 
  
Rectangle^.CleanUp;
+
  dispose(Shape1);
dispose(Rectangle);
+
  dispose(Shape2, CleanUp);
  
dispose(Square, CleanUp);
+
  Rectangle^.CleanUp;
 +
  dispose(Rectangle);
  
end.
+
  dispose(Square, CleanUp);
</delphi>
+
end.</delphi>
  
<pre>
+
<pre>TShape.Init
TShape.Init
 
 
TShape.Init
 
TShape.Init
 
TShape.Init
 
TShape.Init
Line 592: Line 550:
 
TShape.CleanUp
 
TShape.CleanUp
 
TShape.CleanUp
 
TShape.CleanUp
TShape.CleanUp
+
TShape.CleanUp</pre>
 
 
</pre>
 
  
  
Line 601: Line 557:
 
[[Programming Using Objects Page 2]]
 
[[Programming Using Objects Page 2]]
  
 
+
[[Category:Tutorials]]
 
 
 
 
------------------------------
 

Revision as of 08:28, 17 March 2011

Objects - Basics

Of the two OOP implementations FPC provides, the one used less often seems to be what is referred to as "Objects" which probably gets its name from the type definition syntax. The syntax diagram for an object type declaration can be found in the FPC language reference - Chapter 5. Basically, an object type looks like a record type with additional fields including procedure fields and also optional keywords which indicate the scope of the fields. In fact, an oversimplified (but valid) simple object is hard to distinguish from a record structure as can be seen below.

<delphi>Type

  MyObject = Object
     f_Integer : integer;
     f_String : ansiString;
     f_Array : array [1.3] of char;
  end;</delphi>

The only difference in the above example is that the Pascal keyword record has been replaced with the keyword object. Things start to be different when methods are added to the object. Object methods are declared in FPC using the keywords procedure or function. These object methods (procedures or functions) are declared the same way as normal Pascal procedures and functions; just that they are declared within the object declaration itself. Lets create a different object; this time possibly as part of an oversimplified graphics drawing program.

<delphi>Type

  DrawingObject = Object
     x, y : single;
     height, width : single;
     procedure Draw;
  end;

Var

 Rectangle : DrawingObject;</delphi>

Besides the simple datatype fields providing some application specific location and size attributes, the object shown above declares an additional parameter; a procedure called Draw. The type declaration is followed by a variable identifier called Rectangle of the type DrawingObject. Next, the Draw procedural code itself needs to be written as well as well as code for accessing and manipulating the data fields, as well as how to invoke the Draw procedure. The following simple program shows how all this works. It should compile and run on any system with FPC 2.2.2 and above. Note: For Mac OS X, the -macpas compiler directive must be turned off.

<delphi>Program TestObjects;

Type

  DrawingObject = Object
     x, y : single;
     height, width : single;
     procedure Draw;  //  procedure declared in here
  end;
 procedure DrawingObject .Draw;
 begin
      writeln('Drawing an Object');
      writeln(' x = ', x, ' y = ', y);  // object fields
      writeln(' width = ', width);
      writeln(' height = ', height);
      writeln;

// moveto (x, y); // probably would need to include a platform dependent drawing unit to do actual drawing // ... more code to actually draw a shape on the screen using the other parameters

 end;

Var

 Rectangle : DrawingObject;

begin

 Rectangle.x:= 50;  //  the fields specific to the variable "Rectangle"
 Rectangle.y:= 100;
 Rectangle.width:= 60;
 Rectangle.height:= 40;
 writeln('x = ', Rectangle.x);
 Rectangle.Draw;  //  Calling the method (procedure)
 with Rectangle do   //  With works the same way even with the method (procedure) field
  begin
      x:= 75;
      Draw;
  end;

end.</delphi>

As can be seen in the above program, the body of the Draw method (procedure) is declared after the object type declaration by concatenating the type identifier with the procedure name. In a more realistic situation, the object would likely be declared in the interface section of a separate unit while the procedure body would be written in the implementation section of the same external unit. In this example, only standard vanilla Pascal is used, but if actual graphic primitives are available, they can be used if desired. The second thing to notice is that inside the Draw method (procedure), the object's data fields are referenced as if they were regular local variables. The only difference to regular local variables is that the values of these fields will persist between calls to the Draw procedure.

In the main program, the fields are assigned values and accessed just like record fields are. Similarly, the Draw procedure is invoked using the same dot notation as the fields. And like records, the with keyword works the same for accessing object fields and invoking methods. Finally, notice that the second time the Draw method is called, all the fields persisted between calls; the only field different is the x attribute which was explicitly changed.

The above example is meant to show the basic mechanics of simple Objects. There are a couple of problems, however. The first problem is that the Draw method only draws one thing and that is a rectangle (although even that is questionable.) Additional methods could be declared in the DrawingObject object such as DrawRectangle, DrawCircle, DrawTriangle, etc..., but this would not be too much different than declaring separate regular procedures and having to use case statements to select the appropriate procedure. So doing it this way with objects would require more work (and is not how it is accomplished.) The other problem is, the object's fields are able to be accessed and modified anywhere in the program very similar to using global variables which makes it harder to write well encapsulated and robust programs. These problems will be addressed in the next section.

Objects - Static Inheritance

Next, the code will be expanded to specifically handle rectangles and squares. Other shapes could be handled as well in a similar manner. An attempt will be made to leverage the existing code, if possible, by creating two new objects to handle the two specific shapes. In order to make the code and output easier to follow, the main object type, DrawingObject has been renamed TShape. Other object types will be named TRectangle, TSquare, and TTriangle with corresponding variables named Shape, Rectangle, Square and Triangle. The letter "T" is used as a prefix to the object type names since it is a commonly used convention in many object libraries and frameworks including FPC's.

What follows are type declarations for: the main TShape object type (previously DrawingObject) along with the new types TRectangle and TSquare. TShape now includes a new method (procedure GetParams) to obtain values for its fields. Next, the types TRectangle and TSquare are declared. TShape is usually referred to as an ancestor or parent object, while TRectangle is said to be a child or subobject or subclass of TShape or that it descends from TShape. This parent child, grandchild heiracry is identified by the qualifier type name following the Object keyword. Similarly, TSquare is a child of TRectangle. Also, TShape is not really a complete object type in itself but rather a template for other objects to inherit a common structure and behavior(s) from. Such templates are often useful for code clarity and there are language features (explained later) which can be used to enforce certain characteristics of these template objects. For an actual application, the TShape type would more than likely be declared differently. Here it is used to illustrate some basic concepts.

<delphi>Type

 TShape = Object
   x, y : single;
   height, width : single;
   procedure GetParams;
   procedure Draw;
 end;
 TRectangle = Object(TShape)
   procedure Draw;
 end;
 TSquare = Object(TRectangle)
   procedure GetParams;
   procedure Draw;
 end;

Var

 Shape : TShape;
 Rectangle : TRectangle;
 Square : TSquare;</delphi>

Notice that TRectangle lists only the Draw procedure while TSquare lists both the GetParams and Draw procedures. Neither of the subobject types include any fields. The "missing" fields and missing procedure names are said to be inherited from the fields and procedures declared in ancestor objects. In this case, any object variables declared and instantiated of type TRectangle will inherit from the TShape type all four fields, (x, y, height, and width) and the GetParams method . At runtine, a variable of the type TRectangle will look and behave like a TShape variable except that the Draw procedure will use different code than the procedure by the same name in an instantiated variable of the TShape type. Similarly, the TSquare object type will inherit all the "grandparent" fields from TShape and have its own different procedure blocks. If desired, TRectangle and TSquare could have declared additional fields in their type definitions which would show up as additional fields (memory locations) available at runtime which instantiated parent object variables would not have or be able to access.

Next are the specific procedure implementations for these object types.

<delphi>procedure TShape.GetParams; begin

 write('TShape.GetParams : ');
 readln(x, y, width, height);
 writeln;

end;

procedure TShape.Draw; begin

 writeln('TShape.Draw');
 writeln('Position: x = ', x:4:0, ' y = ', y:4:0);
 writeln('    Size: w = ', width:4:0, ' h = ', height:4:0);
 writeln;

end;

procedure TRectangle.Draw; begin

 writeln('TRectangle.Draw');

end;

procedure TSquare.GetParams; begin

 write('TSquare.GetParams : ');
 readln(x, y, width, height);
 height := width;
 writeln('making sure all sides are equal for Square');
 writeln;

end;

procedure TSquare.Draw; begin

 writeln('TSquare.Draw');

end;</delphi>

To provide clarity, no actual (platform specific) drawing routines are used. Rather, generic Pascal I/O routines are included to illustrate runtime behavior. This particular hierarchy of objects, fields and methods would likely not be the best implementation for an actual shape drawing application which will become more apparent after new concepts are introduced.

The GetParams method is used to obtain and to perform any needed processing of the field values. Although the object fields could be accessed directly as was done in the previous section, it is considered better programming style to encapsulate the "getting" a "setting" of object fields using object methods. In fact, FPC provides additional language features to help support and enforce narrowing the accessibility and visibility of internal object data which will be covered later.

Since the different types of objects in this program all use the same fields, using a common GetParams method would seem to make sense to include at the top level ancestor object which all other descendent objects can inherit/use. Thus, this method is implemented in the TShape object type.

The TShape object type includes a second method, Draw, which is probably not needed in an actual shape drawing program since it is not a defined shapre. However, a variable declared of type TShape likely will be assigned to a variable of a descendent object type such as TRectangle, which does need to have a draw method which will draw using the code of the descendent object type. Normally, this "template" method would just be declared as a stub or not implemented at all. The syntax for this type of situation and others will be described later. In this case, we want to provide some feedback to inspect field values and illustrate program flow so for demonstration purposes, it includes some writeln statements.

The TRectangle object type does not include a GetParams method but does have its own Draw method. The TSquare object type has it's own GetParams method which repeats much of the the same code from the TShape.GetParams method but also adds code to ensure that the height and width fields are the same. TSquare defines its own Draw method. In an actual program, the Draw method for squares and rectangles would probably be the same and the Draw method for TSquare could be omitted and just inherited from TRectangle.

Here is a sample program which demonstrates the behavior of the various objects.

<delphi>Var

 Shape : TShape;
 Rectangle : TRectangle;
 Square : TSquare;

begin

 writeln;
 writeln ('Getting parameters for Shape');
 Shape.GetParams;
 
 write ('Calling Shape.Draw : ');
 Shape.Draw;
 writeln;
 writeln ('Getting parameters for Rectangle');
 Rectangle.GetParams;
 write ('Calling Rectangle.Draw : ');
 Rectangle.Draw;
 writeln;
 writeln ('Getting parameters for Square');
 Square.GetParams;
 write ('Calling Square.Draw : ');
 Square.Draw;</delphi>

The above code produces the following output.

Getting parameters for Shape
TShape.GetParams : 1 2 3 4
Calling Shape.Draw : TShape.Draw
Position: x =    1 y =    2
    Size: w =    3 h =    4


Getting parameters for Rectangle
TShape.GetParams : 11 22 33 44
Calling Rectangle.Draw : TRectangle.Draw

Getting parameters for Square
TSquare.GetParams : 111 222 333 444
making sure all sides are equal for Square

Calling Square.Draw : TSquare.Draw

Tthe Shape object is initialized by calling the GetParams method and then the Draw method is called which prints out the field values. Next, the same is done for the Rectangle object. Notice that since there is no GetParams method for Rectangles, the compiler used the parent object's method, TShape.GetParams to carry out this action. Finally, the Square object is initialized by calling the GetParams method which is explicitly defined for Squares and the output shows this method was called and did extra processing. The Draw method is called and the Draw method specifically defined for Squares was executed. Note that if the Draw procedure was left out for the TSquare type (as would be reasonable for an actual program) the last output line would look like this.

Calling Square.Draw : TRectangle.Draw

Now what happens if we assign a sub object variable to a parent variable as follows?

<delphi>writeln; writeln ('Assigning Rectangle to Shape'); Shape := Rectangle; writeln; write ('Calling Shape.Draw : '); Shape.Draw;

writeln; writeln ('Assigning Square to Shape'); Shape := Square; writeln; write ('Calling Shape.Draw : '); Shape.Draw;

writeln; writeln ('Assigning Square to Rectangle'); Rectangle := Square; writeln; write ('Calling Rectangle.Draw : '); Rectangle.Draw;</delphi>

The following is output.

Assigning Rectangle to Shape

Calling Shape.Draw : TShape.Draw
Position: x =   11 y =   22
    Size: w =   33 h =   44

Assigning Square to Shape

Calling Shape.Draw : TShape.Draw
Position: x =  111 y =  222
    Size: w =  333 h =  333

Assigning Square to Rectangle

Calling Rectangle.Draw : TRectangle.Draw

The first block of code assigns the Rectangle variable to the Shape variable. When the Shape.Draw method is invoked, it executes the TShape.Draw code and not the TRectangle.Draw code. But as can be seen, the Rectangle fields is what are printed out. This behavior is called Static method inheritance in FPC. If it is desired to instead invoke the child TRectangle.Draw method in this situation, FPC provides way to do this called virtual methods which will be covered in the next section. Continuing on, Shape is assigned to Square and the TShape.Draw method is invoked which prints out the field values of the Square object. Finally, the Rectangle variable is assigned the Square and the TRectangle.Draw method is invoked similarly to the behavior of the TShape.Draw methods.

Assigning an ancestor object to a descendent object is not allowed. The compiler will flag the following line as an error.

<delphi>Rectangle := Shape; <--- can't assign a parent to a child, does not compile</delphi>

Objects - Virtual Inheritance

For a drawing application, a common task would be to refresh the display and stepping through an array or linked list of shapes and calling the draw method. Instead of needing a case statement inside the loop which selects one of many possible specific drawing procedures

<delphi>for k := 1 to NumShapes do

with ShapeRec[k] do
 case ShapeRec[k].ShapeKind of
   cRectangle : DrawRectangle (x, y, width, .height);
   cSquare:  DrawSquare :  (x, y, width, .height);
  cTriangle: DrawTriangle:(x, y, angle1, angle2, base);
end;  // case</delphi>

the code would look something like this

<delphi>for k:= 1 to Numshapes do

Shape[k].draw;</delphi>

Where each Shape object could be one of any sub objects descended from TShape. Code maintenance is made easier since there is one fewer locations in code which needs to be modified. All changes to the behavior of a particular sub object of shape is kept together in one place.

Virtual Keyword

However, as seen in the last section, calling the Draw method for the Shape variable this way will not invoke the appropriate draw method of the sub object which is the behavior that is desired in this (and most) cases. To obtain the desired behavior, the virtual keyword must be inserted after the method declaration in the type definition as follows:

<delphi>Type

 TShape = Object
   x, y : single;
   height, width : single;
   procedure GetParams;
   procedure Draw; virtual;
 end;

 TRectangle = Object(TShape)
   procedure Draw; virtual;
 end;</delphi>

Now if a Rectangle object is assigned to the Shape variable, the Draw method of TRectangle will be used. The term often used to describe this situation is called overriding a parent method. Although the body of main program will the same as in the previous section, the execution behavior will be different. The virtual keyword tells the compiler to hold off fixing the specific procedure used and instead lets the binding of the method be determined at runtime dynamically.

By adding the virtual keyword to the type declarations of TShape, TRectangle and TSquare in the last section, the latter portion of the output would look as shown below. Note that in order to run the previous program using virtual methods, some other code needs to be added in order for the program to run. This additional code is described in the next section.

Assigning Rectangle to Shape

Calling Shape.Draw : TRectangle.Draw

Assigning Square to Shape

Calling Shape.Draw : TSquare.Draw

Assigning Square to Rectangle

Calling Rectangle.Draw : TSquare.Draw

Although it is allowed, mixing virtual methods and non virtual (static) methods in the inheritance hierarchy may result in behavior which is difficult to manage.

Objects - Constructors and Destructors

Compiling the above example program after adding the virtual keywords will result in non fatal compiler warnings about missing constructors. Although the warnings can be ignored, a run time error will almost certainly occur when one of the virtual Draw methods is executed. Due to the peculiarities of this particular OOP implementation, when virtual methods are declared, special initialization code must be included for that object. Specifically, two specialized methods must be included in the object type definition called a constructor and a destructor. The constructor must be called at runtime to initialize the object's virtual method before the method is called. In addition, the constructor can be (and should) be used to initialize any fields, dynamically create associated objects and any other initialization tasks needed when introducing an object. The special destructor method is used to take care of any internal and program specific housekeeping when an object is no longer needed. The initialization and cleanup tasks are more useful when using dynamically allocated objects which will be covered in this section also. For simple programs with few objects (like the one in this tutorial), calling not using destructors won't cause any problems. However, in large programs and those that use large class libraries which routinely allocate and deallocate objects dynamically, the implementation of constructors and destructors is very useful.

Here are are the Shape declarations again, this time using virtual methods and including constructors and destructors.

<delphi>Type

 TShape = Object
   x, y : single;
   height, width : single;
   procedure GetParams; virtual;
   procedure Draw; virtual;
   Constructor Init(xx, yy, h, w : single);
   Destructor CleanUp;
 end;
 TRectangle = Object(TShape)
   procedure Draw; virtual;
 end;
 TSquare = Object(TRectangle)
   procedure GetParams; virtual;

   Constructor Init(xx, yy, h, w : single);
 end;</delphi>

The TShape object type includes fields, two virtual methods (Draw and GetParams), a constructor called Init and a destructor called CleunUp.

TRectangle declares its own Draw method which will override the Parent method in TShape but will inherit all other fields, methods, and the constructor and destructor from TShape.

TSquare inherits everything from its ancestors (TShape mostly) except for the GetPArams method and the Init constructor.

In declaring a constructor or destructor, the keyword constructor or destructor is used instead of the keyword function or procedure. In all other respects, they look just like object methods and are called just like object methods. The use of the constructor/destructor keyword is to let the compiler know of any behind the scenes actions to take. Also notice, that the Init constructor includes a parameter list. Normal methods can include parameter lists although none have been used in the tutorial up to this point.

Fields, methods, constructors and destructors can be declared in any order in the type definition.

Constructors and destructors act like virtual methods without having to add the virtual keyword.

For the current program, the Draw method for TSquare has been removed. It will be assumed that the (fictional) graphics code for for drawing a square is the same as for a rectangle and the Draw method can be inherited from TRectangle. The difference in the TSquare and TRectangle object is inherent in the Init constructor and GetParams method. Implementation of the new constructors and destructors is shown below.

<delphi>Constructor TShape.Init(xx, yy, h, w : single); begin

 writeln('TShape.Init');
 x := xx;
 y := yy;
 height := h;
 width := w;

end;

Destructor TShape.CleanUp; begin

 writeln('TShape.CleanUp')

end;

Constructor TSquare.Init(xx, yy, h, w : single); begin

 writeln('TSquare.Init');
 x := xx;
 y := yy;
 height := h;
 width := w;
 if height <> width then
   height := width;

end;</delphi>

Again, the implentation of constructors look like regular methods except the keywords constructor and destructor are used instead of the keywords procedure and function. Consider the following main program.

<delphi>begin

 writeln;
 
 Shape.Init(1,2,3,4);
 Rectangle.Init(11, 22, 33, 44);
 Square.Init(111, 222, 333, 444);

 writeln;
 write ('Calling Shape.Draw : ');
 Shape.Draw;
 write ('Calling Rectangle.Draw : ');
 Rectangle.Draw;
 write ('Calling Square.Draw : ');
 Square.Draw;
 writeln;
 writeln ('Assigning Rectangle to Shape');
 Shape := Rectangle;
 writeln;
 write ('Calling Shape.Draw : ');
 Shape.Draw;
 writeln;
 writeln ('Assigning Square to Shape');
 Shape := Square;
 writeln;
 write ('Calling Shape.Draw : ');
 Shape.Draw;
 writeln;
 writeln ('Assigning Square to Rectangle');
 Rectangle := Square;
 writeln;
 write ('Calling Rectangle.Draw : ');
 Rectangle.Draw;
 writeln;
 Shape.CleanUp;
 Rectangle.CleanUp;
 Square.CleanUp;

end.</delphi>

which produces the output below.

TShape.Init
TShape.Init
TSquare.Init

Calling Shape.Draw : TShape.Draw
Position: x =    1 y =    2
    Size: w =    4 h =    3

Calling Rectangle.Draw : TRectangle.Draw
Calling Square.Draw : TSquare.Draw

Assigning Rectangle to Shape

Calling Shape.Draw : TRectangle.Draw

Assigning Square to Shape

Calling Shape.Draw : TSquare.Draw

Assigning Square to Rectangle

Calling Rectangle.Draw : TSquare.Draw

TShape.CleanUp
TShape.CleanUp
TShape.CleanUp

Notice that all three calls to the destructor CleanUp result in the using the inherited destructor of TShape since TRectangle and TSquare did not override the CleanUp constructor. The virtual Draw method was overridden for each sub object type and the output reflects this even when the sub objects were assigned to the parent object. Finally, since none of the sub objects implemented destructors, all calls to the CleanUp destructors used the one declared for the parent TShape object. If any of the destructors were declared separately in a sub object, they would have overridden the destructor for TShape since all constructors and destructors are virtual by default.

FPC allows any procedure identifier to be used for a constructor or destructor name. However, by convention, many OOP languages and object libraries use specific identifiers. One convention is Create for constructors and Destroy for destructors.

Objects - Dynamic Variables

Although static object variables can be created on the stack as has been shown up to this point, it is much more likely that for most programs, objects will be created dynamically. The syntax for declaring dynamic objects and invoking them are similar to that of any other dynamically created variable. The only difference is the addition of an extended syntax for the New keyword which incorporates invoking the object constructor.

Here, pointer types and variables are declared for the various object types defined previously with some sample code snippets showing how the resulting dynamic variables are created, manipulated and disposed. Only the object declaration for TShape is shown and none of the methods, constructors or destructors. Note that in order to inspect the field values more easily, the Draw method was reverted back to static so the TShape.Draw behavior was available for all the different types of objects.

The first thing to notice that there are three different ways to create dynamic object variables. All three produce the same results. All use the new procedure in different ways. The Shape1 and Shape2 objects are being created using new as a function, passing it two parameters: (1) the type name and (2) the name of the Init constructor and returning a pointer to the object. The second way is using new as a procedure with the desired variable (Rectangle in this case) to be to be created and the constructor name as parameters. Finally, a pointer for the Square object is created with the new procedure and then the constructor method is called separately. This last manner of dynamic object creation will generate a compiler warning but will compile and run OK.

Next some assignments with objects are made and the Draw method to see how the objects were affected. As can be seen, the results of the assignment operations differ depending whether or not the assignments were done with the pointer variables themselves or whether the pointer was dereferenced. The results are the same for manipulating and dereferencing pointers to objects just as they are for any other type of data structure. Finally, the dispose procedure is called for all the created objects. Just as there are three ways to use the new procedure, there are three ways to use the dispose procedure. All three ways are shown.

<delphi>Type

 TShape = Object
   x, y : single;
   height, width : single;
   procedure GetParams; virtual;
   procedure Draw;
   Constructor Init(xx, yy, h, w : single);
   Destructor CleanUp;
 end;
 
 PShape = ^TShape;
 PRectangle = ^TRectangle;
 PSquare = ^TSquare;

Var

 Shape1, Shape2 : PShape;
 Rectangle : PRectangle;
 Square : PSquare;

begin

 Shape1 := new (PShape, Init(1, 1, 1, 1) );
 Shape2 := new (PShape, Init(2, 2, 2, 2) );
 new (Rectangle, Init(11, 22, 33, 44) ); 
 new(Square);
 Square^.Init(111, 222, 333, 444);
 writeln;
 Write ('1) Shape1 : ');
 Shape1^.Draw;
 Shape1^ := Rectangle^;
 Write ('2) Shape1 : ');
 Shape1^.Draw;
 Rectangle^.x := 77;
 Write ('3) Shape1 : ');
 Shape1^.Draw;
 Write ('4) Shape2 : ');
 Shape2^.Draw;
 Shape2 := Square;
 Write ('5) Shape2 : ');
 Shape2^.Draw;
 Square^.y := 88;
 Write ('6) Shape2 : ');
 Shape2^.Draw;
 writeln;
 dispose(Shape1);
 dispose(Shape2, CleanUp);
 Rectangle^.CleanUp;
 dispose(Rectangle);
 dispose(Square, CleanUp);

end.</delphi>

TShape.Init
TShape.Init
TShape.Init
TSquare.Init

1) Shape1 : TShape.Draw
Position: x =    1 y =    1
    Size: w =    1 h =    1

2) Shape1 : TShape.Draw
Position: x =   11 y =   22
    Size: w =   44 h =   33

3) Shape1 : TShape.Draw
Position: x =   11 y =   22
    Size: w =   44 h =   33

4) Shape2 : TShape.Draw
Position: x =    2 y =    2
    Size: w =    2 h =    2

5) Shape2 : TShape.Draw
Position: x =  111 y =  222
    Size: w =  444 h =  444

6) Shape2 : TShape.Draw
Position: x =  111 y =   88
    Size: w =  444 h =  444


TShape.CleanUp
TShape.CleanUp
TShape.CleanUp


Objects - Continued

Programming Using Objects Page 2