So thats the basics... now lets put it to use.
Code: [View]
interface uses streamedObject; type TMyDataStore = class(TStreamedObject) protected fMyIntData : integer; fMyStringData : string; public property myIntData:integer read fMyIntData write fMyIntData; property myStringData:string read fMyStringData write fMyStringData; published procedure load_ver1(reader:TReader); procedure save_ver1(writer:TWriter); end; implementation procedure TMyDataStore.load_ver1(reader:TReader); begin fMyIntData:=reader.readInteger; fMyStringData:=reader.readString; end; procedure TMyDataStore.save_ver1(writer:TWriter); begin writer.writeInteger(fMyIntData); writer.writeString(fMyStringData); end;
Code: [View]
var aStream : TFileStream; begin // To Save... aStream:=TFileStream.create('C:\MyData.dat',fmCreate); myDataStore.saveToStream(aStream); aStream.free; // To Load... aStream:=TFileStream.create('C:\MyData.dat',fmOpenRead); myDataStore.loadFromStream(aStream); aStream.free; end;
Well, thats a basic case... so lets look at version 2. We have added the field 'MyIntData2'.
Code: [View]
interface uses streamedObject; type TMyDataStore = class(TStreamedObject) protected fMyIntData : integer; fMyIntData2 : integer; fMyStringData : string; public constructor create; property myIntData:integer read fMyIntData write fMyIntData; property myIntData2:integer read fMyIntData2 write fMyIntData2; property myStringData:string read fMyStringData write fMyStringData; published procedure load_ver1(reader:TReader); procedure save_ver1(writer:TWriter); procedure load_ver2(reader:TReader); procedure save_ver2(writer:TWriter); end; implementation constructor TMyDataStore.create; begin inherited; fObjectVersion:=2; end; procedure TMyDataStore.load_ver1(reader:TReader); begin fMyIntData:=reader.readInteger; fMyStringData:=reader.readString; end; procedure TMyDataStore.save_ver1(writer:TWriter); begin writer.writeInteger(fMyIntData); writer.writeString(fMyStringData); end; procedure TMyDataStore.load_ver2(reader:TReader); begin load_ver1(reader); fMyIntData2:=reader.readInteger; end; procedure TMyDataStore.save_ver2(writer:TWriter); begin save_ver1(writer); writer.writeInteger(fMyIntData2); end;
One thing you will notice is that when you load a version 1 stream, the version 2 fields are not initialised. This can be addressed by initialising them in the version 1 loader, or by modifying the load method to call an initialisation function that sets all fields to a known state.
Now lets look at the version 3 code. Version 3 removes an item of data. This is a little trickier since we have removed the field that holds the data once it is loaded. The item we will remove is fMyIntData. To achieve this, it is necessary to make a modification to the routines that load and save this item (in this case the load_ver1 and save_ver1 methods) and to declare a new set of load and save code for version 3 and beyond.
Code: [View]
interface uses streamedObject; type TMyDataStore = class(TStreamedObject) protected fMyIntData2 : integer; fMyStringData : string; public constructor create; property myIntData2:integer read fMyIntData2 write fMyIntData2; property myStringData:string read fMyStringData write fMyStringData; published procedure load_ver1(reader:TReader); procedure save_ver1(writer:TWriter); procedure load_ver2(reader:TReader); procedure save_ver2(writer:TWriter); procedure load_ver3(reader:TReader); procedure save_ver3(writer:TWriter); end; implementation constructor TMyDataStore.create; begin inherited; fObjectVersion:=3; end; procedure TMyDataStore.load_ver1(reader:TReader); var empty : integer; begin // Since we no longer use fMyIntData, read its value into an empty variable empty:=reader.readInteger; fMyStringData:=reader.readString; end; procedure TMyDataStore.save_ver1(writer:TWriter); begin // Write an empty value (just in case we save to version 1) writer.writeInteger(0); writer.writeString(fMyStringData); end; procedure TMyDataStore.load_ver2(reader:TReader); begin load_ver1(reader); fMyIntData2:=reader.readInteger; end; procedure TMyDataStore.save_ver2(writer:TWriter); begin save_ver1(writer); writer.writeInteger(fMyIntData2); end; procedure TMyDataStore.load_ver3(reader:TReader); begin fMyStringData:=reader.readString; fMyIntData2:=reader.readInteger; end; procedure TMyDataStore.save_ver3(writer:TWriter); begin writer.writeString(fMyStringData); writer.writeInteger(fMyIntData2); end;
That concludes this little introduction to dynamic method calls and their practical application in constructing versioned data stores. I hope you can see that there are many possibile applications of this ability to call methods dynamically at runtime, but care should be taken when considering whether it is appropriate. I haven't conducted any timings but my gut feeling is that this is not the quickest way in the world. Depending on your application, a big ugly IF..THEN..ELSE or CASE... may be a better approach. But where speed isn't important and you're IF..THEN..ELSEs are getting out of hand, this may be applicable.
With regards to the data storage method presented here, there are other ways to achieve this kind of thing, but with this system you have full control over whats saved and how, you don't have to expose every field as published/public in order to store it and above all, its easy to understand... even for beginners.
As usual, if you have any questions or comments, then please feel free to email me on athena at outer hyphen reaches dot com or post a comment on the article. Thanks for reading. Until next time... take care and happy coding
2010 Update - This article did receive some comments and questions which were answered on this thread.
vBulletin Message