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.
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- Java Language Specification 8.3.1.3. transient Fields
Variables may be marked transient to indicate that they are not part of the persistent state of an object.
- Java Language Specification 8.3.1.3. transient Fields
- 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.
Excellent article.
ReplyDeleteWhile not relevant to the problem you outline, it's worth mentioning that Delphi takes the "stored" storage specifier into account when persisting published properties.
Thanks!
DeleteTrue, 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 :)