Take for example the following message:
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phone = 4;
}
Let's design a DTO (Data Transfer Object) classes corresponding to this Message.
TPhoneType = (ptMOBILE, ptHOME, ptWORK);
TPhoneNumber = class
private
FTyp: TPhoneType;
FNumber: AnsiString;
const
ft_Number = 1;
ft_Typ = 2;
public
constructor Create;
property Number: AnsiString read FNumber write FNumber;
property Typ: TPhoneType read FTyp write FTyp;
end;
TPerson = class
private
FName: AnsiString;
FEmail: AnsiString;
FId: integer;
FPhones: TObjectList;
function GetPhones(Index: integer): TPhoneNumber;
function GetPhonesCount: integer;
const
ft_Name = 1;
ft_Id = 2;
ft_Email = 3;
ft_Phone = 4;
public
constructor Create;
destructor Destroy; override;
procedure AddPhone(const Number: AnsiString; Typ: TPhoneType = ptHOME);
procedure DeletePhone(Index: integer);
property Name: AnsiString read FName write FName;
property Id: integer read FId write FId;
property Email: AnsiString read FEmail write FEmail;
property PhonesCount: integer read GetPhonesCount;
property Phones[Index: integer]: TPhoneNumber read GetPhones;
end;
I like it when the functionality to load and unload excluded from DTO classes. The class should do what that one function and do it well.
Classes unloading facilities can store anywhere. Classes downloads can instantiate DTO classes from anywhere.
Although nothing prevents implement methods for loading / unloading directly into DTO classes. You can implement a DTO class constructor with a parameter containing the name of the file or stream to download and implement a method to record the save.
In the present example, the objects are created for embedded messages. This could be compared to the code generated by the utility protoc on base message schema.
It would be nice to have a tool for this library. I have a dream to make this tool. I do not want the community of fans of Delphi and Pascal were anything infringed.
TPersonBuilder = class
private
FBuffer: TProtoBufOutput;
public
constructor Create;
destructor Destroy; override;
function GetBuf: TProtoBufOutput;
procedure Write(Person: TPerson);
end;
TPersonReader = class
private
FBuffer: TProtoBufInput;
procedure LoadPhone(Phone: TPhoneNumber);
public
constructor Create;
destructor Destroy; override;
function GetBuf: TProtoBufInput;
procedure Load(person: TPerson);
end;
To write an object is used class TProtoBufOutput.
He has a method to write a fields of different types.
In the method of entry must specify the number of unloading field.
This field number with wireType is used to create a tag.
tag := (fieldNumber shl 3) or wireType;
What rules should be done?
If the field has a default value, then you have to unload it when its value is different from the default
If the field is not required, then do not unload it when the field is empty
procedure TPersonBuilder.Write(Person: TPerson);
var
Phone: TPhoneNumber;
PhonesBuffer: TProtoBufOutput;
i: Integer;
begin
FBuffer.writeString(TPerson.ft_Name, Person.Name);
FBuffer.writeInt32(TPerson.ft_Id, Person.FId);
// Not save empty e-mail
if Person.Email <> '' then
FBuffer.writeString(TPerson.ft_Email, Person.Email);
// Save Phones as Message
if Person.GetPhonesCount > 0 then
begin
PhonesBuffer := TProtoBufOutput.Create;
try
// Write person's phones
for i := 0 to Person.GetPhonesCount - 1 do
begin
PhonesBuffer.Clear;
Phone := Person.Phones[i];
PhonesBuffer.writeString(TPhoneNumber.ft_Number, Phone.Number);
// Not save phone type with Default value = ptHOME
if Phone.FTyp <> ptHOME then
PhonesBuffer.writeInt32(TPhoneNumber.ft_Typ, Ord(Phone.FTyp));
// Write phones as message
FBuffer.writeMessage(TPerson.ft_Phone, PhonesBuffer);
end;
finally
PhonesBuffer.Free;
end;
end;
end;
Let's see the code for loading the class. Parameter person: TPerson must contain an empty class. Otherwise, the value of the fields will change to having a phone add new phones.
Perhaps this is what you want. Perhaps, it would be to implement a method clearing the DTO class and call before load.
Let's, first read the tag. Empty value tag means that we are at the bottom, and we have nothing more to read. Therefore, we will use a loop like while in which we will compare the value of the tag to 0.
Tag contains a packed type and number of fields.
For convenience checks, keep these values to local variables. Then we will use a case statement to the number field.
The case statement is basically a controlled GOTO to the address table and is very fast. That the table has a compact, it is desirable that field numbers were consecutive and not scatter.
procedure TPersonReader.Load(person: TPerson);
var
tag, fieldNumber, wireType: integer;
Phone: TPhoneNumber;
begin
tag := FBuffer.readTag;
while tag <> 0 do begin
wireType := getTagWireType(tag);
fieldNumber := getTagFieldNumber(tag);
case fieldNumber of
TPerson.ft_Name:
begin
Assert(wireType = WIRETYPE_LENGTH_DELIMITED);
person.Name := FBuffer.readString;
end;
TPerson.ft_Id:
begin
Assert(wireType = WIRETYPE_VARINT);
person.Id := FBuffer.readInt32;
end;
TPerson.ft_Email:
begin
Assert(wireType = WIRETYPE_LENGTH_DELIMITED);
person.Email := FBuffer.readString;
end;
TPerson.ft_Phone:
begin
Assert(wireType = WIRETYPE_LENGTH_DELIMITED);
Phone := TPhoneNumber.Create;
Person.FPhones.Add(Phone);
LoadPhone(Phone);
end;
else
FBuffer.skipField(tag);
end;
tag := FBuffer.readTag;
end;
end;
The data is saved to the phone as a embedded message. The structure of the message contains: a tag, message length and content fields phone.
Knowing this, it is easy to write the required code. He is not much different than loading Person, except: reading record size, need to check whether we are not out of the record size.
procedure TPersonReader.LoadPhone(Phone: TPhoneNumber);
var
tag, fieldNumber, wireType: integer;
size: Integer;
endPosition: Integer;
begin
size := FBuffer.readInt32;
endPosition := FBuffer.getPos + size;
repeat
tag := FBuffer.readTag;
if tag = 0 then exit;
wireType := getTagWireType(tag);
fieldNumber := getTagFieldNumber(tag);
case fieldNumber of
TPhoneNumber.ft_Number:
begin
Assert(wireType = WIRETYPE_LENGTH_DELIMITED);
Phone.Number := FBuffer.readString;
end;
TPhoneNumber.ft_Typ:
begin
Assert(wireType = WIRETYPE_VARINT);
Phone.Typ := TPhoneType(FBuffer.readInt32);
end;
else
FBuffer.skipField(tag);
end;
until FBuffer.getPos >= endPosition;
end;