left-icon

Delphi Succinctly®
by Marco Breveglieri

Previous
Chapter

of
A
A
A

CHAPTER 5

Object-Oriented Programming with Delphi

Object-Oriented Programming with Delphi


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.

Classes and Objects

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.

Ancestor Type

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.

Visibility Specifiers

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.

Fields

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.

Methods

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;
  end;

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;
  end;

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;
  end;

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

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

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.

Abstract Classes

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
TShapeRectangle = class

protected

  function GetArea: Integer; override;

end;

// Implementation

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.

Static Members

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.

Instantiating Objects

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.

Type Checking

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
  TSomeClass(SomeObject).SomeMethod();

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

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;

Class Reference Types

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;
begin
  Result := AShapeClass.Create;
end;

Meta-classes allow you to pass classes as parameters.

Code Listing 48: Class Reference Type Value

MyRectangle := ShapeFactory.CreateShape(TRectangleShape);

Summary

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.

Scroll To Top
Disclaimer
DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.