Programming with Objects and Classes

From Lazarus wiki
Revision as of 03:06, 3 March 2009 by Roward (talk | contribs) (→‎Objects - Inheritance: more text entry)
Jump to navigationJump to search

Overview

FPC includes several language extensions to "standard" Pascal implementations for object oriented programming.

  • Objects (chapter 5)
  • Classes (chapter 6)
  • Interfaces (chapter 7)
  • Generics (chapter 8)

These provisions are described in the indicated chapters of the FPC Language Reference Guide: http://www.freepascal.org/docs.var which includes syntax diagrams and further information not contained in this introductory guide. Of these four language features, Objects and Classes form the basis of object oriented programming (OOP) in FPC and for Lazarus. Users migrating from Delphi may want to skip the section on Objects and go to Classes since the Classes implementation/version of OOP is based on Delphi syntax. Conversely, users familiar with older Turbo Pascal or Freevision may want to skip the section on Classes since the Objects implementations is based on the syntax of the latter Pascal dialects. For users familiar with the Apple or THINK Pascal dialects on the Mac OS, neither the Objects or Classes implementation provides a direct migration path. In general, the Classes implementation seems to be more widely in use and is used for Lazarus and also in a future bridge being worked on interfacing Apple's Objective C / Cocoa framework to FPC.

General Concepts

OOP provides different ways to manage and encapsulate data and to manage program flow compared with other available programming language features. This method of programming often lends itself to modeling certain applications such as Graphic User Interfaces and physical systems in a more facile manner. However it is not appropriate for all applications. Program control is not as explicit as using the more standard Pascal procedural constructs and to get the most benefit of OOP, understanding of large class libraries is often required. Also, maintaining large OOP application code has its advantages and disadvantages compared to maintaining procedural code. There are many sources for learning OOP and OO design which are beyond the scope of this guide.

There are a lot of programming languages which incorporate OOP features as extensions or the basis of their language. As such, there are many different terms for describing OO concepts. Even within FPC, some of the terminology overlaps. In general, OOP usually consists of the concept of a programming object (or information unit) which explicitly combines and encapsulates a set of data and procedural code which are related. Usually, this data is persistent during program execution without having to be declared globally. In addition, there are usually features which enable additional objects to be incrementally modified and/or extended based on previously defined objects which is usually referred to by the term "inheritance." Many languages refer to the procedures which belong to an object as an object "method." Much of the power of OOP is realized by late dynamic binding of methods at run time rather than at compile time. This dynamic binding of methods is similar to using procedural variables and procedural parameters but with greater syntactic cohesion with the data it is related to.

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

The next step will expand upon the previous code handle rectangles, squares, and triangles. An attempt will be made to leverage the existing code, if possible, by creating two new objects to handle the two new shapes. In order to make the code and output easier to follow, the main object type, DrawingObject has been renamed to 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.

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.

<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 show any fields. The "missing" fields and procedure names are said to be inherited from the fields and procedures declared in the ancestor objects. In this case, any object variables declared and instantiated of type TRectangle will inherit all four fields, (x, y, height, and width) and the GetParams method from the TShape type. At runtine, a variable of the type TRectangle will look and behave like a TShape variable except that the Draw procedure will use a different code block 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.

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>

Again, in the interest of clarity and portability for this tutorial, no actual (platform specific) drawing routines are used. Rather, generic Pascal I/O routines are included to illustrate program runtime behavior of objects. Similarly, choosing this particular hierarchy of objects, fields and methods would likely not be the best implementation for an actual shape drawing application and later this will become apparent after new concepts are introduced. In actual OO development (like all projects), there are many design decisions that will shape the overall data structures and framework.

The GetParams method is used to obtain field values and to do any needed processing of the input values. Although external procedures could assign values to the object fields directly, it is generally considered better form to encapsulate the "getting" a "setting" of object fields with object methods. In fact, FPC provides additional language features to help support and enforce narrowing the scope and visibility of internal object data.

During design of this tutorial, it was hoped that much of the functionality of getting field values could be declared at the top of the object heirarchy and all other descendents would inherit the exact same code.

Classes