
E
ine in Software-Projekten häufig auftretende Aufgabe ist die Serialisierung und anschließende Deserialisierung von Daten. Dabei handelt es sich um die Darstellung komplexer Daten in einer Zeichenkette (Serialisierung), die dann z.B. auf einem Datenträger gespeichert oder über Netzwerke übertragen werden kann, wobei der Empfänger die empfangene Darstellung wieder in eine für ihn geeignete Form umwandelt (Deserialisierung).
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.
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.
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:
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 erfolgen, was auch erklärt, warum XmlSerializer mit vielen verschiedenen Containern6 umgehen kann. Eine Möglichkeit ist also das Entwickeln einer Implementierung7 von ICollection , die an den in ihr enthaltenen Objekten das Feld Parent über eine (ausschließlich dafür geschaffene) weitere Schnittstelle pflegt. Dies löst nicht nur das Problem der Serialisierung und Deserialisierung, sondern verlagert auch das Setzen von Parent vom umgebenden Code8 in den Container.
Die generisch implementierte Schnittstelle für den Zugriff auf Parent besteht dabei etwas unspektakulär aus nur einer einzigen Eigenschaft mit zwei Akzessoren.
Implementierung:
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 implementiert.
Das Dekorator-Muster basiert zunächst auf einer Schnittstelle für die zu dekorierenden Objekte, die in unserem Fall die bereits bestehende Schnittstelle ICollection ist, auf der auch XmlSerializer arbeitet. Dann wird eine abstrakte Dekorator-Basisklasse10 erstellt, die eine Referenz auf das zu dekorierende Objekt vom Typ ICollection speichert und selbst ICollection implementiert. Alle Felder der Schnittstelle werden an das referenzierte Objekt durchgeschleift und sind, wo es möglich ist, als virtual deklariert. Somit müssen in konkreten von CollectionDecorator erbenden Klassen nur diejenigen Felder implementiert werden, deren Verhalten sich ändert.
Implementierung:
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:
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:
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 ausreicht. Das ist aber überraschenderweise nicht so, da XmlSerializer zwar abstrakte Klassen, aber keine Schnittstellen serialisieren kann – selbst dann nicht, wenn ICollection bei der Deserialisierung von dem sie aggregierenden Typen instanziiert wird12. Weiter könnte ein externes Setzen von Collection fatale Folgen haben, falls ein Container verwendet werden würde, dessen Parent zu einem anderen Objektgraphen gehört oder der gar nicht mit ParentDecorator ausgestattet ist.
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 ist durch Zugriffsmodifizierer eingeschränkt und Node selbst speichert keine Referenz auf ihn. Dadurch wird die Tendenz des Dekorator-Musters, die Objektidentitäten zu verschleiern, stark abgemildert. Da schließlich der Zugriff der Dekoratoren nur über ICollection erfolgt, können mindestens alle im .NET-Framework implementierten generischen Container verwendet werden, die diese Schnittstelle implementieren.
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.
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 ICollection
Implementierung:
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.
Karl Eilebrecht, Gernot Starke: Patterns kompakt – Entwurfsmuster für effektive Software-Entwicklung,
Spektrum akademischer Verlag, 2010.
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.
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.