CHAPTER 5
Delphi has full support for the object-oriented programming paradigm (OOP).
When you start writing your business logic, you can choose to follow an imperative procedural way of coding, therefore declaring global types, variables and routines, or leverage the support for object orientation and model your business logic creating classes with fields, methods and properties, interfaces and other typical elements of any OOP-language. You can even mix and match both styles, using the best parts of each.
This chapter shows how to do OOP programming with Object Pascal but it assumes that you are already familiar with its concepts.
The fundamental elements of OOP are classes and objects. A class defines a structure that contains data and logic: data is represented by fields, and logic is contained inside methods.
Each class can be seen as a template that defines the state and the behavior of any object that belongs to it.
Here is the full code of a unit containing a sample class declaration to represent an abstract two-dimensional shape.
Code Listing 26: Full Sample Class Declaration
unit Shape2D; interface uses System.Classes, Vcl.Graphics; type { TShape2D } TShape2D = class abstract private FHeight: Integer; FWidth: Integer; procedure SetHeight(const Value: Integer); procedure SetWidth(const Value: Integer); protected function GetArea: Integer; virtual; abstract; public constructor Create; procedure Paint(ACanvas: TCanvas); virtual; property Area: Integer read GetArea; property Height: Integer read FHeight write SetHeight; property Width: Integer read FWidth write SetWidth; end; implementation { TShape2D } constructor TShape2D.Create; begin inherited Create; FHeight := 10; FWidth := 10; end; procedure TShape2D.Paint(ACanvas: TCanvas); begin ACanvas.Brush.Color := clWhite; ACanvas.FillRect(ACanvas.ClipRect); end; procedure TShape2D.SetHeight(const Value: Integer); begin if Value < 0 then Exit; FHeight := Value; end; procedure TShape2D.SetWidth(const Value: Integer); begin if Value < 0 then Exit; FWidth := Value; end; end. |
We’ll use this sample to explore some of the Object Pascal OOP features.
Classes, like any other Pascal type, can be declared in the interface section of a unit to make them available to other units. To use a class in the same unit where it is declared, put it in the implementation section.
Tip: You don’t have to manually write every single line of a class implementation. Once you have completed the declaration of your class in the interface section, put the editor cursor inside of it and press CTRL+SHIFT+C to automatically create its implementation.
If you don’t specify an ancestor class, Delphi assumes that you are extending TObject, which is the mother of all the classes and provides the basic support for memory allocation and generic object management.
Here is a sample descendant for our TShape2D class.
Code Listing 27: Sample Descendant Class Declaration
unit ShapeRectangle; interface uses Shape2D, Vcl.Graphics; type { TShapeRectangle } TShapeRectangle = class (TShape2D) protected function GetArea: Integer; override; public procedure Paint(ACanvas: TCanvas); override; end; implementation { TShapeRectangle } procedure TShapeRectangle.Paint(ACanvas: TCanvas); begin inherited Paint(ACanvas); ACanvas.Rectangle(ACanvas.ClipRect); end; function TShapeRectangle.GetArea: Integer; begin Result := Height * Width; end; end. |
The TShapeRectangle class is a descendant of TShape2D: this means that the class inherits everything that is part of the ancestor class, like the Height and Width properties, and it has the faculty of extending it by adding more members or change the logic. However, this applies only where the base class allows it and through specific extension points, like virtual (overridable) methods.
Delphi does not support multiple inheritance; you cannot inherit from more than one class.
The visibility of members inside the class does not depend on the section of the unit (interface or implementation) where they are declared or implemented, but there are specific keywords to assign visibility.
Table 9: Visibility Specifiers
Keyword | Description |
private | Members are visible only to the class that contains them and by the unit where the class is declared. |
protected | Members are visible to the class that contains them and all its descendants, including the unit where the class is declared. |
public | Members are visible to the class that contains them, the unit where it is declared and to other units. |
published | This has the same effect of the public specifier, but the compiler generates additional type information for the members and, if applied to properties, they become available in the Object Inspector. |
Delphi also supports a couple of additional specifiers, strict private and strict protected. If applied to a member, this means members will be visible only to the class itself and not inside the unit where the class is declared.
The TShape2D class has some fields:
Code Listing 28: Instance Fields
TShape2D = class abstract private FHeight: Integer; FWidth: Integer; end; |
These fields are declared under the private section, therefore they are not accessible to client code outside the class. This means that changing their values must be done by calling a method like SetHeight() and SetWidth(), or by changing the value of a property linked to a field.
Note: You are seeing some coding conventions in action here: all the types usually start with the letter “T,” and the fields start with the letter “F.” If you follow these widespread conventions, you will be able to read code written by other people without much effort.
When an instance of this class is created, fields are automatically initialized to their default value, so any integer field becomes 0 (zero), strings are empty, Boolean are false, and so on.
If you want to set an initial value to any field, you must add a constructor to your class.
Here is an excerpt of the TShape2D method declarations taken out from our sample code.
Code Listing 29: Method Declarations
TShape2D = class abstract private procedure SetHeight(const Value: Integer); procedure SetWidth(const Value: Integer); protected function GetArea: Integer; virtual; abstract; public procedure Paint(ACanvas: TCanvas); virtual; |
Methods are functions and procedures that belong to a class and usually change the state of the object or let you access the object fields in a controlled way.
Here is the body of SetWidth() method that you can find in the implementation section of the same unit.
Code Listing 30: Method Implementation
procedure TShape2D.SetWidth(const Value: Integer); begin if Value < 0 then Exit; FWidth := Value; end; |
Each instance of a non-static method has an implicit parameter called Self that is a reference to the current instance on which the method is called.
Our sample includes a virtual method marked with the virtual directive.
Code Listing 31: Virtual Method Declaration
TShape2D = class abstract protected procedure Paint(ACanvas: TCanvas); virtual; |
A descendant class that inherits from TShape2D can override the method using the override directive.
Code Listing 32: Overriden Method Declaration
TShapeRectangle = class (TShape2D) protected procedure Paint(ACanvas: TCanvas); override; |
The implementation in the descendant class will be called in place of the inherited method.
Code Listing 33: Overridden Method Implementation
procedure TShapeRectangle.Paint(ACanvas: TCanvas); begin inherited Paint(ACanvas); ACanvas.Rectangle(ACanvas.ClipRect); end; |
The descendant method has the ability to call the inherited implementation, if needed, using the inherited keyword. The Paint() method in the base class fills the background with a color; the descendant classes inherit and call that implementation adding the instructions to draw the specific shape on the screen.
Properties are means to create field-like identifiers to read and write values from an object while protecting the fields that store the actual value, or using methods to return or accept values.
Code Listing 34: Properties
TShape2D = class abstract private property Area: Integer read GetArea; property Height: Integer read FHeight write SetHeight; property Width: Integer read FWidth write SetWidth; end; |
In the sample above, when you read the Height property, Delphi takes the value from the private FHeight field, which is specified after the read clause; when you set the value of Height, Delphi calls the SetHeight method specified after the write clause and passes the new value.
Note: Both the read and write accessors are optional: you can create read-only and write-only properties.
Properties give the advantages of fields and their ease of access, but keep a layer of protection to passed-in values from client code.
Constructors are special static methods that have the responsibility to initialize a new instance of a class. Declare them using the constructor keyword.
Here is a sample constructor declaration taken from our sample class.
Code Listing 35: Constructor Declaration
TShape2D = class abstract public constructor Create; end; |
Let’s examine the implementation of the constructor method.
Code Listing 36: Constructor Implementation
constructor TShape2D.Create; begin inherited Create; FHeight := 10; FWidth := 10; end; |
The inherited keyword calls the constructor inherited from the base class. You should add this statement to almost every constructor body to ensure that the all the inherited fields are correctly initialized.
Some classes also define a destructor method. Destructors are responsible for freeing the memory allocated by the class, the owned objects, and releasing all the resources created by the object. They are always marked with the override directive so they are able to call the inherited destructor implementation and free the resources allocated by the base class.
When you think about a class hierarchy, there are times when you are unable to implement some methods.
Consider our ancestor TShape2D sample class: how would you implement the GetArea() method? You could add a virtual method that returns 0 (zero), but the zero value has a specific meaning. The definitive answer is that it does not make sense to implement the GetArea() method in TShape2D class, but it has to be there, since every shape logically has it. So, you can make it abstract.
Code Listing 37: Abstract Class
TShape = class abstract protected function GetArea: Integer; virtual; abstract; end; |
Note: Each class that has abstract methods should be marked abstract itself.
The abstract method is devoid of implementation; it has to be a virtual method, and descendant classes must override it.
Code Listing 38: Implementing an Abstract Class
// Interface protected function GetArea: Integer; override; end; function TShapeRectangle.GetArea: Integer; begin Result := Height * Width; end; |
Obviously you must not call the inherited method in the base class, because it is abstract and it would lead to an AbstractError.
Tip: The compiler issues a warning when you create an instance of abstract classes. You should not do that to avoid running into calls to abstract methods.
Object Pascal also supports static members. Instance constructors and destructors are intrinsically static, but you can add static fields, members, and properties to your classes.
Look at an extract of the TThread class declaration from the System.Classes unit.
Code Listing 39: Static Members
TThread = class private type PSynchronizeRecord = ^TSynchronizeRecord; TSynchronizeRecord = record FThread: TObject; FMethod: TThreadMethod; FProcedure: TThreadProcedure; FSynchronizeException: TObject; end; private class var FProcessorCount: Integer; class constructor Create; class destructor Destroy; class function GetCurrentThread: TThread; static; end; |
The ProcessorCount variable is a private static member, and its value is shared by all the instances of TThread class thanks to the class var keywords.
Here you can also see a sample of class constructor and class destructor, a couple of methods aimed at the initialization (and finalization) of static field values, where class function is a static method.
We have spent a lot of time on classes, but how can we create concrete instances?
Here is a sample of code that creates and consumes an instance of TMemoryStream.
Code Listing 40: Creating an Instance
var Stream: TMemoryStream; begin Stream := TMemoryStream.Create; try Stream.LoadFromFile('MyData.bin'); finally Stream.Free; end; end; |
The constructor called using the form TClassName.Create allocates the necessary memory to hold object data and returns a reference to the object structure.
The variable that holds the reference to the created object must be of the same type or an ancestor type.
Tip: If you want to make a reference type variable point to no object, you can set it to nil, a keyword that stands for a null reference value, like null in C#.
Call the method LoadFromFile to bring in the TMemoryStream. This will buffer all the data contained in the specified file (the path is specified as the first parameter to the method).
The object is then destroyed calling the Free method.
Note: Why not call the Destroy() method instead of Free()? After all, Destroy() is the “official” destructor. You must call Free() because it is a non-virtual method and has a static address, so it can be safely called whatever the object type is. The Free() method also checks if the reference is not assigned to nil before calling Destroy().
You might ask yourself what the try…finally construct stands for.
Delphi does not have a garbage collector; you are responsible for freeing every object you create. The try…finally block ensures that the resource is freed even if an error occurs during its use phase. Otherwise, a memory leak occurs.
You can check if an object belongs to a class using the is operator. If you get a positive result, you can cast the object to the specified type and access its members safely.
Code Listing 41: Using the is Keyword
if SomeObject is TSomeClass then |
You can use the as operator to do type checking and casting at the same time.
Code Listing 42: Using the as Keyword
(SomeObject as TSomeClass).SomeMethod(); |
If SomeObject is not of TSomeClass type or a descendant of it, you get an exception.
Note: Never use the is and as operators at the same time, since they perform the same type-checking operations and that has some cost in terms of computing. If is operator checking is successful, use always a direct cast. However, never do a direct cast without checking before.
Interfaces are the thinner layer you can put between implementations, and are a fundamental tool for achieve high decoupling in code.
An interface is the most abstract class you can create in Delphi. It has only abstract methods with no implementation.
Code Listing 43: Interface
ICanPaint = interface ['{D3C86756-DEB7-4BF3-AA02-0A51DBC08904}'] procedure Paint; end; |
Any class can only have a single ancestor, but it can implement any number of interfaces.
Note: Each interface declaration must have a GUID associated to it. This requirement has been introduced with COM support, but there are also lot of RTL parts where interfaces are involved that work with GUIDs. It could appear as an annoyance, but adding the GUID to the interface is really fast and simple: just press the CTRL+SHIFT+G key combination inside the Code Editor.
While classes have TObject as a common ancestor, interfaces all have IInterface.
Code Listing 44: Base IInterface Declaration
IInterface = interface ['{00000000-0000-0000-C000-000000000046}'] function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall; function _AddRef: Integer; stdcall; function _Release: Integer; stdcall; end; |
The IInterface methods add support for reference counting. A class that implements an interface should inherit from TInterfacedObject, since this class provides the implementation for IInterface methods that are responsible for reference counting.
Code Listing 45: Interface Implementation
TShape = class (TInterfacedObject, ICanPaint) procedure Paint; end; |
Delphi includes a unique and powerful feature called class reference types (also called “meta-classes”).
Here is a sample declaration to clarify the concept.
Code Listing 46: Class Reference Type Declaration
TShapeClass = class of TShape; |
Short and simple, but what does it mean? You can use this type for variables, fields, properties, and parameters, and you can pass the TShape class or one of its descendants as a value.
Suppose you want to declare a method that is able to create any kind of TShape object you want by calling its constructor. You can use the class reference type to pass the shape class as a parameter, and the implementation would be similar to the one shown in Code Listing 49.
Code Listing 47: Class Reference Type Usage
function TShapeFactory.CreateShape(AShapeClass: TShapeClass): TShape; |
Meta-classes allow you to pass classes as parameters.
Code Listing 48: Class Reference Type Value
MyRectangle := ShapeFactory.CreateShape(TRectangleShape); |
In this chapter we have seen the basics of object-oriented programming with Object Pascal in Delphi. Since this is a Succinctly series book, we do not have all the space needed to explore every little detail of class implementation in Delphi.
If you really want to delve into all the possibilities of Object Pascal language and apply the most advanced programming techniques, like GoF Design Patterns, Inversion of Control, Dependency Injection and many more, I recommend the Coding in Delphi and More Coding in Delphi books from Nick Hodges.