2
\$\begingroup\$

I'm currently in the process of clearing out the final parts of this question here.

Am I going about this the wrong way / could it be done smarter (interface perhaps) - or am I just plain silly, trying to keep my polymorphism hierarchy alive at "all means"?

I have a TBaseObject class - with a TBaseList<T:TBaseObject> class in the same Unit. And I'm using inheritance to re-use a lot of functionality/properties:

Base Unit;
TBaseObject = class
end;
TBaseList<T:TBaseObject> = class(TObjectList<T>)
end;

CustomCustomer Unit;
TCustomCustomer = class(TBaseObject) 
end;
TCustomCustomerList<T:TCustomCustomer> = class(TBaseList<T>)
end;

Customer Unit;
TCustomer = class(TCustomCustomer)
end;
TCustomerList<T:TCustomer> = class(TCustomCustomerList<T>)
end;

I'm going for a solution where every single class in my hierarchy can be marshalled into a JSON string notation.

Having a look at my TBaseObject, I thought I'll use a property Mar:TJsonMarshal and a class procedure TBaseObject.RegisterConverters(aClass:TClass; aMar:TJsonMarshal); to aid me in the process. This is also working as long as we don't get too creative. Let me explain with a bit more source code.

The BaseObject RegisterConverter procedure is somewhat like this:

class procedure TBaseObject.RegisterConverters(aClass:TClass; aMar:TJsonMarshal); virtual;
begin
  aMar.RegisterConverter(aClass, 'fMar',
    function(Data: TObject; Field: String): TObject
    begin
      Result := nil;
    end);  
end;

In a subclass - lets look at TCustomer I will now override the RegisterConverters procedure like this.

class procedure TCustomer.RegisterConverters(aClass:TClass; aMar:TJsonMarshal); override;
begin
  inherited; 
  aMar.RegisterConverter(aClass, 'fTimeOfLastContact',
  function(Data: TObject; Field: String): string
  var
    ctx: TRttiContext;
    date: TDateTime;
  begin
    date := ctx.GetType(Data.ClassType).GetField(Field).GetValue(Data).AsType<TDateTime>;
    Result := FormatDateTime('yyyy-mm-dd hh:nn:ss', date);
  end);  
end;

Now we place the central marshalling in TBaseObject like this :

function TBaseList<T>.Marshal: TJSONObject;
begin
  fMar := TJSONMarshal.Create();
  try
    RegisterConverters;
    try
      Result := fMar.Marshal(Self) as TJSONObject;
    except
      on e: Exception do
        raise Exception.Create('Marshal Error : ' + e.Message);
    end;
  finally
    fMar.Free;
  end;
end;

By doing this I have very easy access to do complete marshalling of my class TCustomer. Simply by declaring a TJsonObject variable and do this:

SomeJsonObj := SomeCustomer.Marshal;

And every other subclass (I have a lot) can do the same - just remember to override the RegisterConverter procedure in the subclass to support "special types".

But is this really the best solution?

Marshalling the List part

Is this an OK way of doing it?

Remember - it actually works. And REALLY eases the process of writing new units base on TBaseObject to achieve marshalling.

I always implement a List class to go alongside with my object class. Hence I have TBaseObject = class and TBaseList<T:TBaseObject> = TObjectList<T> declared in my base unit.

Now with the "nice" feature of being able to marshal any instance (ie. TCustomer) I would also like the possibility of marshaling a TCustomerList<TCustomer> class.

What I came up with was:

Make a copy of the RegisterConverters and Marshal method from the BaseObject implementation and use it in the List. That way - I should be able to get started.

I end up with a RegisterConverters procedure like this for the List:

procedure TBaseList<T>.RegisterConverters(aClass:TClass; aMar:TJsonMarshal); virtual;
begin
  aMar.RegisterConverter(aClass, 'fMar',
    function(Data: TObject; Field: String): TObject
    begin
      Result := nil;
    end);  

  T.RegisterConverters(T, fMar); // class method - Register element specific converters.
end;

Note that the List version of the RegisterConverters is NOT a class procedure. I do not see the need (but that might be my problem later). But now it should be clear as to WHY it was declared as a class method on the BaseObject class.

Implementing a TCustomerList<T>.RegisterConverters is easy as pie. Let's say we have a property DataBuildTime : TDateTime; on the list. Then again we would need a converter to ensure our datetime format is maintained:

procedure TCustomerList<T>.RegisterConverters(aClass:TClass; aMar:TJsonMarshal); override;
begin
  inherited; 
  aMar.RegisterConverter(aClass, 'fDataBuildTime',
  function(Data: TObject; Field: String): string
  var
    ctx: TRttiContext;
    date: TDateTime;
  begin
    date := ctx.GetType(Data.ClassType).GetField(Field).GetValue(Data).AsType<TDateTime>;
    Result := FormatDateTime('yyyy-mm-dd hh:nn:ss', date);
  end);  
end;

And finally we can do this with a TCustomerList<TCustomer> variable called aList:

SomeJsonObject := aList.Marshal;

The problem:

This still works - my problem first arises when combining my hierarchy classes. Having a TWorker that holds a TCustomerList<TCustomer>, then my whole marshaling act falls apart.

\$\endgroup\$
1
  • \$\begingroup\$ You spend lots of text describing what works, but your entire explanation of "the problem" is that something "falls apart." That's probably why this question was closed on Stack Overflow. \$\endgroup\$ Feb 15, 2012 at 15:22

2 Answers 2

3
\$\begingroup\$

Your RegisterConverters functions are bad, and part of the reason is that they're not sure what they're registering anything for. The class methods actually receive two classes as arguments. The first is the obvious one in aClass, but then there's also Self. Ideally, aClass should be a descendant of Self, so you should assert that:

Assert(aClass.InheritsFrom(Self),
  'I''m being asked to register things for an unrelated class');

Once you've determined that the assertion never fails, you can simplify your code by removing the aClass parameter entirely. That way, it's clear that the RegisterConverters method is to register converters that the class needs for its own marshaling. For example, TCustomer has an fTimeOfLastContact field, so when the program calls TCustomer.RegisterConverters, it should register a converter for that field on TCustomer classes. And since we already know that the Data argument will be an instance of TCustomer, we don't really need all the RTTI exercises, either. Just type-cast to the type you already know it is, and read the fTimeOfLastContact field directly.

class procedure TCustomer.RegisterConverters(aMar: TJsonMarshal); override;
begin
  inherited; 
  aMar.RegisterConverter(aClass, 'fTimeOfLastContact',
    function(Data: TObject; Field: string): string
    var
      date: TDateTime;
    begin
      Assert(Data is TCustomer);
      Assert(Field = 'fTimeOfLastContact');
      date := TCustomer(Data).fTimeOfLastContact;
      Result := FormatDateTime('yyyy-mm-dd hh:nn:ss', date);
    end);  
end;

The RegisterConverters methods for your list classes should be class methods. You say you don't "see the need" for them to be class methods, but if that's the case, then you're looking at things backward. Every method should be a class method unless it requires access to instance data. Your lists' RegisterConverters methods never access any instance data. In particular, they never consider their own lengths or their contents. They're designed to teach the marshaller how to marshal an arbitrary instance of the list class. Your example tells the marshaller that instances of TCustomerList<T> should have a JSON property named fDataBuildTime, and when it's time to marshal a list, the marshaller will call the callback function to ask what value that property should have for the given instance. None of that depends on the instance you call RegisterConverters on.

class procedure TCustomerList<T>.RegisterConverters(aMar: TJsonMarshal); override;
begin
  inherited;
  aMar.RegisterConverter(Self, 'fDataBuildTime',
    function(Data: TObject; Field: String): string
    var
      ctx: TRttiContext;
      date: TDateTime;
    begin
      date := ctx.GetType(Data.ClassType).GetField(Field).GetValue(Data).AsType<TDateTime>;
      Result := FormatDateTime('yyyy-mm-dd hh:nn:ss', date);
    end);
  T.RegisterConverters(aMar);
end;

I've added a call to T.RegisterConverters at the end so that when you register the list class, you implicitly register the contents, too.


I see no reason for fMar to be a member of your TBaseObject class. It being a member suggests that every single one of your objects will have its own JSON marshaller, which is ludicrous, especially since you've demonstrated how that field is used in practice. You create the marshaller, use it, and destroy it all in the same Marshal method. Here's how your Marshal method should look instead:

function TBaseList<T>.Marshal: TJSONObject;
var
  Mar: TJSONMarshal;
begin
  Mar := TJSONMarshal.Create;
  try
    RegisterConverters(Mar);
    try
      Result := Mar.Marshal(Self) as TJSONObject;
    except
      on e: Exception do
        raise Exception.Create('Marshal Error : ' + e.Message);
    end;
  finally
    Mar.Free;
  end;
end;
\$\endgroup\$
1
  • \$\begingroup\$ GREAT answer Rob. I will try and explain some of my insanity :-) Your absolutely right - No need for mar and unmar properties. I wonder why I haven't seen this before! And the RTTI stuff is also not needed - so that's gone too :-) About the aClass parameter to my RegisterConverter method - your right on the money again - that was a leftover from an earlier test design. Regarding the call to Registerconverter from the list - I've done that already - on the BaseList. But the miracle happened and it actually seems to be working. I stuffed it all into one unit - if anyone is interested. \$\endgroup\$
    – Bimmer_R
    Feb 16, 2012 at 19:24
0
\$\begingroup\$

This is just to let everyone see my final test. All stuffed into on unit. This has all the changes suggested by Rob Kennedy (thanks again) and it also has a few other methods.

unit uDataObject;

interface

uses
  System.SysUtils, System.Classes, Generics.Collections, Data.DBXJson, Data.DBXJsonReflect, System.DateUtils, System.RTTI;

type
  TMyClass = class
  private
    aInteger: integer;
    aString: String;
    class procedure RegisterConverters(aMar: TJsonMarshal); virtual;
    class procedure RegisterReverters(aUnMar: TJSONUnMarshal); virtual;
  public
    constructor Create; virtual;
    procedure Assign(aSource: TMyClass); virtual;
    procedure SetupData; virtual;
    function Marshal: TJSONObject; virtual;
    procedure UnMarshal(aJsonObj: TJSONObject); virtual;
    function SaveToJsonFile(sFileName: String; bOverWriteIfExists: Boolean = true): Boolean; virtual;
    procedure LoadFromJsonFile(sFileName: String); virtual;
    property pInteger: integer read aInteger write aInteger;
    property pString: String read aString write aString;
  end;

  TMyClassList<T: TMyClass, constructor> = class(TObjectList<T>)
  private
    class procedure RegisterConverters(aMar: TJsonMarshal); virtual;
    class procedure RegisterReverters(aUnMar: TJSONUnMarshal); virtual;
  public
    constructor Create; virtual;
    procedure Assign(aSource: TMyClassList<T>); virtual;
    function Marshal: TJSONObject; virtual;
    procedure UnMarshal(aJsonObj: TJSONObject); virtual;
    function SaveToJsonFile(sFileName: String; bOverWriteIfExists: Boolean = true): Boolean; virtual;
    function AsBaseList: TMyClassList<TMyClass>; virtual;
    procedure LoadFromJsonFile(sFileName: String); virtual;
    procedure Populate100Elements;
  end;

  // COMPLEX OBJECT - Contains TStringList as property.
  TMyComplexClass = class(TMyClass)
  private
    aStringList: TStringList;
    class procedure RegisterConverters(aMar: TJsonMarshal); override;
    class procedure RegisterReverters(aUnMar: TJSONUnMarshal); override;
  public
    constructor Create; override;
    destructor Destroy; override;
    procedure Assign(aSource: TMyClass); override;
    procedure SetupData; override;
    property pStringList: TStringList read aStringList write aStringList;
  end;

  TMyComplexClassList<T: TMyComplexClass, constructor> = class(TMyClassList<T>)
  private
  public
    constructor Create; override;
    procedure Assign(aSource: TMyClassList<T>); override;
  end;

  // VERY COMPLEX CLASS - Contains a TMyComplexClassList as property.

  // Class to test one instance with a Complex List as property.
  TMyCompositeClass = class(TMyComplexClass)
  private
    aRandomAbove100: integer;
    aTimeStamp: TDateTime;
    aComplexList: TMyComplexClassList<TMyComplexClass>; // Complex list
    class procedure RegisterConverters(aMar: TJsonMarshal); override;
    class procedure RegisterReverters(aUnMar: TJSONUnMarshal); override;
  public
    constructor Create; override;
    destructor Destroy; override;
    procedure Assign(aSource: TMyClass); override;
    procedure SetupData; override;
    property pRandomAbove100: integer read aRandomAbove100 write aRandomAbove100;
    property pTimeStamp: TDateTime read aTimeStamp write aTimeStamp;
    property pComplexList: TMyComplexClassList<TMyComplexClass> read aComplexList write aComplexList;
  end;

var
  initComplexList: TMyComplexClassList<TMyComplexClass>;

implementation

{ TMyClass }

procedure TMyClass.Assign(aSource: TMyClass);
begin
  aInteger := aSource.aInteger;
  aString := aSource.aString;
end;

constructor TMyClass.Create;
begin
  inherited Create;
  aString := '';
  aInteger := 0;
end;

procedure TMyClass.LoadFromJsonFile(sFileName: String);
var
  aSL: TStringList;
  aObj: TJSONObject;
begin
  aSL := TStringList.Create;
  try
    aSL.LoadFromFile(sFileName);
    aObj := TJSONObject.Create;
    try
      aObj.Parse(TEncoding.ASCII.GetBytes(aSL.Text), 0);
      UnMarshal(aObj);
    finally
      aObj.Free;
    end;
  finally
    aSL.Free;
  end;
end;

function TMyClass.Marshal: TJSONObject;
var
  FMar : TJsonMarshal;
begin
  FMar := TJsonMarshal.Create;
  try
    RegisterConverters(FMar);
    try
      Result := FMar.Marshal(Self) as TJSONObject;
    except
      Result := nil;
    end;
  finally
    FMar.Free;
  end;
end;

class procedure TMyClass.RegisterConverters(aMar: TJsonMarshal);
begin
  // none yet - abstract?
end;

class procedure TMyClass.RegisterReverters(aUnMar: TJSONUnMarshal);
begin
  // none yet - abstract ?
end;

function TMyClass.SaveToJsonFile(sFileName: String; bOverWriteIfExists: Boolean): Boolean;
var
  aSL: TStringList; // makes it easy to save a file :-)
  aJObj: TJSONObject;
begin
  if not(bOverWriteIfExists) and (FileExists(sFileName)) then
    Exit(false);
  aSL := TStringList.Create;
  try
    aJObj := Marshal;
    try
      aSL.Add(aJObj.ToString);
      aSL.SaveToFile(sFileName);
      Result := true;
    finally
      aJObj.Free;
    end;
  finally
    aSL.Free;
  end;
end;

procedure TMyClass.SetupData;
begin
  aString := 'Created : ' + FormatDateTime('ddmmyyyy hhnnss_zzz', now);
  aInteger := Random(100);
end;

procedure TMyClass.UnMarshal(aJsonObj: TJSONObject);
var
  tmpObj: TMyClass;
  FUnMar : TJSONUnMarshal;
begin
  FUnMar := TJSONUnMarshal.Create;
  try
    RegisterReverters(FUnMar);
    try
      tmpObj := TMyClass(FUnMar.UnMarshal(aJsonObj)); // MUST USE CAST ... not as ... as uses RTTI and therefore needs info about class.
      try
        try
          Assign(tmpObj);
        except
          on e: Exception do
            raise Exception.Create('ERROR - UnMarshal MyClass(Assign) : ' + e.Message);
        end;
      finally
        tmpObj.Free;
      end;
    except
      on e: Exception do
        raise Exception.Create('ERROR - UnMarshal MyClass : ' + e.Message);
    end;
  finally
    FUnMar.Free;
  end;
end;

{ TMyComplexClass }

procedure TMyComplexClass.Assign(aSource: TMyClass);
var
  aStr: String;
  aCC: TMyComplexClass;
begin
  inherited Assign(aSource);
  aStringList.Clear;
  aCC := aSource as TMyComplexClass;
  for aStr in aCC.aStringList do
    aStringList.Add(aStr);
end;

constructor TMyComplexClass.Create;
begin
  inherited Create;
  aStringList := TStringList.Create;
end;

destructor TMyComplexClass.Destroy;
begin
  aStringList.Free;
  inherited;
end;

class procedure TMyComplexClass.RegisterConverters(aMar: TJsonMarshal);
begin
  inherited;
  // register class specifik converters here.
  aMar.RegisterConverter(TStringList,
    function(Data: TObject): TListOfStrings
    var
      i, Count: integer;
    begin
      Count := TStringList(Data).Count;
      SetLength(Result, Count);
      for i := 0 to Count - 1 do
        Result[i] := TStringList(Data)[i];
    end);
end;

class procedure TMyComplexClass.RegisterReverters(aUnMar: TJSONUnMarshal);
begin
  inherited;
  // register class specific reverters here
  aUnMar.RegisterReverter(TStringList,
    function(Data: TListOfStrings): TObject
    var
      StrList: TStringList;
      Str: string;
    begin
      StrList := TStringList.Create;
      for Str in Data do
        StrList.Add(Str);
      Result := StrList;
    end);
end;

procedure TMyComplexClass.SetupData;
var
  i: integer;
begin
  inherited SetupData;
  for i := 0 to aInteger do
    aStringList.Add(IntToStr(aInteger) + ' - Text - ' + IntToStr(i));
end;

{ TMyClassList<T> }

procedure TMyClassList<T>.Populate100Elements;
var
  i: integer;
  Element: T;
begin
  for i := 1 to 100 do
  begin
    Element := T.Create;
    Element.SetupData;
    Add(Element);
  end;
end;

function TMyClassList<T>.SaveToJsonFile(sFileName: String; bOverWriteIfExists: Boolean): Boolean;
var
  aSL: TStringList;
  aJObj: TJSONObject;
begin
  if not(bOverWriteIfExists) and (FileExists(sFileName)) then
    Exit(false);
  aSL := TStringList.Create;
  try
    aJObj := Marshal;
    try
      aSL.Add(aJObj.ToString);
      aSL.SaveToFile(sFileName);
      Result := true;
    finally
      aJObj.Free;
    end;
  finally
    aSL.Free;
  end;
end;

procedure TMyClassList<T>.UnMarshal(aJsonObj: TJSONObject);
var
  tmpList: TMyClassList<T>;
  FUnMar : TJSONUnMarshal;
begin
  FUnMar := TJSONUnMarshal.Create;
  try
    RegisterReverters(FUnMar);
    try
      tmpList := TMyClassList<T>(FUnMar.UnMarshal(aJsonObj)); // MUST CAST .. not use as ... since as uses RTTI ???
      try
        try
          Assign(tmpList);
        except
          on e: Exception do
            raise Exception.Create('ERROR - TMyClassList.UnMarshal(Assign) : ' + e.Message);
        end;
      finally
        tmpList.Free;
      end;
    except
      on e: Exception do
        raise Exception.Create('ERROR - TMyClassList.UnMarshal : ' + e.Message);
    end;
  finally
    FUnMar.Free;
  end;
end;

function TMyClassList<T>.AsBaseList: TMyClassList<TMyClass>;
var
  Element: T;
  NewBase: TMyClass;
begin
  Result := TMyClassList<TMyClass>.Create;
  Result.OwnsObjects := false;
  for Element in Self do
  begin
    NewBase := Element;
    Result.Add(NewBase);
  end;
end;

procedure TMyClassList<T>.Assign(aSource: TMyClassList<T>);
var
  newObj, Element: T;
begin
  Clear; // delete data - if present
  for Element in aSource do
  begin
    newObj := T.Create;
    newObj.Assign(Element);
    Add(newObj);
  end;
end;

constructor TMyClassList<T>.Create;
begin
  inherited Create; // WARNING .... specify create here - otherwise we leak mem ???? WHY ?  (seen in XE2)
end;

procedure TMyClassList<T>.LoadFromJsonFile(sFileName: String);
var
  aSL: TStringList;
  aObj: TJSONObject;
begin
  aSL := TStringList.Create;
  try
    aSL.LoadFromFile(sFileName);
    aObj := TJSONObject.Create;
    try
      aObj.Parse(TEncoding.ASCII.GetBytes(aSL.Text), 0);
      UnMarshal(aObj);
    finally
      aObj.Free;
    end;
  finally
    aSL.Free;
  end;
end;

function TMyClassList<T>.Marshal: TJSONObject;
var
  FMar : TJsonMarshal;
begin
  FMar := TJsonMarshal.Create;
  try
    RegisterConverters(FMar);
    try
      Result := FMar.Marshal(Self) as TJSONObject;
    except
      Result := nil;
    end;
  finally
    FMar.Free;
  end;
end;

class procedure TMyClassList<T>.RegisterConverters(aMar: TJsonMarshal);
begin
  // call class method to get element converters registered with list marshaller
  T.RegisterConverters(aMar); // class method - Register element specific converters.
end;

class procedure TMyClassList<T>.RegisterReverters(aUnMar: TJSONUnMarshal);
begin
  // List property specific reverters goes here
  // call class method to get element reverters registered with list unmarshaller
  T.RegisterReverters(aUnMar); // class method - Register element specific reverters.
end;

{ TMyComplexClassList<T> }

procedure TMyComplexClassList<T>.Assign(aSource: TMyClassList<T>);
begin
  inherited Assign(aSource);
end;

constructor TMyComplexClassList<T>.Create;
begin
  inherited Create;

end;

{ TMyListComplexClass }

procedure TMyCompositeClass.Assign(aSource: TMyClass);
var
  aCC: TMyCompositeClass;
begin
  inherited Assign(aSource);
  aCC := aSource as TMyCompositeClass;
  aRandomAbove100 := aCC.aRandomAbove100;
  aTimeStamp := aCC.aTimeStamp;
  aComplexList.Assign(aCC.aComplexList);
end;

constructor TMyCompositeClass.Create;
begin
  inherited Create;
  aRandomAbove100 := 0;
  aTimeStamp := 0;
  aComplexList := TMyComplexClassList<TMyComplexClass>.Create;
end;

destructor TMyCompositeClass.Destroy;
begin
  aComplexList.Free;
  inherited;
end;

class procedure TMyCompositeClass.RegisterConverters(aMar: TJsonMarshal);
begin
  inherited;
  aMar.RegisterConverter(Self, 'aTimeStamp',
    function(Data: TObject; Field: String): string
    begin
      Result := FormatDateTime('yyyy-mm-dd hh:nn:ss:zzz', TMyCompositeClass(Data).aTimeStamp);
    end);

  TMyComplexClassList<TMyComplexClass>.RegisterConverters(aMar);
end;

class procedure TMyCompositeClass.RegisterReverters(aUnMar: TJSONUnMarshal);
begin
  inherited;
  aUnMar.RegisterReverter(Self, 'aTimeStamp',
    procedure(Data: TObject; Field: string; Arg: string)
    begin
      TMyCompositeClass(Data).aTimeStamp := EncodeDateTime(StrToInt(Copy(Arg, 1, 4)), StrToInt(Copy(Arg, 6, 2)), StrToInt(Copy(Arg, 9, 2)), StrToInt(Copy(Arg, 12, 2)),
        StrToInt(Copy(Arg, 15, 2)), StrToInt(Copy(Arg, 18, 2)), StrToInt(Copy(Arg, 21, 3)));
    end);

  TMyComplexClassList<TMyComplexClass>.RegisterReverters(aUnMar);
end;

procedure TMyCompositeClass.SetupData;
begin
  inherited SetupData;
  aRandomAbove100 := Random(1000) + 100;
  aTimeStamp := now;
  aComplexList.Populate100Elements;
end;

initialization

initComplexList := TMyComplexClassList<TMyComplexClass>.Create;

finalization

initComplexList.Free;

end.
\$\endgroup\$
2
  • \$\begingroup\$ RegisterConverters and RegisterReverters should be protected. \$\endgroup\$ Sep 2, 2014 at 13:31
  • \$\begingroup\$ Your probably correct about that - but it could be a matter of design. But since this is a few years old now, newer and better technologies have been implemented, and Embacadero is deprecating the DBX. Marshal / UnMarshal is no longer needed as described. \$\endgroup\$
    – Bimmer_R
    Sep 12, 2014 at 10:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.