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-Entwurfsmusters
1 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 Implementierung
2 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 deserialisiert
3 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 wird
4. 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 Containern
5 kann dabei nämlich über
ICollection erfolgen, was auch erklärt, warum
XmlSerializer mit vielen verschiedenen Containern
6 umgehen kann. Eine Möglichkeit ist also das Entwickeln einer Implementierung
7 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 Code
8 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:
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 Typparameters
9 T sicherstellen, dass der verwendete Typ für die Elemente des Containers die Schnittstelle
IChild implementiert.
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 ist, auf der auch
XmlSerializer arbeitet. Dann wird eine abstrakte Dekorator-Basisklasse
10 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:
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 macht
11. 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 wird
12. 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.
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.