Object Georiënteerd Delegeren... |
Tot zover zou het zonder problemen moeten gaan. En Delphi biedt dan ook ingebouwde ondersteuning voor een OO-aanpak, bijvoorbeeld in de vorm van de VCL (Visual Class Library) componenten met hun properties, events en methods. C++(Builder) liefhebbers herinneren me er echter regelmatig aan dat Delphi's Object Pascal iets mist dat wel in C++ zit, namelijk multiple inheritance. In Delphi is het slechts mogelijk om een nieuwe klasse van ten hoogste één voorvader af te leiden, en niet van meer dan één voorvader. In C++ kan dit wel, en kunnen we dus bijvoorbeeld van een TEuro en een TEdit control een TEuroEdit maken door van beide components een nieuwe af te leiden. In Object Pascal kan dit dus niet. En dat is aan één kant jammer, maar aan de andere kant toch wel fijn, want het resultaat van multiple inheritance (zeker als het te pas en te onpas wordt gebruikt) is vaak code die moeilijker te onderhouden en debuggen is (zeker als later één van beide voorvader klassen wordt aangepast, en dit consequenties heeft voor de afgeleide TEuroEdit component).
Een alternatief voor multiple inheritance waar ik tevens zelf een voorstander van ben is delegation. Met behulp van delegation plaatsen we één of meerdere (sub-)komponenten binnen een nieuw komponent. Door de properties, methods en events van de subkomponenten vervolgens "door te sluizen" (te delegeren) van het nieuwe komponenten naar de subkomponenten, krijgen we het samengestelde gedrag van alle betrokken komponenten ineen.
Euro voorbeeld
Laat ik het illustreren met een voorbeeld.
Stel we hebben een euro component, genaamd TEuro als volgt:
type
TEuro = class(TComponent)
private
FEuro: double;
protected
procedure SetEuro(Value: double);
function GetGulden: double;
published
property Euro: double read FEuro write SetEuro;
property Gulden: double read GetGulden;
end;
Dit komponent heeft een read/write property Euro, en een read-only property Gulden.
Het zou ook events kunnen hebben (bijvoorbeeld in geval van een bankrekening een event voor het rood staan, of juist voor het weer niet-rood staan).
Voor het vervolg van dit stukje zijn de events echter niet noodzakelijk - de properties zijn al voldoende om de techniek van delegation te kunnen illustreren (deelnemers aan de Delphi 5 Clinic over CBD hebben inmiddels ervaren hoe ook event handlers te delegeren zijn).
De uitdaging is nu om van de TEuro klasse een "euro-editbox" te maken, zonder daarbij de implementatie van TEuro zelf opnieuw te schrijven (de implementatie om van de interne waarde van FEuro een resultaat in Guldens via GetGulden terug te geven heb ik dan ook met opzet niet vermeld). In C++ zouden we hierbij een nieuwe klasse TEuroEdit kunnen maken door simpelweg een nieuw component van zowel TEuro als TEdit af te leiden. In Delphi is dit dus niet mogelijk, en moeten we via delegation aan het werk.
Aangezien de TEdit component verreweg de meeste properties en events heeft, nemen we deze als uitgangspunt.
Onze TEuroEdit is dan ook direkt afgeleid van TEdit, en zal een subcomponent van type TEuro hebben:
type
TEuroEdit = class(TEdit)
private
FEuro: TEuro;
public
constructor Create(AOwner: TComponent); override;
end;
De constructor is nodig om te zorgen dat de property FEuro ook daadwerkelijk wordt aangemaakt.
De implementatie van de constructor Create is als volgt:
constructor TEuroEdit.Create(AOwner: TComponent);
begin
inherited;
FEuro := TEuro.Create(Self);
FEuro.Name := 'Euro'
end;
Door in de aanroep van TEuro.Create als Owner de EuroEdit zelf (oftewel: Self) mee te geven weten we in ieder geval dat de FEuro property zal worden opgeruimd zodra de EuroEdit zelf wordt opgeruimd (die zal immers alles wij hij eigenaar van is netjes opruimen).
Eerste Poging...
Om de TEuroEdit nu naast de (inherited) properties en gedrag van TEdit ook de properties en gedrag van TEuro te geven zullen we deze opnieuw aan de buitenwereld kenbaar moeten maken.
De meest flauwe manier is door een property Euro te definiëren die zijn waarde van de FEuro property ophaalt:
type
TEuroEdit = class(TEdit)
private
FEuro: TEuro;
public
constructor Create(AOwner: TComponent); override;
published
Euro: TEuro read FEuro write FEuro;
end;
Het lijkt leuk, maar werkt toch niet helemaal.
In plaats van een Euro property waarvan we bijvoorbeeld de subproperties kunnen zien (denk maar eens een de Font property), krijgen we een Euro property met een dropdown-combobox waarin we de naam van onze EuroEdit zien, gevolgd door een punt en de naam van onze Euro property (in dit geval Euro), dus EuroEdit1.Euro.
Als we de dropdown-combobox open klappen zien we alle andere TEuro componenten die op de Form aanwezig zijn, en die we kunnen selecteren als waarde voor onze property.
Dat is dus niet echt wat we nodig hebben; en doet zelfs iets geheel onverwacht.
Om het gedrag van de Font property te kunnen nabootsen zullen we een speciale Property Editor moeten schrijven.
Voor meer informatie over dat onderwerp verwijs ik graag naar mijn artikel over Property & Component Editors, maar hier volgt de source code voor de simpele TEuroPropertyEditor:
type
TEuroProperty = class(TClassProperty)
public
function GetAttributes: TPropertyAttributes; override;
end;
implementation
function TEuroProperty.GetAttributes: TPropertyAttributes;
begin
Result := [paSubProperties]
end;
Om de EuroProperty editor voor de Euro property van de EuroEdit component te registreren moeten we gebruikmaken van een aanroep naar RegisterPropertyEditor:
RegisterPropertyEditor(TypeInfo(TEuro),TEuroEdit,'Euro',TEuroProperty)
Zelf vind ik het handig om dit in dezelfde Register procedure te zetten waar we ook de componenten TEuro en TEuroEdit zelf geregistreerd hebben.
Als we deze versie van de componenten met de property editor opnieuw installeren, dan zien we dat de Object Inspector deze keer wel reageert zoals we dat zouden verwachten: de subproperties Euro, Name en Tag zijn nu beschikbaar.
Tweede Poging...
Helaas werkt de techniek van de subproperties alleen voor normale properties, en niet voor event handlers (die we in properties van bijvoorbeeld type TNotifyEvent terugvinden).
Stel dat de TEuro component een event handler krijgt om de omslag van negatief naar positief te detecteren.
Dit kunnen we kwijt in een property OnSaldoOmslag van type TNotifyEvent.
Merk op dat het detecteren van de omslag en het vuren van de event handler door de implementatie van de TEuro zal worden afgehandeld.
De nieuwe definitie van TEuro is als volgt:
type
TEuro = class(TComponent)
private
FEuro: double;
FOnSaldoOmslag: TNotifyEvent;
protected
procedure SetEuro(Value: double);
function GetGulden: double;
published
property Euro: double read FEuro write SetEuro;
property Gulden: double read GetGulden;
property OnSaldoOmslag: TNotifyEvent read FOnSaldoOmslag write FOnSaldoOmslag;
end;
Om het event OnSaldoOmslag aan de buitenwereld bekend te maken op het moment dat we TEuro alleen maar als "embedded" (gedelageerd) component gebruiken binnen de TEuroEdit, zullen we de definitie van de OnSaldoOmslag property moeten herhalen.
Deze keer zal de implementatie (het vuren van het event) echter moeten gebeuren als het oorsponkelijke OmSaldoOmslag event van de TEuro vuurt.
Dit kan door zelf de event handler van de TEuro in te vullen...
De nieuwe definitie van TEuroEdit is als volgt:
type
TEuroEdit = class(TEdit)
private
FEuro: TEuro;
FOnSaldoOmslag: TNotifyEvent;
protected
procedure SaldoOmslag(Sender: TObject); virtual;
public
constructor Create(AOwner: TComponent); override;
published
property OnSaldoOmslag: TNotifyEvent read FOnSaldoOmslag write FOnSaldoOmslag;
end;
De constructor zal tevens moeten worden aangepast, om onze "SaldoOmslag" routine te koppelen aan de OnSaldoOmslag event handler van de embedded TEuro property:
constructor TEuroEdit.Create(AOwner: TComponent);
begin
inherited;
FEuro := TEuro.Create(Self);
FEuro.Name := 'Euro';
FEuro.OmSaldoOmslag := SaldoOmslag { event handler }
end;
Zodra nu het OnSaldoOmslag event van de TEuro vuurt, zal dit tot gevolg hebben dat de OnSaldoOmslag van de TEuroEdit ook vuurt.
En dat is precies de kern van delegation: het gedrag van de subcomponent wordt via de vader (TEuroEdit) naar de zoon (TEdit) doorgegeven (lees: gedelegeerd), en het event wordt door de zoon getriggerd en doorgegeven aan de vader (gepropageerd).
Dezelfde techniek kunnen we uithalen met de individuele properties van de TEuro property.
Behalve het publishen van een Euro property van type TEuro zelf, kunnen we ook een Euro property van type double publiceren.
De waarde wordt uiteraard niet opgeslagen in de TEuroEdit, maar juist overgelaten (gedelegeerd) aan het embedded TEuro component:
type
TEuroEdit = class(TEdit)
private
FEuro: TEuro;
FOnSaldoOmslag: TNotifyEvent;
protected
procedure SaldoOmslag(Sender: TObject); virtual;
public
constructor Create(AOwner: TComponent); override;
published
property Euro: double read GetEuro write SetEuro;
property OnSaldoOmslag: TNotifyEvent read FOnSaldoOmslag write FOnSaldoOmslag;
end;
De GetEuro en SetEuro methods zullen hun taak dus moeten delegeren naar de Euro property, als volgt:
function TEuroEdit.GetEuro: double;
begin
Result := FEuro.Euro
end;
procedure TEuroEdit.SetEuro(Value: double);
begin
FEuro.Euro := Value
end;
En zo kunnen we dus properties en events delegeren aan subcomponenten.
Een erg nuttige techniek, en in ieder geval inzichtelijker en beter te onderhouden dan multiple inheritance, nietwaar?
Delphi 5 heeft bovendien een handige feature die hiermee te combineren is, namelijk property categories.
Maar daarover de volgende keer meer.
Mocht iemand nog vragen, opmerkingen of suggesties hebben, dan hoor ik die het liefst via .