Blog / Research · Deserialisierung
DESERIALISIERUNG UND DAS DEKORATOR-MUSTER
16. April 2018
Florian Diedrich
Ein typisches Beispiel ist die Serialisierung von Objektgraphen in XML-Dokumente. Das .NET-Framework bietet dafür z.B. durch XmlSerializer gute Unterstützung. Diese allein reicht aber nicht aus, wenn dabei Objektreferenzen nicht nur top-down, sondern auch bottom-up verwaltet werden sollen. Wir stellen eine an die vorhandenen Strukturen angepasste Lösung vor, die im Kontext eines etablierten Software-Entwurfsmusters1 beleuchtet wird.
EINFÜHRUNG
Zuerst gehen wir auf ein bei der Serialisierung auftretendes Problem ein und beschreiben den technischen Hintergrund eines Lösungsansatzes. Dann erläutern wir eine Umsetzung mit Hilfe des Dekorator-Musters. Weiter diskutieren wir Vor- und Nachteile unserer Implementierung2 und schließen mit einem kurzen Ausblick.1. OBJEKTREFERENZEN UND OBJEKTIDENTITÄT
Wenn in der Software-Entwicklung etwas komplexere, dynamische Datenstrukturen nötig sind, hat man schnell einen Baum implementiert. Ein Knotentyp für diesen sieht dann etwa wie folgt aus, wobei Id das einzige Nutzdatum sein soll.Implementierung:
public class Node { public string Id; public List<Node> Children = new List<Node>(); }
Dieser Typ kann ohne Angabe von Attributen durch XmlSerializer serialisiert und deserialisiert3 werden. Eine direkte Navigation ist aber immer nur top-down möglich, nämlich vom Elternobjekt zu seinen Kindern, aber nicht umgekehrt. Dies kann von einem betroffenen Entwickler schnell als Mangel empfunden werden. Es wird dann reflexhaft ein neues Feld, etwa public Node Parent hinzugefügt, das dann immer konsistent gesetzt werden muss. Beim Serialisieren fällt auf, dass XmlSerializer eher schlecht mit mehrfach referenzierten Objekten umgehen kann – bei korrekter Belegung von Parent enthält der Objektgraph einen Kreis, der zu dem vergeblichen Versuch führt, die gleichen Objekte unendlich oft zu serialisieren. Der Grund dafür ist, dass die Objektidentitäten nicht berücksichtigt werden.
Abhilfe schafft das Attribut XmlIgnore, mit dem Parent von der Serialisierung und Deserialisierung ausgeschlossen wird4. Nachdem die Serialisierung damit wieder möglich ist, kommt beim Entwickler erneut milde Frustration auf – soll jetzt etwa in einem der Deserialisierung nachgelagerten Schritt (z.B. durch Tiefensuche) jeweils das Feld Parent individuell gesetzt werden? Das erscheint überflüssig, da die nötige Information ja während der Deserialisierung vorhanden ist.
Der Schlüssel ist ein detaillierteres Verständnis der Serialisierung und Deserialisierung. Das Befüllen von Containern5 kann dabei nämlich über ICollection
Die generisch implementierte Schnittstelle für den Zugriff auf Parent besteht dabei etwas unspektakulär aus nur einer einzigen Eigenschaft mit zwei Akzessoren.
Implementierung:
public interface IChild<T> { T Parent { get; set; } }
Nachdem wir also den technischen Rahmen geklärt haben, werden wir die gewünschten Schritte mit Hilfe des Dekorator-Musters implementieren, wobei wir noch eine verbesserte Trennung der Zuständigkeiten erhalten. Dabei werden wir durch Einschränkung des Typparameters9 T sicherstellen, dass der verwendete Typ für die Elemente des Containers die Schnittstelle IChild
2. UMSETZUNG MIT HILFE DES DEKORATOR-MUSTERS
Das Dekorator-Muster basiert zunächst auf einer Schnittstelle für die zu dekorierenden Objekte, die in unserem Fall die bereits bestehende Schnittstelle ICollectionImplementierung:
public abstract class CollectionDecorator<T> : ICollection<T> { private ICollection<T> Collection; public CollectionDecorator (ICollection<T> Collection) { this.Collection = Collection; } // implementation of ICollection<T> is omitted }
Dabei ist zu beachten, dass die Referenz auf das zu dekorierende Objekt selbst als private deklariert ist, also nach seinem Setzen im Konstruktor von außen nicht mehr zugänglich ist. Eine minimale, aber vollständig parametrisierte Implementierung des gewünschten konkreten Dekorators sieht dann folgendermaßen aus.
Implementierung:
public class ParentDecorator<T> : CollectionDecorator<T> where T : IChild<T> { private T Parent; public ParentDecorator(ICollection<T> Collection, T Parent) : base(Collection) { this.Parent = Parent; } public override void Add(T item) { base.Add(item); if (null != item) item.Parent = Parent; } public override bool Remove(T item) { var Result = base.Remove(item); if (Result && null != item) item.Parent = default(T); return Result; } }
Die so vorbereiteten Strukturen ermöglichen eine äußerst kurze Implementierung von Node, da die gesamte „Intelligenz“ im Dekorator steckt und sowohl für XmlSerializer als auch den Anwendungscode transparent ist.
Implementierung:
public class Node : IChild<Node> { public string Id; [XmlIgnore] public Node Parent { get; set; } public readonly CollectionDecorator<Node> Children; public Node() { this.Children = new ParentDecorator<Node>(new List<Node>(), this); } }
3. VOR- UND NACHTEILE DER LÖSUNG
Wie es bei der Ausarbeitung von Details in einem vorgegebenen technischen Kontext immer der Fall ist, haben die bei der Implementierung getroffenen Entscheidungen natürlich Konsequenzen. Um ein realistisches Bild zu zeichnen, stellen wir also klar die Vor- und Nachteile gegenüber.
Zunächst kann Parent von außen gesetzt werden, obwohl dies ausschließlich von ParentDecorator aus erwünscht ist. Ebenso nachteilig ist die sehr starke Anpassung an XmlSerializer, was eine direkte Übertragung des Ansatzes auf DataContractSerializer unmöglich macht11. Auffallend ist die Verwendung von CollectionDecorator in Node. Intuitiv würde man erwarten, dass ICollection
Durch den Zugriffsmodifikator readonly für Children können wir dies jedoch verhindern und somit erreichen, was uns bei Parent nicht gelungen ist. Der Zugriff auf den im Konstruktor von Node erzeugten Container vom Typ List
AUSBLICK
Wir haben eine Verwaltung von Objektreferenzen über das Dekorator-Muster so implementiert, dass sie für die Serialisierung komplett und den Anwendungscode zumindest weitestgehend transparent ist. Dadurch fügt sich unsere Implementierung nahtlos in ihre Umgebung ein, ist durch die Trennung von Zuständigkeiten wiederverwendbar und kann auch ohne vollständige Kenntnis der Details in bestehende Implementierungen integriert werden. Da in unserem Objektgraphen nur ein einziger Typ vorkommt, hat ParentDecorator auch nur einen einzigen Typparameter. Wenn im Objektgraphen mehr als ein Typ vorkommt, was etwa im Zusammenwirken mit dem Kompositum-Muster der Fall sein kann, wird es etwas komplexer. Gleichzeitig wird es aber auch spannender, weil wir uns dann über die Gemeinsamkeiten der vorkommenden Typen bewusst werden müssen, um eine Serialisierung und Deserialisierung über einen einheitlichen Mechanismus vorzunehmen.Die Implementierung ist zusätzlich als Projektmappe für Visual Studio verfügbar unter
https://github.com/DbRRaU6m/Serialization https://msdn.microsoft.com/de-de/library/58a18dwa(v=vs.120).aspx
https://msdn.microsoft.com/
Bei bloßer Anwendung ist der genaue Ablauf der Serialisierung einem Entwickler typischer- und berechtigterweise nicht voll bewusst; stattdessen wird der gesamte Ablauf als „automagisch“ hingenommen.
https://msdn.microsoft.com/de-de/library/system.collections.generic(v=vs.110).aspx
http://www.thomaslevesque.com/2009/06/12/c-parentchild-relationship-and-xml-serialization/
Das Setzen bzw. Löschen von Parent dient der Aufrechterhaltung einer Invariante des Objektgraphen. Möglicherweise unterstützt es die Kapselung stärker, diese Aufgabe in den Klassen zu implementieren, die am Objektgraphen beteiligt sind, und nicht außerhalb davon.
www.fastleansmart.com/blog/die-drei-interessantesten-features-von-cs/
Sie kann auch als Basisklasse weiterer Dekoratoren dienen, die sich dann miteinander verketten lassen. Dadurch ist es möglich, Objekte mit verschiedenem Verhalten zu erzeugen (und in einem gewissen Rahmen das Verhalten sogar zur Laufzeit zu verändern), ohne neue Typen zu implementieren.
Dies ist ein geringer Nachteil, da DataContractSerializer mit Kreisen im Objektgraph umgehen kann:
https://msdn.microsoft.com/en-us/library/hh241056(v=vs.100).aspx
Die Klasse ParentDecorator besitzt keinen parameterlosen Konstruktor, was eine direkte Erzeugung durch XmlSerializer unmöglich macht. Stattessen erfolgt die Erzeugung im Konstruktor von Node, was aus Gründen der Sichtbarkeit der benötigten Argumente auch die einzige sinnvolle Stelle ist.