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;