Vaak is het de beeldvorming die voor het grootste probleem zorgen; OOP (Object
Oriented Programming) vraagt om een iets andere kijk en denkwijze dan traditioneel
of proceduriëel programeren.
Je moet een klasse (of object) zien als een losse blok logica (een "Black Box") die
een bepaalde functionaliteit bevat en die je van buitenaf kunt aansturen zonder
dat je hoeft te weten hoe het er van binnen aan toe gaat.
Klassen geven je de mogelijkeid om functionele code te scheiden en om meerdere
instanties van een klasse te hebben in je programma, zonder dat het een veelvoud
aan geheugenruimte kost voor iedere instantie en om code overzichtelijk te houden.
Bijkomend voordeel van een klasse is ook dat alle eventuele properties, netjes
gegroepeerd zijn en duidelijk onderdeel uitmaken van de klasse en dat deze
properties "Dom" kunnen zijn (gewone variabelen) of "Intelligent" (dat ze eerst
nog door de klasse een controle of bewerking kunnen ondergaan, alvorens ze
daadwerkelijk in de klasse worden opgeslagen/gebruikt) mbv een "Getter" en/of "Setter".
Eventuele "Domme" properties kunnen in een later stadium "Intelligent" gemaakt
worden, door ze van een "Getter" en/of "Setter" te voorzien, zonder dat je je
hele programma overhoop hoeft te halen; het aanpassen van de klasse zelf
is voldoende.
Neem het volgende voorbeeld:
Delphi Code:
Procedure Berekenen1;
Var
A1, // Variabelen voor 1e waarden
A2,
A3,
A4,
B1, // Variabelen voor 2e waarden
B2,
B3,
B4 : Single;
O : Single; // Optellen
A : Single; // Aftrekken
V : Single; // Vermenigvuldigen
D : Single; // Delen
Begin
A1 := 10;
B1 := 20;
A2 := 30;
B2 := 40;
A3 := 50;
B3 := 60;
A4 := 70;
B4 := 80;
O := B1 + A1;
A := B1 - A1;
V := B1 * A1;
D := B1 / A1;
ShowMessage('A = ' + FloatToStr(A1) + ', B = ' + FloatToStr(B1) + #13#10 +
'Optellen = ' + FloatToStr(O) + #13#10 +
'Aftrekken = ' + FloatToStr(A) + #13#10 +
'Vermenigvuldigen = ' + FloatToStr(V) + #13#10 +
'Delen = ' + FloatToStr(D));
O := B2 + A2;
A := B2 - A2;
V := B2 * A2;
D := B2 / A2;
ShowMessage('A = ' + FloatToStr(A2) + ', B = ' + FloatToStr(B2) + #13#10 +
'Optellen = ' + FloatToStr(O) + #13#10 +
'Aftrekken = ' + FloatToStr(A) + #13#10 +
'Vermenigvuldigen = ' + FloatToStr(V) + #13#10 +
'Delen = ' + FloatToStr(D));
O := B3 + A3;
A := B3 - A3;
V := B3 * A3;
D := B3 / A3;
ShowMessage('A = ' + FloatToStr(A3) + ', B = ' + FloatToStr(B3) + #13#10 +
'Optellen = ' + FloatToStr(O) + #13#10 +
'Aftrekken = ' + FloatToStr(A) + #13#10 +
'Vermenigvuldigen = ' + FloatToStr(V) + #13#10 +
'Delen = ' + FloatToStr(D));
O := B4 + A4;
A := B4 - A4;
V := B4 * A4;
D := B4 / A4;
ShowMessage('A = ' + FloatToStr(A4) + ', B = ' + FloatToStr(B4) + #13#10 +
'Optellen = ' + FloatToStr(O) + #13#10 +
'Aftrekken = ' + FloatToStr(A) + #13#10 +
'Vermenigvuldigen = ' + FloatToStr(V) + #13#10 +
'Delen = ' + FloatToStr(D));
End;
Bovenstaande procedure neemt een 4-tal getallenparen en berekent vervolgens
het opgetelde, afgetrokken, vermenigvuldigde en gedeelde resultaat en laat deze zien.
Verder weinig noemenswaardige code, maar 1 ding valt al direct op: wat zijn die
variabelen? Waar dienen ze voor?
Met zoiets eenvoudigs als dit is dat makkelijk bij te houden, maar zodra een programma
groeit wordt het steeds onoverzichtelijker en zie je door de bomen op een gegeven
moment het bos niet meer.
Uiteraard kun je de variabelen betere/duidelijkere namen geven, maar je kunt ze
ook gewoon groeperen, zodat duidelijk te zien is welke variabelen er bij elkaar horen.
Dit doen we met klassen!
Een klasse moet je eerst declareren als een Type, zodat de Delphi-compiler hem
correct kan klassificeren en registreren in z'n systeem.
Dit declareren doen we in het "Interface"-gedeelte van onze code:
Delphi Code:
Unit BerekenUnit; // Naam van deze unit
Interface // Eerst gaan we het uitleggen aan buitenstaande code hoe deze
// unit (en zijn types) aangesproken dient te worden
Uses
SysUtils; // Hier zetten we de units neer die we nodig hebben voor onze eigen
// code (bijvoorbeeld de functie FloatToStr die in SysUtils zit)
type
TMijnCalculator = Class(TObject)
Private
fValueA : Single;
fValueB : Single;
Protected
Function GetOptellen : Single;
Function GetAftrekken : Single;
Function GetVermenigvuldigen : Single;
Function GetDelen : Single;
Public
Constructor Create;
Property ValueA : Single Read fValueA Write fValueA;
Property ValueB : Single Read fValueB Write fValueB;
Property Optellen : Single Read GetOptellen;
Property Aftrekken : Single Read GetAftrekken;
Property Vermenigvuldigen : Single Read GetVermenigvuldigen;
Property Delen : Single Read GetDelen;
End;
Allereerst geven we de klasse een naam, dit mag iedere willekeurge naam zijn,
zolang het maar duidelijk aangeeft wat die klasse doet.
Volgens de geldende Delphi naamgeving moet een type-declaratie met een hoofdletter
"T" beginnen (zonder T ervoor doet de compiler ook niet moeilijk, maar we moeten
ons wel een beetje aan de geaccepteerde regels houden natuurlijk).
Zorg er overigens wel voor dat je niet een naam bedenkt die al bestaat, dit om
verwarring te voorkomen met een reeds bestaande klasse!
Oke, dus we noemen onze klasse "TMijnCalculator".
Omdat onze klasse geen functionaliteit hoeft te bevatten die al in een andere klasse
beschikbaar is kunnen we hem gewoon afleiden van de laagste basis-klasse die
er bestaat: TObject.
TObject is een klasse die verder weinig om het lijf heeft en maar een paar basis-methods
(procedures en functies) heeft om het zaakje te laten werken.
Het aangeven dat onze TMijnCalculator een afgeleide is van TObject doe je middels:
Delphi Code:
TMijnCalculator = Class(TObject)
Let op: er staat geen punt-komma (; ) achter! Dit omdat de type-declaratie nog niet klaar is.
Vervolgens gaan we een aantal variabelen toevoegen aan de klasse die niet beschikbaar
zijn voor code buiten de klasse; deze zetten we dan ook in een "Private"-blok.
Deze variabelen hebben we nodig voor de werking van onze klasse.
Daarna declareren we een aantal functies (of procedures) die nodig zijn om de klasse
zijn werk te laten doen, maar we willen niet dat code buiten onze klasse deze functies
kunnen uitvoeren, dus plaatsen we ze in een "Protected"-blok.
Als laatste zetten we dingen neer die code van buiten de klasse aan mogen spreken,
zoals bepaalde openbaar toegankelijke functies, procedures en "Properties".
Omdat ze voor code vanaf buiten toegankelijk moeten zijn, zetten we ze in een blok
genaamd "Public".
Properties kunnen eenvoudige (domme) variabelen zijn of in- en uit-gangen van
waarden via een getter en/of setter.
Onder properties worden ook eventuele events verstaan (welke eigenlijk gewoon
pointers zijn naar een procedure van een bepaald type).
Om het einde aan te geven van onze TMijnCalculator-klasse complementeren we
de declaratie met "End;"
In feite is de klasse-definitie voldoende voor een programmeur om met deze klasse
te gaan werken; hij hoeft tenslotte niet te weten wat er in de klasse-code staat,
zolang de klasse maar doet wat hij moet doen.
Helaas moet de compiler wel eerst de werkende code maken, dus moeten we deze
code ook implementeren...
Dit doen we in het "Implementation"-gedeelte van onze code:
Delphi Code:
Implementation
Function TMijnCalculator.GetOptellen : Single;
Begin
Result := fValueB + fValueA;
End;
Function TMijnCalculator.GetAftrekken : Single;
Begin
Result := fValueB - fValueA;
End;
Function TMijnCalculator.GetVermenigVuldigen : Single;
Begin
Result := fValueB * fValueA;
End;
Function TMijnCalculator.GetDelen : Single;
Begin
If (fValueA <> 0) Then
Result := fValueB / fValueA
Else
Result := 0;
End;
Constructor TMijnCalculator.Create;
Begin
Inherited Create;
fValueA := 0;
fValueB := 0;
End;
Met deze code maken we de klasse af en kan de compiler het resultaat naar
machinetaal omzetten (compileren).
Met de Constructor TMijnCalculator.Create kunnen we er voor zorgen dat bepaalde
variabelen op een standaard waarde worden gezet, of zodat andere klassen die
door jouw klasse worden gebruikt, ge-create kunnen worden.
Een tegenhanger van Constructor bestaat ook: Destructor Tnaamvandeklasse.Destroy
Als een klasse wordt vernietigd, wordt deze destructor aangeroepen zodat je
bepaalde acties kunt uitvoeren zoals het destroyen van gebruikte klassen.
Overigens zien we bij de TMijnCalculator.GetDelen-functie nog een voordeel bij
het maken van klassen: alvorens het resultaat gegeven wordt kun je controleren
of de berekening wel mogelijk is (delen door 0 kan namelijk niet).
Bij proceduriele code zou je hier midden in de code zelf op moeten controleren en
handelen, terwijl in een klasse altijd een geldig antwoord komt (of 0).
Om onze unit zelf af te sluiten, moeten we nog het einde aangeven met:
Vervolgens kunnen we de klasse gaan gebruiken met bijvoorbeeld deze code:
Delphi Code:
Uses
BerekenUnit;
Procedure Berekenen2;
Var
C1,
C2,
C3,
C4 : TMijnCalculator;
begin
C1 := TMijnCalculator.Create;
Try
C1.ValueA := 10;
C1.ValueB := 20;
C2 := TMijnCalculator.Create;
Try
C2.ValueA := 30;
C2.ValueB := 40;
C3 := TMijnCalculator.Create;
Try
C3.ValueA := 50;
C3.ValueB := 60;
C4 := TMijnCalculator.Create;
Try
C4.ValueA := 70;
C4.ValueB := 80;
ShowMessage('A = ' + FloatToStr(C1.ValueA) + ', B = ' + FloatToStr(C1.ValueB) + #13#10 +
'Optellen = ' + FloatToStr(C1.Optellen) + #13#10 +
'Aftrekken = ' + FloatToStr(C1.Aftrekken) + #13#10 +
'Vermenigvuldigen = ' + FloatToStr(C1.Vermenigvuldigen) + #13#10 +
'Delen = ' + FloatToStr(C1.Delen));
ShowMessage('A = ' + FloatToStr(C2.ValueA) + ', B = ' + FloatToStr(C2.ValueB) + #13#10 +
'Optellen = ' + FloatToStr(C2.Optellen) + #13#10 +
'Aftrekken = ' + FloatToStr(C2.Aftrekken) + #13#10 +
'Vermenigvuldigen = ' + FloatToStr(C2.Vermenigvuldigen) + #13#10 +
'Delen = ' + FloatToStr(C2.Delen));
ShowMessage('A = ' + FloatToStr(C3.ValueA) + ', B = ' + FloatToStr(C3.ValueB) + #13#10 +
'Optellen = ' + FloatToStr(C3.Optellen) + #13#10 +
'Aftrekken = ' + FloatToStr(C3.Aftrekken) + #13#10 +
'Vermenigvuldigen = ' + FloatToStr(C3.Vermenigvuldigen) + #13#10 +
'Delen = ' + FloatToStr(C3.Delen));
ShowMessage('A = ' + FloatToStr(C4.ValueA) + ', B = ' + FloatToStr(C4.ValueB) + #13#10 +
'Optellen = ' + FloatToStr(C4.Optellen) + #13#10 +
'Aftrekken = ' + FloatToStr(C4.Aftrekken) + #13#10 +
'Vermenigvuldigen = ' + FloatToStr(C4.Vermenigvuldigen) + #13#10 +
'Delen = ' + FloatToStr(C4.Delen));
Finally
C4.Free;
End;
Finally
C3.Free;
End;
Finally
C2.Free;
End;
Finally
C1.Free;
End;
end;
Om een klasse te kunnen gebruiken moeten we, naast een variabele waarmee
we hem kunnen refereren, eerst creëren; als we er mee klaar zijn moeten we
hem ook weer vrijgeven.
Bij het Createn wordt er geheugen vrijgemaakt voor een nieuwe instantie van
onze klasse en deze moet weer worden vrijgegeven als we ermee klaar zijn anders
krijg je geheugenlekken (en het is gewoon slordig).
Vandaar dat je gecreëerde klassen in een Try...Finally...End-constructie moet
zetten, om ervoor te zorgen dat je klasse altijd wordt opgeruimd, ook als er tussentijds
iets fout mocht gaan (een exceptie bijvoorbeeld).
Code tussen Finally...End wordt altijd uitgevoerd, ook als er een exceptie optreed.
We pakken er even 1 gecreëerde klasse uit (de anderen zijn zo goed als identiek):
Delphi Code:
Begin
C1 := TMijnCalculator.Create;
Try
C1.ValueA := 10;
C1.ValueB := 20;
ShowMessage('A = ' + FloatToStr(C1.ValueA) + ', B = ' + FloatToStr(C1.ValueB) + #13#10 +
'Optellen = ' + FloatToStr(C1.Optellen) + #13#10 +
'Aftrekken = ' + FloatToStr(C1.Aftrekken) + #13#10 +
'Vermenigvuldigen = ' + FloatToStr(C1.Vermenigvuldigen) + #13#10 +
'Delen = ' + FloatToStr(C1.Delen));
Finally
C1.Free;
End;
End;
Regel 2 in onze code zorgt ervoor dat instantie C1 van onze klasse gecreërd wordt;
in de klasse zelf wordt nu eerst intern fValueA en fValueB op 0 gezet.
Daarna lopen we het Try-blok in waar we vanuit onze code aan ValueA en ValueB
een waarde wordt toegekend (respectivelijk 10 en 20).
In de klasse verwijzen deze properties direct naar de interne variabelen fValueA en
fValueB, dus hier gebeurt verder weinig spannends...
In de ShowMessage op regel 6 wordt vervolgens eerst een complete string samengesteld
voor de ShowMessage zelf uit alle losse strings en functies die er als parameter inzitten.
Even "Optellen" als voorbeeld nemende (voor de andere properties is de werking
gelijk): omdat we een string willen hebben (voor de ShowMessage) en de Optellen-
property een single (= float) als uitgang heeft moeten we gebruik maken van
de functie FloatToStr() welke in SysUtils zit.
Deze FloatToStr vraagt aan onze klasse-instantie (C1.Optellen) wat de waarde is.
Onze klasse kijkt in zijn lijst en ziet dat het geen domme property is, maar gebruik
maar van een Getter, dus roept hij intern de functie "TMijnCalculator.GetOptellen" aan
welke de som van fValueA en fValueB teruggeeft als resultaat.
Zo worden alle uitkomsten opgevraagd uit de klasse, omgezet naar strings en als
parameter aan ShowMessage gevoerd, welke vervolgens het resultaat laat zien.
Uiteraard is bovenstaande een beetje onzinnig en totale overkill, maar het dient
alleen maar ter illustratie.
Maar... kunnen we misschien onze klasse toch nog uitbreiden met handige functionaliteit?
Jazeker! Kijk maar eens naar wat onze eigen code moet doen in de ShowMessage
om er een geldige string uit te halen...
We kunnen heel eenvoudig extra functies of properties toevoegen aan onze klasse
die in plaats van de standaard Single-waarde ook direct een string-versie ter
beschikking stelt:
Delphi Code:
// In onze TMijnCalculator-klasse
Function GetOptellenAsString : String;
Function GetAftrekkenAsString : String;
Function GetVermenigvuldigenAsString : String;
Function GetDelenAsString : String;
//
Implementation
Function TMijnCalculator.GetOptellenAsString : String;
Begin
Result := FloatToStr(GetOptellen);
End;
Function TMijnCalculator.GetAftrekkenAsString : String;
Begin
Result := FloatToStr(GetAftrekken);
End;
Function TMijnCalculator.GetVermenigvuldigenAsString : String;
Begin
Result := FloatToStr(GetVermenigvuldigen);
End;
Function TMijnCalculator.GetDelenAsString : String;
Begin
Result := FloatToStr(GetDelen);
End;
Deze extra functies kunnen we gewoon toevoegen aan het Public-gedeelte van
onze klasse, zodat ze direct publiekelijk toegankelijk zijn vanaf buiten, maar we kunnen
ook de extra stap zetten door ze te implementeren als Getters van properties
(en de functies zelf dan in het Private-gedeelte te plaatsen).
De klasse komt er dan zo uit te zien:
Delphi Code:
type
TMijnCalculator = Class(TObject)
Private
fValueA : Single;
fValueB : Single;
Protected
Function GetOptellen : Single;
Function GetOptellenAsString : String;
Function GetAftrekken : Single;
Function GetAftrekkenAsString : String;
Function GetVermenigvuldigen : Single;
Function GetVermenigvuldigenAsString : String;
Function GetDelen : Single;
Function GetDelenAsString : String;
Public
Constructor Create;
Property ValueA : Single Read fValueA Write fValueA;
Property ValueB : Single Read fValueB Write fValueB;
Property Optellen : Single Read GetOptellen;
Property OptellenAsString : String Read GetOptellenAsString;
Property Aftrekken : Single Read GetAftrekken;
Property AftrekkenAsString : String Read GetAftrekkenAsString;
Property Vermenigvuldigen : Single Read GetVermenigvuldigen;
Property VermenigvuldigenAsString : String Read GetVermenigvuldigenAsString;
Property Delen : Single Read GetDelen;
Property DelenAsString : String Read GetDelenAsString;
End;
Met deze versie van onze klasse kunnen we direct de berekende waarde als
string opvragen zoals hieronder:
Delphi Code:
//
ShowMessage('A = ' + FloatToStr(C1.ValueA) + ', B = ' + FloatToStr(C1.ValueB) + #13#10 +
'Optellen = ' + C1.OptellenAsString + #13#10 +
'Aftrekken = ' + C1.AftrekkenAsString + #13#10 +
'Vermenigvuldigen = ' + C1.VermenigvuldigenAsString + #13#10 +
'Delen = ' + C1.DelenAsString);
//
Dan nog een ding over over-erving (inheritence)...
Zoals verteld hebben we onze TMijnCalculator-klasse overgeëfd van TObject en TObject heeft
van zichzelf ook al een aantal methods in zich zitten voor de basis-functionaliteit.
Probeer voor de grap eens deze code:
Delphi Code:
Procedure Test;
Var
C : TMijnCalculator;
begin
C := TMijnCalculator.Create;
Try
ShowMessage(C.ClassName);
Finally
Free;
End;
End;
Als je deze procedure uitvoert, krijg je een message te zien die zegt: "TMijnCalculator"
Maar we hebben helemaal niet de property of functie TMijnCalculator.ClassName in onze
klasse gedeclareert!
Dat klopt... deze is onderdeel van (en wordt geïmplementeert in) TObject en wordt
in die klasse (de voorouder van onze klasse dus) uitgevoerd, maar wij hebben daar
wel de beschikking over in onze overgeërfde klassen)!
Ik hoop dat mijn uitleg enigszins te begrijpen (en compleet genoeg) is om te gaan
experimenteren met eigen klassen.
Nog een ding wil ik wel even kwijt: Klassen zijn niet de heilige graal en de oplossing
tot alle voorkomende problemen; soms is het beter/handige/korter om gewoon
procedurieel te werken, dan om er een complete klasse van te maken.
Overweeg dus goed of het inpaken in een klasse wel verantwoord kan worden
Succes!
Peter.
Bookmarks