|
| Why abstract?
|
| Whether in Delphi, or in the real World, classes are groupings of objects. Cars are a good example. They are all different, but have a common characteristic that allows you and I to call them by the same generic name of car. You drive, park and service your car in much the same way as everyone else. Except that your car has it's own particular way of doing these things.
|
| So you can say that the class of cars has a common method for driving, parking and so on, but that this method can only be provided as a concept at the generic car level. It can only be properly described for each particular car model. The car class is the best place to put a placeholder for these methods, to ensure that each car can do these things, and in a relatively similar way. This placeholder in the Car class is empty - it simply paves the way for sub-classes to provide a real method. In Delphi terms, it is called an abstract method. It is an abstraction of the method, forcing the child classes (the car models) to flesh out the details of the method for itself.
|
| An example
|
| For our example, we will keep it simple. We'll look at the class of polygons, such as triangles, squares, pentagons, and so on. It's a neat and simple classification - these shapes look pretty much the same, tending to circular as the number of sides increases. There is a lot of commonality that we might want all sub-classes (triangle, square and so on) to have:
|
| Give the area inside the polygon
Give the number of sides it has
Give the length of each side
|
|
| The parent class does not know how to implement these methods because it does not know the number of sides. This is why they are abstract. The triangle class, for example, extends this parent polygon class, and implements the above methods. For simplicity, we will use a fixed length for each side. So this goes inside the parent polygon class. And the parent can provide its own, non-abstract propetry for getting and setting this value.
|
| This shows that a class can have both abstract and non-abstract (concrete) methods.
|
| In summary, we have a polygon class with one concrete method, and two abstract methods. A triangle class, for example, would inherit the concrete side length get method, and must implement the abstract methods (unless it wants to be an abstract class itself).
|
| The polygon master class
|
| Let us define the polygon class first:
| |
| type
// Define our polygon base class
TPolygon = class
private
sideLength : Byte;
sideCount : Byte;
// Define concrete methods - implemnted in this parent class
function GetSideLength : Byte;
procedure SetSideLength(length : Byte);
// Define abstract methods - they must be implemented in a sub-class
function GetSideCount : Byte; virtual; abstract;
procedure SetSideCount(count : Byte); virtual; abstract;
function GetArea : Single; virtual; abstract;
published
// Properties used to access private data fields
// They use private methods to do this
property length : Byte
read GetSideLength
write SetSideLength;
property count : Byte
read GetSideCount
write SetSideCount;
// The polygon constructor
constructor Create(length : Byte); virtual;
end;
|
|
| Notice that the abstract methods must also be defined as virtual (or dynamic. The dynamic construct is semantically equivalent to virtual. Dynamic optimises for code size, virtual for speed). This is allows us to override them in sub-classes.
| |
| To accompany this class definition, we must define the concrete methods defined for this, along with the constructor:
| |
| // Implement a concrete method of TPolygon
function TPolygon.GetSideLength : Byte;
begin
Result := sideLength; // Return the side length as crrently set
end;
// Implement a concrete method of TPolygon
procedure TPolygon.SetSideLength(length : Byte);
begin
sideLength := length; // Set the side length from the passed parameter
end;
// Implement the TPolygon constructor
constructor TPolygon.Create(length : Byte);
begin
// Save the passed parameter
sideLength := length;
end;
|
|
| Defining a triangle sub-class
|
| Here we define our first sub-class of TPolygon:
| |
| // Define our triangle sub-class
type
TTriangle = class(TPolygon)
private
function GetSideCount : Byte; override;
procedure SetSideCount(count : Byte); override;
function GetArea : Single; override;
published
constructor Create(length : Byte); override;
end;
|
|
| There are a number of things to note here. First, that we have specified TPolygon as our base class. Second, that we have defined methods to override the abstract classes in the master class. This helps to clarify the fact that we are not defining them here for the first time. Third, that we do not have to redeclare concrete methods and the properties of TPolygon. We inherit them.
| |
| We even override the constructor - so that we can se the side count of each object to suit the object shape. Such as 3 for a triangle.
| |
| We must implement these override methods:
| |
| // Implement the triangle private methods
function TTriangle.GetSideCount : Byte;
begin
Result := sideCount;
end;
procedure TTriangle.SetSideCount(count : Byte);
begin
sideCount := count; // Set the side count from the passed parameter
end;
function TTriangle.GetArea : Single;
begin
// Return the area of the triangle
Result := sideLength * sideLength * SQRT(3)/4;
end;
constructor TTriangle.Create(length : Byte);
begin
// Save the passed parameter
sideLength := length;
// And set the side count to 3 - a triangle
sideCount := 3;
end;
|
|
| Defining a square subclass
|
| We can define a square subclass similarly. We'll see this subclass in the full code below. It is similar to the triangle class.
| |
| Showing the full code
|
| Now let us show these three classes altogether in one full unit that you can copy and paste into Delphi, remembering to follow the instructions at the start.
| |
| // Full Unit code.
// -----------------------------------------------------------
// You must store this code in a unit called Unit1 with a form
// called Form1 that has an OnCreate event called FormCreate.
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs;
type
// Define our polygon base class
TPolygon = class
private
sideLength : Byte;
sideCount : Byte;
// Define concrete methods - implemnted in this parent class
function GetSideLength : Byte;
procedure SetSideLength(length : Byte);
// Define abstract methods - they must be implemented in a sub-class
function GetSideCount : Byte; virtual; abstract;
procedure SetSideCount(count : Byte); virtual; abstract;
function GetArea : Single; virtual; abstract;
published
// Properties used to access private data fields
// They use private methods to do this
property length : Byte
read GetSideLength
write SetSideLength;
property count : Byte
read GetSideCount
write SetSideCount;
// The polygon constructor
constructor Create(length : Byte); virtual;
end;
// Define our triangle sub-class
TTriangle = class(TPolygon)
private
function GetSideCount : Byte; override;
procedure SetSideCount(count : Byte); override;
function GetArea : Single; override;
published
constructor Create(length : Byte); override;
end;
// Define our triangle sub-class
TSquare = class(TPolygon)
private
function GetSideCount : Byte; override;
procedure SetSideCount(count : Byte); override;
function GetArea : Single; override;
published
constructor Create(length : Byte); override;
end;
// The form class itself
TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
// Implement a concrete method of TPolygon
function TPolygon.GetSideLength : Byte;
begin
Result := sideLength; // Return the side length as crrently set
end;
// Implement a concrete method of TPolygon
procedure TPolygon.SetSideLength(length : Byte);
begin
sideLength := length; // Set the side length from the passed parameter
end;
// Implement the TPolygon constructor
constructor TPolygon.Create(length : Byte);
begin
// Save the passed parameter
sideLength := length;
end;
// Implement the triangle private methods
function TTriangle.GetSideCount : Byte;
begin
Result := sideCount;
end;
procedure TTriangle.SetSideCount(count : Byte);
begin
sideCount := count; // Set the side count from the passed parameter
end;
function TTriangle.GetArea : Single;
begin
// Return the area of the triangle
Result := sideLength * sideLength / 2;
end;
constructor TTriangle.Create(length : Byte);
begin
// Save the passed parameter
sideLength := length;
// And set the side count to 3 - a triangle
sideCount := 3;
end;
// Implement the square private methods
function TSquare.GetSideCount : Byte;
begin
Result := sideCount;
end;
procedure TSquare.SetSideCount(count : Byte);
begin
sideCount := count; // Set the side count from the passed parameter
end;
function TSquare.GetArea : Single;
begin
// Return the area of the triangle
Result := sideLength * sideLength;
end;
constructor TSquare.Create(length : Byte);
begin
// Save the passed parameter
sideLength := length;
// And set the side count to 4 - a square
sideCount := 4;
end;
// Main line code
procedure TForm1.FormCreate(Sender: TObject);
var
triangle : TTriangle;
square : TSquare;
begin
// Create triangle and square objects
triangle := TTriangle.Create(10);
square := TSquare.Create(10);
// Show the area of each polygon
ShowMessageFmt('Triangle with side length %d area = %f',
[triangle.length, triangle.GetArea]);
ShowMessageFmt('Square with side length %d area = %f',
[square.length, square.GetArea]);
// Now change the side length of the triangle and reshow
triangle.length := 5;
ShowMessageFmt('Triangle with side length %d area = %f',
[triangle.length, triangle.GetArea]);
end;
end.
|
|
| The ShowMessage rountines display the following :
Triangle with side length 10 area = 50.0
Square with side length 10 area = 100.0
Triangle with side length 5 area = 12.5
|
|
| Note that we have used triangle and square routines to get the area, and a polygon defined routine to get and set the side length.
| |
| A tangible benefit of abstraction
|
| If the above has appeared a little academic, then there is one nice benefit of this abstraction. Whilst an abstract class such as a polygon cannot be used to create a new object (such an object would not be fully defined), you can create a reference to one. By doing this, you can point this reference to any sub-class, such as a triangle, and use the polygon class abstract methods. Your code can process arrays of sub-classes, and treat them all as if they were generic polygons, without needing to know what variants they each are.
|
| How do abstract classes differ from Interfaces?
|
| Abstract classes and Interfaces are quite similar, in that they both provide placeholder methods. Their approach is quite different though. Interfaces are not classes. Your class does not extend an interface. The nature of your class has basically nothing to do with the interface. It can be a new class, or it can extend an existing class. The placeholder methods in the Interface are implemented in your class. All of them must be implemented, in addition to other methods in your class. By implementing the interface, your class simply adds a standard set of metods or properties - a flavour in common with other classes implementing the interface.
|
| Note also that a class may extend only one ancestor class, but can implement multiple interfaces. |
| | | | |