Schönen guten Tag liebe Delphi Gemeinde!
Was in vielen “modernen” Sprachen bereits gang und gäbe ist und zum guten Ton gehört haben wir nicht zur Verfügung, das serealisieren und deserialisieren von Objekten. Mit hilfe der RTTI (Run Time Type Info) und einigen Einschränkungen gibt es jedoch auch für uns die Möglichkeit derartige Mechanismen zu schaffen. Im folgendem möchte ich gerne meinen Lösungsansatz vorstellen, der mir und vielleicht sogar dem ein oder anderem Arbeit erspart oder zumindest Hilfestellung gibt.
Da ich in der Entwicklung alle meine Objekte mit dieser Funktion ausstatten möchte, muss ich mir eine eigene Classroot schaffen. Ich habe mich an dieser Stelle für den Typ TPersistent als Vaterklasse entschieden, da mir dieser die Methode Assign() zur Verfügung stellt.
type TeqObject = class( TPersistent )
In meiner eigenen Entwicklung leite ich nun alle meine benötigten Klassen von TeqObject ab.
Ein einfaches Beispiel soll die Anwendung und den Nutzen demonstrieren.
type TMitarbeiter = class( TeqObject ) private M_Vorname : String; M_Nachname : String; M_Alter : Integer; published property Vorname : String read M_Vorname write M_Vorname; property Nachname : String read M_Nachname write M_Nachname; property Alter : Integer read M_Alter write M_Alter; end; procedure ErzeugeMitarbeiter; var mitarbeiter : TMitarbeiter; begin mitarbeiter := TMitarbeiter.Create; with mitarbeiter do begin Vorname := 'Pascal'; Nachname := 'Potrafke'; Alter := 24; SaveAsXML( 'mitarbeiter.xml' ); end; FreeAndNil( mitarbeiter ); end;
Was habe ich nun gemacht? Ich habe ein Klasse erstellt, von dieser ein Objekt instanziiert und dieses einfach als XML gespeichert. Das ergebnis sieht wie folgt aus.
24 Potrafke Pascal
Die Delphi RTTI erlaubt es nur auf Eigenschaften zuzugreifen die im published Block definiert sind. Es werden also nicht alle Member Eigenschaften serialisiert. Das ist auch der Grund dafür, dass in dem Beispiel nur die Properties in der XML Datei zu sehen sind.
Was serialisiert wurde kann natürlich auch wieder deserialisiert werden. Dafür gibt es das obligatorische Gegenstück zur allen Save() Methoden in Delphi, nämlich die Load() Methoden.
mitarbeiter.LoadFromXML( 'mitarbeiter.xml' );
Soweit so gut, allerdings gibt es auch die ein oder andere Einschränkung. Im Detail bedeutet das, dass nicht für alle Typen von Borland RTTI Informationen zur Laufzeit erzeugt werden.
Dazu gehören tkRecord, tkArray, tkDynArray (bedingt) und Pointer jeglicher Art.
- tkArray: Delphi brauch hier keine Informationen, da normale (statische) Arrays kein Cleanup brauchen.
- tkDynArray: Für tkDynArray wird zwar eine RTTI Information zur Laufzeit erzeugt, diese dient Delphi jedoch nur um bei dem Speicher Management die Infos zu bekommen wieviele Einträge es von welchem Typ aufräumen muss.
Ein kurzer Ausschnitt aus der TypeInfo.pas (Delphi 2007 Version 11) soll die fehlenden Implementationen zeigen.
TTypeData = packed record case TTypeKind of tkUnknown, tkLString, tkWString, tkVariant: (); tkInteger, tkChar, tkEnumeration, tkSet, tkWChar: ( OrdType: TOrdType; case TTypeKind of tkInteger, tkChar, tkEnumeration, tkWChar: ( MinValue: Longint; MaxValue: Longint; case TTypeKind of tkInteger, tkChar, tkWChar: (); tkEnumeration: ( BaseType: PPTypeInfo; NameList: ShortStringBase; EnumUnitName: ShortStringBase)); tkSet: ( CompType: PPTypeInfo)); tkFloat: ( FloatType: TFloatType); tkString: ( MaxLength: Byte); tkClass: ( ClassType: TClass; ParentInfo: PPTypeInfo; PropCount: SmallInt; UnitName: ShortStringBase; {PropData: TPropData}); tkMethod: ( MethodKind: TMethodKind; ParamCount: Byte; ParamList: array[0..1023] of Char {ParamList: array[1..ParamCount] of record Flags: TParamFlags; ParamName: ShortString; TypeName: ShortString; end; ResultType: ShortString}); tkInterface: ( IntfParent : PPTypeInfo; { ancestor } IntfFlags : TIntfFlagsBase; Guid : TGUID; IntfUnit : ShortStringBase; {PropData: TPropData}); tkInt64: ( MinInt64Value, MaxInt64Value: Int64); tkDynArray: ( elSize: Longint; elType: PPTypeInfo; // nil if type does not require cleanup varType: Integer; // Ole Automation varType equivalent elType2: PPTypeInfo; // independent of cleanup DynUnitName: ShortStringBase); end;
Das hat zur Folge, das wenn man dieses Feature verwenden möchte, sich von Records und (dynamischen) Arrays verabschieden muss.
Anstelle von Records werden nun Klassen verwendet. Was bereits beim Mitarbeiter Beispiel gemacht wurde. Denn für solch einfache Strukturen hätte es eigentlich auch ein normaler Record getan.
Dennoch denke ich das dieser geringe Mehraufwand bei der Definition in hinsicht auf das (de)serialisieren kaum ins Gewicht fällt.
(Dynamische) Arrays werden nun durch eine weitere Classroot ersetzt.
type TDyneqObjectList = array of TeqObject; type TeqObjectList = class( TeqObject ) private M_List : TDyneqObjectList; ... ... ...
Mit dieser Listenklassen können Array Strukturen abgebildet werden und das (de)serialisieren Feature weiterhin bequem verwendet werden. Folgendes Beispiel soll die Listenklasse demonstrieren.
type TUnternehmen = class( TeqObject ) private M_Firmenname : String; M_Internetadresse : String; M_Mitarbeiter : TeqObjectList; public constructor Create; override; published property Firmenname : String read M_Firmenname write M_Firmenname; property Internetadresse : String read M_Internetadresse write M_Internetadresse; property Mitarbeiter : TeqObjectList read M_Mitarbeiter write M_Mitarbeiter; end; type TMitarbeiter = class( TeqObject ) private M_Vorname : String; M_Nachname : String; M_Alter : Integer; published property Vorname : String read M_Vorname write M_Vorname; property Nachname : String read M_Nachname write M_Nachname; property Alter : Integer read M_Alter write M_Alter; end; constructor TUnternehmen.Create; begin inherited Create; Self.M_Mitarbeiter := TeqObjectList.Create; end; procedure ErzeugeUnternehmen; var unternehmen : TUnternehmen; begin unternehmen := TUnternehmen.Create; with unternehmen do begin Firmenname := 'EQUITANIA Software GmbH'; Internetadresse := 'http://www.equitania.de'; with ( Mitarbeiter.Item[ Mitarbeiter.Add( TMitarbeiter.Create ) ] as TMitarbeiter ) do begin Vorname := 'Pascal'; Nachname := 'Potrafke'; Alter := 24; end; with ( Mitarbeiter.Item[ Mitarbeiter.Add( TMitarbeiter.Create ) ] as TMitarbeiter ) do begin Vorname := 'Paul'; Nachname := 'Exler'; Alter := 24; end; SaveAsXML( 'unternehmen.xml' ); end; end;
Die List funktioniert wie eine StringList und besitzt Methode wie Add(), Remove(), Item, etc.
Das Ergebnis sieht nun wie folgt aus.
EQUITANIA Software GmbH
http://www.equitania.de
24
Potrafke
Pascal
24
Exler
PaulVon der Komplexität der Klassenstrukturen gibt es keinerlei Einschränkungen.
Auch das Einlesen dieses XML Gebildes ist kein Problem. Einzige Schwierigkeit war hier, das beim einlesen die Objekte IN der Liste von dem jeweiligen Typ instanziiert werden müssen damit zur Laufzeit drauf zurückgegriffen werden kann. Aber auch darum muss man sich nicht mehr kümmern.
Was man jedoch derzeit noch von Hand machen musst ist das registrieren der Klassen. Ohne dem werden bzw. können die Objekte nicht instanziiert werden.
initialization RegisterClasses( [ TUnternehmen, TMitarbeiter ] );
So, nun wünsche ich viel Spass beim ausprobieren.
Eines möchte ich noch sagen. Der Code kann Fehler enthalten. Daher ist die Benutzung auf eigene Gefahr. Verbesserungsvorschläge und Anregungen gerne gesehen.
Sourcecode zum Download
