Oeps

Inleiding

In dit hoofdstuk zal ik proberen de basis-principes van Object Oriented Programming uit te leggen.

Ik zal dat doen aan de hand van een viertal voorbeelden.

Als uitgangspunt neem ik de uitbreiding van Records naar Classes.

Overigens is dit hoofdstuk bedoeld voor mensen die al wat langer met Delphi programmeren.

In dit hoofdstuk leer je:

Begrippen

Bij oop (Object Oriented Programming; spreek uit als oep) zijn een aantal begrippen van belang.

  1. Encapsulation of Inkapseling
  2. Inheritance of Overerving
  3. Polymorfism of Polymorfisme
  4. Class of klasse
  5. Object of Instantie

Ad. 1: Encapsulation betekent zoveel als dat je de velden kunt verbergen voor de buitenwereld.
Ad. 2: Inheritance betekent dat je van bestaande Classes gebruik kan maken en daar naar eigen believen functionaliteit aan kunt toevoegen.
Ad. 3: Polymorfism betekent dat verschillende methodes toch dezelfde naam kunnen hebben.
Ad. 4: Een Class is niets anders dan de definitie van een object, zeg maar de blauwdruk.
Ad. 5: Een Object is een ‘instantie’ van een Class. Werken doe je met Objecten en niet met Classes. Van een Class kunnen meerdere Objecten bestaan, andersom niet!

De voorbeelden moeten één en ander duidelijk gaan maken. Voor het merendeel maak je van bovenstaande al impliciet gebruik, ervan uitgaande dat je al in Delphi of een andere Windows-taal programmeert.

Eerst Delphi zelf

We gaan eerst eens kijken wat Delphi zelf doet met Classes e.d..

– Start eens een nieuw project in Delphi en kijk naar de code:

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls,   Forms, Dialogs;

type
  TForm1 = class(TForm)
    private
    { Private declarations }
    public
    { Public declarations }
end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

end.

Wat hopelijk opvalt is dat Delphi een Class TForm1 heeft aangemaakt. TForm1 stamt af van TForm. Dit betekent dat TForm1 dezelfde eigenschappen en functionaliteit heeft als TForm. Laat dit project maar eens runnen. Je krijgt nu een scherm die je kunt verplaatsen, vergroten, verkleinen, maximaliseren etc., etc..

– Zet eens een button op het form en kijk weer naar de code:

type
  TForm1 = class(TForm)
  Button1: TButton;
  private
  { Private declarations }
  public
  { Public declarations }
end;

De button staat in de Class-definitie van TForm1. Dit is nu het wezenlijke van overerving of inheritance: Maak (her)gebruik van bestaande functionaliteit en voeg en implementeer alleen nieuwe functionaliteiten toe, ofwel: ga niet opnieuw het wiel uitvinden.

– Dubbelklik op de Button en voeg de volgende regel toe:
ShowMessage(‘Knop geklikt’);

– Kijk weer naar de code:

type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

procedure TForm1.Button1Click(Sender: TObject);
begin
  ShowMessage(‘Knop geklikt’);
end;

De procedure Button1Click is toegevoegd aan de Class-definitie. In de implementation-sectie staat de implementatie van de procedure. Kijk goed naar de heading van de implementatie. Je moet altijd expliciet aangeven van welke Class je iets implementeerd.

– Probeer de opbouw van de code, zoals Delphi die zelf aanmaakt, goed te begrijpen.

– Wanneer je denkt dat je het begrijpt kijk dan eens naar onderstaande code:

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls,    Forms, Dialogs,
  StdCtrls;

type
  TForm1 = class(TForm)
  Button1: TButton;
  Edit1: TEdit;
  Label1: TLabel;
  procedure Button1Click(Sender: TObject);
  private
  { Private declarations }
  public
  { Public declarations }
end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

function AantalCijfers : Integer;
  var a,t : Integer;
begin
  a := 0;
  for t := 1 to Length(Edit1.Text) do
    if Edit1.Text[t] in [‘0’..’9′]
      then Inc(a);
  AantalCijfers := a;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  Label1.Caption := IntToStr(AantalCijfers);
end;

end.

– Voorspel waar de compiler met een foutmelding komt, en welke dat is.

– Geef twee oplossingen voor dit probleem.

Op weg naar een eigen Class

Wanneer we verschillende typen variabelen tot één logisch geheel willen maken, kunnen we in Pascal, en dus ook in Delphi, gebruik maken van Record’s.

Nog logischer zou zijn om ook functionaliteit betreffende die variabelen hierin te kunnen onderbrengen.
Helaas biedt standaard Pascal hiervoor geen mogelijkheden. Delphi echter wel!

In de volgende twee voorbeelden zullen we zien hoe een en ander in zijn werk gaat.

Het eerste voorbeeld gaat uit van de oude vertrouwde record, het tweede gaat uit van een Class-definitie. Beide programma’s doen hetzelfde en hebben dus dezelfde functionaliteit.

De programma’s houden Naam, Adres, Plaats en Leeftijd van personen bij en hebben daarbij de volgende functionaliteit: Naar volgende gaan, naar vorige gaan, indien nodig een nieuwe toevoegen, bewaren op schijf, halen van schijf.

Voorbeeld 1

unit vbU1;
interface
uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls;type
  TNAW = Record
    Naam, Adres, Plaats : ShortString;
    Leeftijd : Byte;
  end;
  TNamen = Array of TNAW;
  TForm1 = class(TForm)
    Edit1: TEdit;
    Edit2: TEdit;
    Edit3: TEdit;
    Edit4: TEdit;
    Label1: TLabel;
    Label2: TLabel;
    Label3: TLabel;
    Label4: TLabel;
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    Button4: TButton;
    procedure FormCreate(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Edit1Change(Sender: TObject);
    procedure Edit2Change(Sender: TObject);
    procedure Edit3Change(Sender: TObject);
    procedure Edit4Change(Sender: TObject);
    procedure FormActivate(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    procedure Button4Click(Sender: TObject);
  private
    { Private declarations }
    procedure UpdateScreen;
  public
    { Public declarations }
  end;var
  Form1: TForm1;implementationvar
  Namen : TNamen;
  NamenFile : File of TNAW;
  AantalNamen,HuidigeNaam : Integer;{$R *.DFM}procedure TForm1.FormCreate(Sender: TObject);
begin
  AantalNamen := 1;
  HuidigeNaam := 0;
  SetLength(Namen,AantalNamen);
end;procedure TForm1.UpdateScreen;
begin
  Edit1.Text := Namen[Huidigenaam].Naam;
  Edit2.Text := Namen[Huidigenaam].Adres;
  Edit3.Text := Namen[Huidigenaam].Plaats;
  Edit4.Text := IntToStr(Namen[Huidigenaam].Leeftijd);
end;procedure TForm1.Button2Click(Sender: TObject);
begin
  Inc(HuidigeNaam);
  If HuidigeNaam = AantalNamen Then
    Begin
      Inc(AantalNamen);
      SetLength(Namen,AantalNamen);
      Edit1.Text := ”;
      Edit2.Text := ”;
      Edit3.Text := ”;
      Edit4.Text := ”;
    End
  Else UpdateScreen;
end;procedure TForm1.Button1Click(Sender: TObject);
begin
  If HuidigeNaam > 0 Then
    Begin
      Dec(HuidigeNaam);
      UpdateScreen;
    End
end;procedure TForm1.Edit1Change(Sender: TObject);
begin
  Namen[Huidigenaam].Naam := Edit1.Text;
end;procedure TForm1.Edit2Change(Sender: TObject);
begin
  Namen[Huidigenaam].Adres := Edit2.Text;
end;procedure TForm1.Edit3Change(Sender: TObject);
begin
  Namen[Huidigenaam].Plaats := Edit3.Text;
end;procedure TForm1.Edit4Change(Sender: TObject);
  var c : Integer;
begin
  Val(Edit4.Text,Namen[Huidigenaam].Leeftijd,c);
end;procedure TForm1.FormActivate(Sender: TObject);
begin
  Edit1.SetFocus;
end;procedure TForm1.Button3Click(Sender: TObject);
  var f : Integer;
begin
  AssignFile(NamenFile,’vb1.rec’);
  Rewrite(NamenFile);
  For f := 0 To AantalNamen – 1 Do
    Write(NamenFile,Namen[f]);
  CloseFile(NamenFile);
end;procedure TForm1.Button4Click(Sender: TObject);
  var f : Integer;
begin
  SetLength(Namen,0);
  AssignFile(NamenFile,’vb1.rec’);
  Reset(NamenFile);
  f := 0;
  While not Eof(NamenFile) Do
    Begin
      SetLength(Namen,Succ(f));
      Read(NamenFile,Namen[f]);
      Inc(f);
    End;
  CloseFile(NamenFile);
  AantalNamen := f;
  HuidigeNaam := 0;
  UpdateScreen;
end;end.

– Probeer dit programma te doorgronden. Dit moet niet al te veel moeite kosten.

– Merk ook op dat in dit programma gebruik wordt gemaakt van een dynamische array.

In bovenstaand voorbeeld behoren naast de variabelen Naam, Adres, Plaats en Leeftijd, die we in een Record velden noemen, ook de functionaliteiten Naar volgende, Bewaar op schijd e.d. tot één logisch geheel.

In het nu volgende voorbeeld gaan we dit voor elkaar maken d.m.v. een Class-definitie.

– Bekijk de volgende type-declaraties:

type
  TNawRec =  Record
    Naam, Adres, Plaats : ShortString;
    Leeftijd : Byte;
  end;
  TNAW = class
    Namen : Array of TNawRec;
    AantalNamen,HuidigeNaam : Integer;
    constructor Init;
    procedure Nieuw;
    procedure Volgende;
    procedure Vorige;
    procedure SaveToFile(FileName: ShortString);
    procedure LoadFromFile(FileName: ShortString);
  end;

Het Record uit het vorige voorbeeld bestaat nog steeds.

Maar in plaats van alles vervolgens onder te brengen in losse variabelen, procedures en functies, brengen we nu alles onder in een Class-definitie.

De Class TNAW bezit alles wat we willen: alle velden, alle namen en alle functionaliteit.

Uiteraard is het zo dat we de definitie nog wel moeten uitwerken, ofwel moeten implementeren.

En dat gaat analoog aan hoe Delphi dat doet met de opbouw van een venster!

Laten we bijvoorbeeld gaan kijken naar de implementatie van de procedure Nieuw.

procedure TNAW.Nieuw;
begin
  Inc(AantalNamen);
  SetLength(Namen,AantalNamen);
end;

In de aanhef van de procedure moet expliciet worden vermeld bij welke Class de procedure hoort.

Vandaar procedure TNAW.Nieuw.

Binnen de procedure kunnen we vrij gebruik maken van alle velden en functionaliteiten van de Class.

Nadat we de implementatie hebben gemaakt kunnen we de Class gaan gebruiken.

Hiervoor dienen we van de Class een Object of Instantie te maken.

Dat doen we door allereerst een variabele te definieren van het Class-type, in ons voorbeeld dus van het type TNAW.

Var
  Namen: TNAW;

Vervolgens maken we de instantie als volgt:

Namen := TNAW.Create;

Dus aan de variabele kennen we toe de naam van de Class gevolgd door .Create.

De methode (procedure) Create stamt af van het moeder-Object in Delphi genaamd TObject.

Iedere Class stamt hier direct of indirect vanaf.

Om het Object of Instantie weer op te ruimen maken we gebruik van de methode Free, ook van TObject.

Nu zal het vaak voorkomen dat we bij het maken van een Object meteen een aantal zaken willen regelen.

In ons voorbeeld willen we ervoor zorgen dat de variabelen Aantalnamen en Huidigenaam een startwaarde krijgen en dat er een eerste naam tot onze beschikking staat.

Met de methode Create gaat dit niet lukken.

In Delphi kunnen we dit voor elkaar krijgen door een Constructor-methode te maken die dan in plaats komt van de Create-methode.

De aanmaak van een Object wordt in ons voorbeeld dan:

  Namen := TNAW.Init;

,omdat Init de naam van onze Constructor is.

Het is overigens toegestaan om aan een Constructor ook parameters mee te geven. Wat dat betreft is een Constructor dus gewoon te beschouwen als een normale procedure.

Zo zouden we ook een Destructor kunnen maken die dan in plaats komt van de methode Free.

Om, tot slot, gebruik te kunnen maken van de functionaliteit van ons Object, roepen we die functionaliteit aan d.m.v. de variabele gevolgd door een punt met daarachter de naam van de methode, net zoals het gebruik van ieder ander Object in Delphi.

Dus bijvoorbeeld Namen.SaveToFile(‘vb2.rec’);.

En voor de volledigheid volgt hier het gehele tweede voorbeeld:

Voorbeeld 2

unit vbU2;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls,    Forms, Dialogs,
  StdCtrls;

type
  TNawRec =  Record
    Naam, Adres, Plaats : ShortString;
    Leeftijd : Byte;
  end;
  TNAW = class
    Namen : Array of TNawRec;
    AantalNamen,HuidigeNaam : Integer;
    constructor Init;
    procedure Nieuw;
    procedure Volgende;
    procedure Vorige;
    procedure SaveToFile(FileName: ShortString);
    procedure LoadFromFile(FileName: ShortString);
  end;
  TForm1 = class(TForm)
    Edit1: TEdit;
    Edit2: TEdit;
    Edit3: TEdit;
    Edit4: TEdit;
    Label1: TLabel;
    Label2: TLabel;
    Label3: TLabel;
    Label4: TLabel;
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    Button4: TButton;
    procedure FormCreate(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Edit1Change(Sender: TObject);
    procedure Edit2Change(Sender: TObject);
    procedure Edit3Change(Sender: TObject);
    procedure Edit4Change(Sender: TObject);
    procedure FormActivate(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    procedure Button4Click(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
  private
    { Private declarations }
    procedure UpdateScreen;
  public
    { Public declarations }
end;

var
  Form1: TForm1;

implementation

var
  Namen : TNAW;

{$R *.DFM}

constructor TNAW.Init;
begin
  AantalNamen := 1;
  HuidigeNaam := 0;
  SetLength(Namen,AantalNamen);
end;

procedure TNAW.Nieuw;
begin
  Inc(AantalNamen);
  SetLength(Namen,AantalNamen);
end;

procedure TNAW.Volgende;
begin
  Inc(HuidigeNaam);
  If HuidigeNaam = AantalNamen Then
    Nieuw;
end;

procedure TNAW.Vorige;
begin
  If HuidigeNaam > 0 Then
    Dec(HuidigeNaam);
end;

procedure TNAW.LoadFromFile(FileName: ShortString);
var
  f : Integer;
  NamenFile: File of TNawRec;
begin
  SetLength(Namen,0);
  AssignFile(NamenFile,’vb2.rec’);
  Reset(NamenFile);
  f := 0;
  While not Eof(NamenFile) Do
    Begin
      SetLength(Namen,Succ(f));
      Read(NamenFile,Namen[f]);
      Inc(f);
    End;
  CloseFile(NamenFile);
  AantalNamen := f;
  HuidigeNaam := 0;
end;

procedure TNAW.SaveToFile(FileName: ShortString);
var
  f : Integer;
  NamenFile: File of TnawRec;
begin
  AssignFile(NamenFile,’vb2.rec’);
  Rewrite(NamenFile);
  For f := 0 To AantalNamen – 1 Do
    Write(NamenFile,Namen[f]);
  CloseFile(NamenFile);
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  Namen := TNAW.Init;
end;

procedure TForm1.UpdateScreen;
begin
  Edit1.Text := Namen.Namen[Namen.Huidigenaam].Naam;
  Edit2.Text := Namen.Namen[Namen.Huidigenaam].Adres;
  Edit3.Text := Namen.Namen[Namen.Huidigenaam].Plaats;
  Edit4.Text := IntToStr(Namen.Namen[Namen.Huidigenaam].Leeftijd);
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  Namen.Volgende;
  UpdateScreen;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  Namen.Vorige;
  UpdateScreen;
end;

procedure TForm1.Edit1Change(Sender: TObject);
begin
  Namen.Namen[Namen.Huidigenaam].Naam := Edit1.Text;
end;

procedure TForm1.Edit2Change(Sender: TObject);
begin
  Namen.Namen[Namen.Huidigenaam].Adres := Edit2.Text;
end;

procedure TForm1.Edit3Change(Sender: TObject);
begin
  Namen.Namen[Namen.Huidigenaam].Plaats := Edit3.Text;
end;

procedure TForm1.Edit4Change(Sender: TObject);
  var c : Integer;
begin
  Val(Edit4.Text,Namen.Namen[Namen.Huidigenaam].Leeftijd,c);
end;

procedure TForm1.FormActivate(Sender: TObject);
begin
  Edit1.SetFocus;
end;

procedure TForm1.Button3Click(Sender: TObject);
begin
  Namen.SaveToFile(‘vb2.rec’);
end;

procedure TForm1.Button4Click(Sender: TObject);
begin
  Namen.LoadFromFile(‘vb2.rec’);
  UpdateScreen;
end;

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  ShowMessage(‘Closing…’);
  Namen.Free;
end;

end.

Denk eraan alles weer netjes op te ruimen zoals hierboven gebeurt in de procedure FormClose.

Overerving

Eén van de grote voordelen van het werken met Classes is Overerving.
Iedere Class stamt af van een andere Class en erft daarmee meteen alle functionaliteit van de Ouder-Class.

Het enige wat dan nog moet gebeuren is het toevoegen van nieuwe functionaliteit.

Zoals reeds eerder opgemerkt, doe je niets anders wanneer je in Delphi een programma schrijft.

Het nu volgende voorbeeld is iewat gekunsteld maar is slechts ter illustratie bedoeld.

We gaan een Class TVervoerMiddel maken met wat algemene velden en functionaliteiten.

Daarna gaan we een aantal Classes hiervan afleiden.

We gaan er vanuit dat ieder vervoermiddel een aantal wielen heeft (al zijn dit er misschien nul) en dat een vervoermiddel zich op een bepaalde manier voortbeweegt.

Verder willen we bij initialisatie de waarden van de velden meegeven, en willen we methodes hebben om één en ander te kunnen uitlezen.

Hier komt de definitie:

type
  TSoort = (rijden, vliegen, varen);
  TVervoerMiddel = class
    Aantal_wielen: integer;
    Soort: TSoort;
    constructor Init(aw: integer; srt: TSoort);
    function Hoe: string;
    procedure WatEnHoe;
  end;

Nu gaan we een afgeleide Class TAuto maken. De nieuwe functionaliteit is het soort auto, coupe, sedan e.d..

  TAutoType = (sedan,coupe,stationwagon,cabriolet);
  TAuto = class(TVervoerMiddel)
  Auto_type: TAutoType;
  procedure WatEnHoe;
end;

Om aan te geven dat TAuto afstamt van TVervoerMiddel staat achter het woord Class tussen haakjes de ouder-Class, TVervoerMiddel.

Formeel zou achter TVervoerMiddel tussen haakjes TObject moeten staan, maar daar deze Class default is mag dat ook worden weggelaten.

De implementatie van de Constructor Init is als volgt:

Constructor TVervoerMiddel.Init(aw: integer; srt: TSoort);
begin
  Aantal_Wielen := aw;
  Soort := srt
end;

Dit is overigens vaak de manier om aan velden een waarde toe te kennen, dus d.m.v. een Constructor of een procedure!

Nu volgen de twee andere implementaties van TVervoerMiddel:

function TVervoerMiddel.Hoe: string;
begin
Case soort of
rijden: Hoe := ‘rijdt’;
vliegen: Hoe := ‘vliegt’;
varen: Hoe := ‘vaart’;
end;
end;

procedure TVervoerMiddel.WatEnHoe;
begin
  ShowMessage(‘Dit vervoermiddel ‘ + Hoe + ‘ en heeft ‘ + inttostr(Aantal_wielen) + ‘ wielen.’);
end;

Alleen bij TAuto hebben we meer functionaliteit en moeten we derhalve een eigen WatEnHoe-procedure maken:

procedure TAuto.WatEnHoe;
  var tp: string;
begin
  Case Auto_type of
    sedan: tp := ‘sedan.’;
    coupe: tp := ‘coupe.’;
    stationwagon: tp := ‘stationwagon.’;
    cabriolet: tp := ‘cabriolet.’
  else
    tp := ‘onbekend.’
  end;
  ShowMessage(‘Dit vervoermiddel ‘ + Hoe
+ ‘ en heeft ‘ + inttostr(Aantal_wielen) + ‘ wielen, en is van het type ‘+ tp);
end;

Met name de laatste regen (ShowMessage) lijkt iewat dubbelop, en dat is ook zo.
We zouden dit als volgt kunnen oplossen:

procedure TAuto.WatEnHoe;
  var tp: string;
begin
  Case Auto_type of
    sedan: tp := ‘sedan.’;
    coupe: tp := ‘coupe.’;
    stationwagon: tp := ‘stationwagon.’;
    cabriolet: tp := ‘cabriolet.’
  else
    tp := ‘onbekend.’
  end;
  inherited WatEnHoe;
  ShowMessage(‘Deze auto is van het type ‘ + tp);
end;

Het woord inherited zoekt naar de eerst voorgaande versie van WatEnHoe in de Class-hierarchie.
Het is natuurlijk wel noodzakelijk dat deze bestaat!

Voor de volledigheid volgt hier het complete programma:

Voorbeeld 3

unit Unit1;
interface
uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls;
type
  TSoort = (rijden, vliegen, varen);
    TVervoerMiddel = class
    Aantal_wielen: integer;
    Soort: TSoort;
    constructor Init(aw: integer; srt: TSoort);
    function Hoe: string;
    procedure WatEnHoe;
  end;
  TAutoType = (sedan,coupe,stationwagon,cabriolet);
    TAuto = class(TVervoerMiddel)
    Auto_type: TAutoType;
    procedure WatEnHoe;
  end;
  TFiets = class(TVervoerMiddel)
  end;
  TVliegtuig = class(TVervoerMiddel)
  end;
  TBoot = class (TVervoerMiddel)
  end;
TForm1 = class(TForm)
  Button1: TButton; //met caption ‘Auto’
  Button2: TButton; //met caption ‘Fiets’
  Button3: TButton; //met caption ‘Vliegtuig’
  Button4: TButton; //met caption ‘Boot’
  procedure FormCreate(Sender: TObject);
  procedure FormClose(Sender: TObject; var Action: TCloseAction);
  procedure Button1Click(Sender: TObject);
  procedure Button2Click(Sender: TObject);
  procedure Button3Click(Sender: TObject);
  procedure Button4Click(Sender: TObject);
private
  { Private declarations }
public
  { Public declarations }
end;

var
  Form1: TForm1;

implementation

{$R *.DFM}
var
  Auto: TAuto;
  Fiets: TFiets;
  Vliegtuig: TVliegtuig;
  Boot: TBoot;

Constructor TVervoerMiddel.Init(aw: integer; srt: TSoort);
begin
  Aantal_Wielen := aw;
  Soort := srt
end;

function TVervoerMiddel.Hoe: string;
begin
  Case soort of
    rijden: Hoe := ‘rijdt’;
    vliegen: Hoe := ‘vliegt’;
    varen: Hoe := ‘vaart’;
  end;
end;

procedure TVervoerMiddel.WatEnHoe;
begin
  ShowMessage(‘Dit vervoermiddel ‘ + Hoe + ‘ en heeft ‘ + inttostr(Aantal_wielen) + ‘ wielen.’);
end;

procedure TAuto.WatEnHoe;
  var tp: string;
begin
  Case Auto_type of
    sedan: tp := ‘sedan.’;
    coupe: tp := ‘coupe.’;
    stationwagon: tp := ‘stationwagon.’;
    cabriolet: tp := ‘cabriolet.’
  else
    tp := ‘onbekend.’
  end;
  inherited WatEnHoe;
  ShowMessage(‘Deze auto is van het type ‘ + tp);
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  Auto := TAuto.Init(4,rijden);
  Auto.Auto_type := coupe;
  Fiets := TFiets.Init(2,rijden);
  Vliegtuig := TVliegtuig.Init(16,vliegen);
  Boot := TBoot.Init(0,varen);
end;

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  Auto.Free;
  Fiets.Free;
  Vliegtuig.Free;
  Boot.Free;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
 Auto.WatEnHoe;
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
 Fiets.WatEnHoe;
end;

procedure TForm1.Button3Click(Sender: TObject);
begin
 Vliegtuig.WatEnHoe;
end;

procedure TForm1.Button4Click(Sender: TObject);
begin
 Boot.WatEnHoe;
end;

end.

Hoewel Delphi met het werken met Classes/objecten het hele verhaal pointer-impliciet heeft gemaakt, moet je wel goed in de gaten houden dat een Object-variabele, zoals bv Auto hierboven, niets anders dan een pointer-variabele is!
De vraag is dan ook gerechtvaardigd of we geen gebruik kunnen maken van één Object-variabele VervoerMiddel, die we naar believen kunnen gebruiken voor een auto, fiets, vliegtuig of boot.
Het antwoord op die vraag luidt: JA, maar niet zomaar naar believen.

Vandaar…

Polymorfisme

Polymorfisme houdt in dat een methode van het ene object dezelfde naam heeft als een methode van een ander object, met dien verstande dat beide methodes min of meer dezelfde werking hebben.
Denk hierbij bijvoorbeeld aan de mehode Hide die zowel bij een label als een button voorkomt en in beide gevallen het object onzichtbaar maakt. En dit terwijl een label en een button toch echt twee verschillende objecten zijn.

Een ander voorbeeld hebben we gezien in de vorige paragraaf met de methode WatEnHoe die we voor het type TAuto hebben aangepast.

Belangrijk hierbij op te merken is dat deze aanpassing een zogenaamde statische vorm heeft. Tijdens het compileren is al bekend hoe de methode WatEnHoe voor de verschillende objecten eruit ziet.

Het kan echter ook dynamisch ofwel tijdens run-time uitgemaakt worden.
Vergeet de laatste opmerking van de vorige paragraaf niet: een object-variabele is niets anders dan een pointer-variabele, ofwel een variabele die naar een geheugenplaats wijst.

En zolang er sprake is van overerfde objecten kunnen ze allemaal aan 1 object-variabele worden toegekend.

Overigens spreken we in het eerste geval van “early-binding”, dus een koppeling tijdens compileren, en in het tweede geval van “late-binding”, een koppeling tijdens de uitvoer.

Met het komende voorbeeld begint het dus ergens op te lijken.

In het volgende voorbeeld maken we een Class TDier waar we de Class THond en TKat vanaf zullen leiden.

De Class TDier zou er als volgt uit kunnen zien:

type
  TDier = class
  procedure WieBenIk;
  procedure WatZegIk;virtual;
end;

Met de methode WieBenIk moet duidelijk worden gemaakt om welk dier het gaat, en met de methode WatZegIk moet duidelijk worden gemaakt welk geluid het dier maakt.

Verder is de methode WieBenIk statisch en is de methode WatZegIk dynamisch.

Om aan te geven dat een methode dynamisch is, dus dat pas tijdens run-time bepaalt moet worden welke implementatie moet worden uitgevoerd, voegen we bij de ouder-klasse (of bron-klasse of root-class) het woordje virtual toe.

Bij iedere afstammeling voegen we bij de dynamische methode het woordje override toe.

Hier volgen de definitie’s van THond en TKat:

THond = class(TDier)
  procedure WieBenIk;
  procedure WatZegIk;override;
end;
TKat = class(TDier)
  procedure WieBenIk;
  procedure WatZegIk;
end;

Merk op dat bij THond de methode WatZegIk dynamisch is, maar bij TKat niet!

Dan volgen nu de implementatie’s van de verschillende classes:

procedure TDier.WieBenIk;
begin
ShowMessage(‘Ik ben een dier’);
end;

procedure TDier.WatZegIk;
begin
  ShowMessage(‘Ik maak geluid’);
end;

procedure THond.WieBenIk;
begin
  ShowMessage(‘Ik ben een hond’);
end;

procedure THond.WatZegIk;
begin
  ShowMessage(‘Ik blaf’);
end;

procedure TKat.WieBenIk;
begin
  ShowMessage(‘Ik ben een kat’);
end;

procedure TKat.WatZegIk;
begin
  ShowMessage(‘Ik mauw’);
end;

Deze implementatie’s roepen hopelijk geen vragen op, behalve dan dat de woordjes virtual en override alleen maar in de class-definitie voorkomen.

Maak nu eens een Delphi-project met een button waarin bovenstaande verwerkt is met een button-Click-Event zoals in het nu volgende uitgewerkte voorbeeld:

Voorbeeld 4

unit Unit1;
interface
uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls;type
TDier = class
  procedure WieBenIk;
  procedure WatZegIk;virtual;
end;
THond = class(TDier)
  procedure WieBenIk;
  procedure WatZegIk;override;
end;
TKat = class(TDier)
  procedure WieBenIk;
  procedure WatZegIk;
end;
TForm1 = class(TForm)
  Button1: TButton;
  procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
end;var
  Form1: TForm1;implementation{$R *.DFM}procedure TDier.WieBenIk;
begin
  ShowMessage(‘Ik ben een dier’);
end;procedure TDier.WatZegIk;
begin
  ShowMessage(‘Ik maak geluid’);
end;procedure THond.WieBenIk;
begin
  ShowMessage(‘Ik ben een hond’);
end;procedure THond.WatZegIk;
begin
  ShowMessage(‘Ik blaf’);
end;procedure TKat.WieBenIk;
begin
  ShowMessage(‘Ik ben een kat’);
end;procedure TKat.WatZegIk;
begin
  ShowMessage(‘Ik mauw’);
end;procedure TForm1.Button1Click(Sender: TObject);
var
  Dier : TDier;
  Kat : TKat;
begin
  Dier := TDier.Create;
  Dier.WieBenIk;
  Dier.WatZegIk;
  Dier.Free;  Dier := THond.Create;
  Dier.WieBenIk;
  Dier.WatZegIk;
  Dier.Free;  Kat := TKat.Create;
  Kat.WieBenIk;
  Kat.WatZegIk;
  Kat.Free;  Dier := TKat.Create;
  Dier.WieBenIk;
  Dier.WatZegIk;
  Dier.Free;
end;
end.

– Probeer voordat je dit programma runt eerst de uitkomst te voorspellen

Hoe werkt dit nu precies?

De methode WieBenIk is in alle klasses statisch, d.i. iedere klasse heeft zijn eigen WieBenIk. De methode WatZegIk is bij TDier dynamisch gemaakt en wordt op dezelfde wijze overgenomen door THond. Bij TKat echter wordt de methode WatZegIk weer statisch. Afstammelingen van TKat kunnen geen dynamische WatZegIk-methode hebben!

De variabele Dier in het voorbeeld wordt gedeclareerd als van het type TDier. Dat betekend dat bij iedere aanroep van de methode WieBenIk als antwoord ‘Ik ben een dier’ komt, omdat WieBenIk statisch is gedeclareerd. Dat laatste betekent dat de binding tijdens het compileer-proces plaats vindt (early-binding). De methode WatZegIk is echter dynamisch gedeclareerd wat betekent dat pas tijdens runtime bepaald wordt welke WatZegIk-methode moet worden gebruikt (late binding). Wanneer Dier een instantie van THond is zal WatZegIk als resultaat ‘Ik blaf’ geven. Bij TKat gaat dat verhaal niet meer op, omdat WatZegIk daar weer als statisch is gedeclareerd.

Encapsulation

Het is een goed gebruik binnen het werken met Classes om op een correcte wijze om te gaan met de scope of zichtbaarheid van velden en methodes.
En hier onstaat, naar mijn mening, een probleem in Delphi, waarover zometeen meer.

Een Class kent 4 soorten van scope:

  • private
  • protected
  • public (default)
  • published
private

Alle velden en methodes die onder private worden gedeclareerd zijn alleen zichtbaar in de implementatie van de Class zelf.

Het is een goed gebruik alle interne velden en methodes private te declareren.

protected

Alle velden en methodes die onder protected worden gedeclareerd zijn alleen zichtbaar in de implementatie van de Class en zijn afgeleiden.

public

Alle velden en methodes die onder public worden gedeclareerd zijn altijd en overal zichtbaar.

Wanneer geen gebruik wordt gemaakt van één van de directives wordt dit als public beschouwd.

published

Het gebruik van published heeft te maken met componenten en properties en wordt hier niet verder behandeld.

Verstoring scope in Delphi

Het probleem van bovenstaande is dat wanneer je e.e.a. in de praktijk wilt testen en je beperkt je daarbij tot één en dezelfde Unit, het hele scope-verhaal de mist in gaat, omdat de scope-regels van de unit preveleren boven die van de Classes.
Naar mijn mening is dit een ernstige tekortkoming in Delphi.

Dynamische componenten

Het volgende voorbeeld laat zien hoe je tijdens run-time componenten kunt aanmaken en weer weggooien. Dit kan erg handig zijn, denk maar bv. aan een report-generator of iets dergelijks.

Bij het gebruik van dynamische componenten zijn een aantal zaken waarmee rekening moet worden gehouden:

  1. Bij Create aangeven wat de container is
  2. Expliciet de Parent-property vullen
  3. Eventueel andere properties vullen
  4. Eventueel voor unieke naamgeving zorgen
  5. Zelf Procedures aan Events koppelen

Bij dynamische componenten heb je natuurlijk geen object-inspector tot je beschikking en zul je bovenstaande zaken dus zelf moeten regelen.

– Start een nieuw project in Delphi en open het code-venster

– Voeg bij de Uses-clausule toe:
StdCtrls

– Voeg via de Object-inspector de event-handlers FormCreate en FormClose toe

– Voeg in het code-venster onder de procedure FormClose de volgende regel toe:
procedure KnopKlik(Sender: TObject);

– Voeg in het code-venster onder de directive private de volgende regel toe:
Knop: TButton;

– Voeg in het code-venster in de event-handler FormCreate de volgende regels toe:
Knop := TButton.Create(Self);
with Knop do
  begin
    Parent := Self;
    Caption := ‘Klik me’;
    OnClick := KnopKlik;
    Top := 50;
    Left := 150;
    Hint := ‘This is what we call a button [‘+IntToStr(SizeOf(Knop))+’]’;
    ShowHint := True;
  end;

– Voeg in het code-venster in de event-handler FormClose de volgende regel toe:
Knop.Free;

– Voeg in het code-venster de event-handler KnopKlik toe:
Procedure TForm1.KnopKlik(Sender: TObject);
begin
  showmessage(‘Knop geklikt’);
end;

– Run het project

Je hebt nu, tijdens het runnen, een form met een knop erop. Wanneer je op de knop klikt wordt keurig de event-handler KnopKlik uitgevoerd.

Omdat je in design-time niets op het form hebt gezet weet Delhi niet dat je tijdens run-time gebruik maakt van controls, vandaar dat je zelf de unit StdCtrls moet toevoegen aan de Uses-clausule.

Het woordje Self refereert aan het ouder-object ofwel de container waar de control deel van uit gaat maken. En ondanks het feit dat je dat al aangeeft bij de creatie van de control moet dit ook nog expliciet worden toegekend aan de propertie (eigenschap) Parent.

We gaan bovenstaand project uitbreiden met een listbox waarin items zitten waarop gedubbelklikt kan worden en een tiental labeltjes.

Om die labeltjes te maken kun je natuurlijk gebruik maken van 10 object-variabelen, maar het kan gelukkig ook met 1. Het enige belangrijke is dan wel dat je kunt/moet bijhouden waar zo’n label zich dan bevindt, bijvoorbeeld voor het vrijmaken van het label.
Daarvoor kun je gebruik maken van de TList-class, die precies bijhoudt waar een control zich in het geheugen bevindt. Via een object van TList kun je dus altijd bij het gewenste control komen.

De procedure KnopKlik is in het volgende voorbeeld veranderd.

Voorbeeld 5

unit Unit1;
interface
uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls;
type
  TForm1 = class(TForm)
    procedure FormCreate(Sender: TObject);
    procedure KnopKlik(Sender: TObject);
    procedure ListBoxDubbelKlik(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
  private
    { Private declarations }
    Knop : TButton;
    LabelLijst : TList;
    Labeltje : TLabel;
    ListBox: TListBox;
  public
    { Public declarations }
  end;var
  Form1: TForm1;implementation{$R *.DFM}procedure TForm1.FormCreate(Sender: TObject);
  var a : Byte;
begin
  LabelLijst := TList.Create;
  ListBox := TListBox.Create(Self);
  with ListBox do
    begin
      Parent := Self;
      OnDblClick := ListBoxDubbelKlik;
      for a := 0 to 9 do
        Items.Add(‘Item ‘+IntToStr(a));
      Top := 100;
      Left := 150;
      Hint := ‘This is a listbox: DubbleClick on Items [‘+IntToStr(SizeOf(ListBox))+’]’;
      ShowHint := True;
    end;
  Knop := TButton.Create(Self);
  with Knop do
    begin
      Parent := Self;
      Caption := ‘Klik me’;
      OnClick := KnopKlik;
      Top := 50;
      Left := 150;
      Hint := ‘This is what we call a button [‘+IntToStr(SizeOf(Knop))+’]’;
      ShowHint := True;
    end;
  for a := 1 To 10 do
    begin
      Labeltje := TLabel.Create(Self);
      with Labeltje do
        begin
          Parent := Self;
          Caption := ‘Dynamisch aangemaakt’;
          AutoSize := True;
          Top := a*30;
          Hint := ‘This is dynamic label ‘+IntToStr(a)+’ [‘+IntToStr(SizeOf(Labeltje))+’]’;
          ShowHint := True;
          LabelLijst.Add(Labeltje);
        end;
    end;
  Labeltje.Color := clRed;
  Labeltje := LabelLijst.Items[4];
  Labeltje.Color := clBlue;
end;procedure TForm1.KnopKlik(Sender: TObject);
  var a : Byte;
begin
  If LabelLijst.Count > 0 then
    begin
      ShowMessage(‘Labeltjes weghalen’);
      For a := 0 To LabelLijst.Count – 1 do
        begin
          Labeltje := LabelLijst.Items[a];
          Labeltje.Free;
          LabelLijst.Items[a] := nil;
        end;
      LabelLijst.Free;
    end;
end;procedure TForm1.ListBoxDubbelKlik(Sender: TObject);
begin
  ShowMessage(‘U koos ‘+ListBox.Items[ListBox.ItemIndex]);
end;procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
var a: Byte;
begin
  If LabelLijst.Count > 0 then
    begin
      For a := 0 To LabelLijst.Count – 1 do
        begin
          Labeltje := LabelLijst.Items[a];
          Labeltje.Free;
          LabelLijst.Items[a] := nil;
        end;
      LabelLijst.Free;
    end;
  Knop.Free;
  ListBox.Free;
end;
end.

Probeer bovenstaand programma te doorgronden. Dit verschaft je nu een flexibele techniek om je forms te kunnen bouwen, bijvoorbeeld je eigen report-generator gekoppel aan een of andere database o.i.d..

Tot slot

Veel van de voorbeelden komen uit een van de boeken van Marco Cantù, de Delphi-Goeroe!

Ik hoop dat dit, misschien wat taaie, hoofdstuk je op weg heeft geholpen naar een nieuwe manier van programmeren en meer kennis van wat er zoal in de achtergrond van Delphi gebeurt.