Deep copying (cloning) objects in Delphi

When I first took a look at the prototype design pattern in GoF(years ago), I realized that there was a big obstacle (challenge) to implement it in Delphi: How to write a routine to really clone (not just recreate) an object? In other words, how to perform a deep-copy of a living object in Delphi.

There are approaches out there mimicking the deep copy by simply calling the constructor and reassigning the state of the object by hand (I don’t like it). There are others exposing that a deep copy could be accomplished for the descendants of TPersistent by calling the Assign method (I don’t like it either).

With the new RTTI extensions it seemed to me (and to others) that a deep copy could be accomplished using reflection.

I was reluctant to write the routine myself since the work is not trivial. It could get really nasty because there might be composition, aggregation and God knows what within an arbitrary object.

So I waited….

Just a few days ago, I realized that I could use the JSON marshalling and unmarshalling features introduced in Delphi (2010?) to write the deep copy method. So I came with this:

.....

uses
  DBXJSON, DBXJSONReflect;
.....
 

function DeepCopy(aValue: TObject): TObject;
var
  MarshalObj: TJSONMarshal;
  UnMarshalObj: TJSONUnMarshal;
  JSONValue: TJSONValue;
begin
  Result:= nil;
  MarshalObj := TJSONMarshal.Create;
  UnMarshalObj := TJSONUnMarshal.Create;
  try
    JSONValue := MarshalObj.Marshal(aValue);
    try
      if Assigned(JSONValue) then
        Result:= UnMarshalObj.Unmarshal(JSONValue);
    finally
      JSONValue.Free;
    end;
  finally
    MarshalObj.Free;
    UnMarshalObj.Free;
  end;
end;

You can now use it like this:

.....

var
  MyObject1,
  MyObject2: TMyObject;
begin
  //Regular object construction
  MyObject1:= TMyObject.Create;

  //Deep copying an object
  MyObject2:= TMyObject(DeepCopy(MyObject1));

  try
    //Do something here

  finally
    MyObject1.Free;
    MyObject2.Free;
  end;
end;

I tested it with some complex cases and it seems to be working quite well. Anyhow, if you find any problems or limitations, please, let me know.

Now that you get the idea we can do more crazy things like patching TObject (or any other class hierarchy) by using helpers. Look at this:

.....

interface

uses
   DBXJSON, DBXJSONReflect;

type
  TObjectHelper = class helper for TObject
    function Clone: TObject;
  end;

implementation

function TObjectHelper.Clone: TObject;
var
  MarshalObj: TJSONMarshal;
  UnMarshalObj: TJSONUnMarshal;
  JSONValue: TJSONValue;
begin
  Result:= nil;
  MarshalObj := TJSONMarshal.Create;
  UnMarshalObj := TJSONUnMarshal.Create;
  try
    JSONValue := MarshalObj.Marshal(Self);
    try
      if Assigned(JSONValue) then
        Result:= UnMarshalObj.Unmarshal(JSONValue);
    finally
      JSONValue.Free;
    end;
  finally
    MarshalObj.Free;
    UnMarshalObj.Free;
  end;
end;

All of a sudden, TObject has a Clone method! Call it like this:

.....

var
  MyObject1,
  MyObject2: TMyObject;
begin
  //Regular object construction
  MyObject1:= TMyObject.Create;

  //Cloning an object
  MyObject2:= TMyObject(MyObject1.Clone);

  try
    //Do something here

  finally
    MyObject1.Free;
    MyObject2.Free;
  end;
end;

If you think that helpers are an aberration, you can still create a TCloneable class with a Clone method and inherit from it, right? You can even use the decorator pattern to attach a Clone method to an object. You can do more…Share it with me, please. Thanks!

7 comments:

  1. The drawback of the TObject helper is, that it will vanish whenever any other helper is insight scope. I would rather suggest a generic approach with a class function like this:

    class function Clone(Source: T): T;

    ReplyDelete
  2. Just thinking... If clone() would be a constructor that takes a prototype (instead of a regular method), you wouldn't need to type-cast the result.

    MyObject2 := TMyObject.Clone(MyObject1);

    I'm not sure what obstacles you'd run into while actually implementing it, but such an interface somehow seems more appealing to me.

    ReplyDelete
  3. Hey guys, I thank you for your feedback. I´ll be back in a few of hours and I´ll comment further. I am in a rush now ;-)

    ReplyDelete
  4. Uwe, the TObject helper was just for "craziness". I always wanted TObject to have a Clone method; so I gave it one just for fun. If we talk seriously, then I agree with you about the helper. I like your class function approach.

    The class function approach could be used to avoid the casting that Wouter was mentioning. The casting can also be avoided by providing the casted class with a regular Clone method that takes no arguments. Something like this:

    type
    TMyObject = class
    public
    function Clone: TMyObject;
    end;

    Then we can call:

    MyObject2:= MyObject1.Clone;

    The whole casting thing happened just because I put the Clone method in TObject.

    Wouter, I wouldn’t make the Clone method a constructor. I was trying not to construct, since it defeats the purpose of the Clone operation itself.

    Nonetheless, I like avoiding the casting. The class function approach we just discussed keeps the interface the way you wanted it, but without the use of a constructor.

    ReplyDelete
  5. Does this deep copy work with firemonkey application in Android? I need to copy a Bluetooth object (TBluetoothLEDevice)

    ReplyDelete
  6. This method as documented here works well for simple objects, but as you get more complex it starts to break down requiring more and more code to be added to the clone method to register different converters.

    When I need this type of functionality, I generally will implement a class specific assign method, and then manually copy over the property values that I need in the new object. There are plenty of existing implementations to use as examples in the VCL source. Its a little more work, but doesn't have the extra overhead of marshaling and unmarshaling.

    ReplyDelete
  7. Using Marshall and UnMarshall to copy a TObject delays processing because it will work with Strings , use sparingly , for example if you using inside a batch processing will delay the total time of this processing.

    ReplyDelete