When in Rome, do as the Romans do - Part I

While different OOP languages share some common principles and many coding patterns will be the same or at least fairly similar, there are also some significant differences that can make some commonly used patterns from one language quite unsuitable in another—and if used, such patterns can cause a lot of trouble.

Photo: Gary Todd from Xinzheng, China, PDM-owner, via Wikimedia Commons, https://commons.wikimedia.org/wiki/File:Colosseum_(48412957912).jpg

One such pattern can be found in their serialization approaches. In Java, object serialization is performed on fields. In Delphi, the convention is to serialize published properties. That is also the original purpose of the published visibility specifier.

Delphi also supports defining custom properties in TPersistent descendants, via DefineProperties method. When it comes to what kind of properties Delphi can serialize out of the box, there are some limitations, so you cannot serialize records, arrays, or indexed properties, and you can only serialize collections defined as TCollection, whose items are TCollectionItem descendants.

But for simplicity, to show the core difference in serialization conventions and what problems it can cause, I will focus on fields vs properties.

Java

  • serializes all fields (private, protected, public)
  • has the transient field modifier—for marking fields that should not be serialized
  • does not have the published visibility specifier

Delphi

  • serializes published properties
  • does not have transient nor any equivalent field modifier
    • newer Delphi versions allow custom attributes that can be used for configuring serialization, but the core Delphi RTL does not define such attributes that could be used across different serialization frameworks.
  • has the published visibility specifier

How that works in practice

Let's say we have a simple class that holds a single piece of data: A string holding the value 'abc'.

class Foo { String data; } Foo foo = new Foo(); foo.data = "abc";
TFoo = class(TPersistent) protected FData: string; published property Data: string read FData write FData; end; var Foo := TFoo.Create; Foo.Data := 'abc';

And if we serialize the above instances to JSON in Java and in Delphi, we expect to get following output:

{"data":"abc"}

If we serialize them to XML, we expect to get:

<data>abc</data>

If we serialize the Foo instance as part of some TComponent and use the default component streaming format in Delphi we will get:

object ... Foo.Data = 'abc' end

So far, so good. But let's add another field in the class, that will not be serialized.

class Foo { String data; transient String temp; } Foo foo = new Foo(); foo.data = "abc"; foo.temp = "temporary";
TFoo = class(TPersistent) protected FData: string; FTemp: string; public property Temp: string read FTemp write FTemp; published property Data: string read FData write FData; end; var Foo := TFoo.Create; Foo.Data := 'abc'; Foo.Temp := 'temporary';

If we serialize such an object instance in Java, regardless of the format (JSON, XML, or any other format, using any serialization framework), we will get exactly the same output like we got when only the data field was declared in the class. After all that is the whole purpose of the transient modifier in Java, and no matter which serialization framework you use in Java, the default serialization output created by those differently declared Java classes and their instances holding the same value in the data field will always be the same.

Now let's take a look on the Delphi side, using the default serialization behavior from REST.Json. Well, that is not right... we ended up with a temp field in the output, even though the property was not published, but public. That is not good.

{"data":"abc","temp":"temporary"}

However, if we use the new TFoo class and serialize it with the Delphi component streaming system, it will behave as expected and regardless of the new field, that field will not be serialized because it was not marked as published.

object ... Foo.Data = 'abc' end

The problem is that REST.Json is not a good Delphi citizen. Instead of doing what Romans—Delphians—do, and serializing published properties, it serializes fields.

And this wreaks havoc if you want to apply that JSON serialization framework on existing classes that use published properties.

Now, you can say there is a solution—we can just use the [JSONMarshalled(False)] attribute declared in REST.Json.Types—but this opens another can of worms. Now, you will have to add a hard dependency to your code, as you will need to include REST.Json.Types in the unit where your class is defined, and add the attribute to the field. If you still don't see why that is wrong: What if you also need to serialize that class to XML or another format? If those serialization frameworks are good citizens and use published property serialization, then you will only end up with a single hard dependency, but if those other frameworks are also using field-based serialization and have custom-defined attributes, your units and classes will soon enough turn into a complete mess and dependency nightmare.

Yes, you can always declare those classes separately, but seriously, if that is not an obvious DRY violation and copy-pasta code, then I don't know what is.

The conclusion is plain and simple. While Java follows the field serialization convention, Delphi uses the published serialization convention. Applying the Java serialization style in Delphi code is not an appropriate coding pattern. It just does not fit well in Delphi, because Delphi was designed around a totally different serialization style from the beginning.

TList<T> story

If you are still not convinced that using field-based serialization in Delphi is wrong, let me tell you a story about TList<T>...

If you don't know it yet, well, then you are the lucky one...

But, this is also a fairly long story, so I will tell you all about it in another post. For now, when writing Delphi code, just remember to do as the Delphians do... not all coding patterns used in other OOP languages fit well in Delphi and vice versa.

Comments

  1. Excellent article.
    While not relevant to the problem you outline, it's worth mentioning that Delphi takes the "stored" storage specifier into account when persisting published properties.

    ReplyDelete
    Replies
    1. Thanks!

      True, conventional Delphi serialization involves more than just publishing the property and that also includes property specifiers like 'stored', 'default' and 'nodefault'.

      I simplified the issue here to show main difference and core issues between field vs property approach.

      More detailed serialization articles covering other aspects will follow :)

      Delete

Post a Comment

Popular posts from this blog

Coming in Delphi 12: Disabled Floating-Point Exceptions

Assigning result to a function from asynchronous code

Beware of loops and tasks