Kindklassen einer Mutterklasse bekanntgeben

von Christian Stelzmann


Es mag Situationen geben, in denen eine Mutterklasse wissen muss, welche Kindklasse von ihr abgeleitet worden sind. Stellen wir uns z.B. ein Programm vor, welches bestimmte Messwerte einlesen und auswerten soll. Dabei werden je nach Einsatzgebiet verschiedene Messwerte benötigt, viele Grundfunktionen sind aber für alle Eisnatzorte identisch.

Also erstellt man eine Mutterklasse mit allen Methoden, von denen einige abstrakt bleiben, weil ihnen erst in den Kindklassen (für jeden Einsatzort eine) Leben eingehaucht wird.

mutter.pas
TMutter = class(TObject) private public //gemeinsame Methoden ... procedure doSomethingCommon; //... //abstrakte Methoden procedure auswertung; virtual; abstract; //... end;
kind1.pas
TKind1 = class(TMutter) private public procedure auswertung; override; //... end;
kind2.pas
TKind2 = class(TMutter) private public procedure auswertung; override; //... end;

Im Programm deklariert man die Mutterklasse und instanziert je nach Einsatzgebiet (vom Nutzer angegeben) eine der Kindklassen. Probleme bekommt man, wenn man eine Funktion "aus Datei laden" einbauen will. Denn die Datei wird je nach verwendeter Kindklasse eine andere Struktur haben, da ja andere Daten verwendet werden. Wie kann man also diese Datei möglichst elegant laden?

Die erste Idee wäre folgendes:

mutter.pas
type TIDString = String[3]; //speichert eine ID, die jede Klasse eindeutig identifiziert TMutter = class(TObject) protected //Wird in jeder Kindklasse implementiert, um die klassenspezifischen Daten von einem Stream zu holen procedure readDateFromStream(fs : TFileStream); virtual; abstract; public //... alle anderen Methoden ... class function loadFromFile(filename : String) : TMutter; end; //... class function TMutter.loadFromFile(filename : String) : TMutter; var fs : TFileStream; ID : TIDString; begin fs := TFileStream.Create(filename, fmOpenRead); try fs.Read(ID, sizeOf(TIDString)); if ID = 'KI1' then result := TKind1.Create; if ID = 'KI2' then result := TKind2.Create; //... result.readDataFromStream(fs); finally fs.Free; end; end

Der Nachteil ist offensichtlich: Jedes Mal, wenn wir eine neue Kindklasse einführen, müssen wir auch die Mutterklasse abändern. Das widerspricht natürlich den Prinzipien der OOP und ist außerdem sehr unpraktisch. Einfacher geht es, wenn jede Kindklasse sich bei der Mutterklasse "anmeldet". Dazu führen wir ein globales (ja, dieses Mal muss es sein ;-)) Array, welches aber nur in der Datei mutter.pas sichtbar ist. Angesprochen wird es über eine Prozedur:

mutter.pas
TMutter = class(TObject) //... public //... alle anderen Methoden ... class function getID : TIDString; virtual; abstract; end; type TKindClass = class of TMutter; procedure registriereKindklasse(klasse : TKindClass); implementation var KIND_KLASSEN : Array of TKindClass; procedure registriereKindklasse(klasse : TKindClass); begin SetLength(KIND_KLASSEN, Length(KIND_KLASSEN)+1); KIND_KLASSEN[High(KIND_KLASSEN)] := klasse; end;

Und in jeder Unit, in der wir eine Kindklasse einführen, muss sich die Kindklasse in das Array eintragen und außerdem so erweitert werden, dass sie ihre ID zurückgibt:

kind1.pas
TKind1 = class(TMutter) private public //... class function getID : TIDString; override; end; //... class function TKind1.getID : TIDString; begin result := 'KI1'; end; //... initialization registriereKindklasse(TKind1);
kind2.pas
TKind2 = class(TMutter) private public //... class function getID : TIDString; override; end; //... class function TKind2.getID : TIDString; begin result := 'KI2'; end; //... initialization registriereKindklasseTKind2);

Die Methode zum Laden einer Datei sieht dann so aus:

mutter.pas
class function TMutter.loadFromFile(filename : String) : TMutter; var fs : TFileStream; ID : TIDString; i : Integer; begin fs := TFileStream.Create(filename, fmOpenRead); try fs.Read(ID, sizeOf(TIDString)); for i:=0 to High(KIND_KLASSEN) do if KIND_KLASSEN[i].getID = ID then begin result := KIND_KLASSEN[i].Create; break; end; result.readDataFromStream(fs); finally fs.Free; end; end;

Somit muss die Mutterklasse nie wieder geändert werden und der Aufwand in den Kindklassen ist ebenfalls minimal.

Ich habe dieses zwar anhand eines (relativ) konkreten Beispiels gezeigt, jedoch glaube ich, dass es sehr viel mehr Situationen gibt, in denen man eine Art "Registrierung" von Klassen gebrauchen kann.

Ich hoffe, es hilft irgendwann mal jemanden. :-)

Vielen Dank an Manuel "Motzi" Pöter, der als Erster ein (damals noch statisches) Array von Klassen vorschlug. Danke!

Viele Grüße
Christian Stelzmann