Heute will ich mal wieder aus einem Beispiel aus meinem Projektalltag ansprechen.
Es geht um Kalendereinträge in SharePoint. Wir haben 4 verschiedene Typen von Einträgen: Urlaub, Krank, Außer Haus, Im Haus. Im vorliegenden Fall müssen die Typen Urlaub und Krank exportiert werden. Generell ist es so, dass alle Typen bis auf 1-2 Felder die gleichen Eigenschaften besitzen. Ergo arbeite ich mit Vererbung. Die Base Class nennt sich CalendarEntry, von der die zwei Kindsklassen HolidayEntry und SicknessEntry erben (reine Data Transfer Objects).
Da ich die Daten untypisiert von SharePoint erhalte, habe ich mir eine generische Factory gebaut, die mir die 2 besagten Klassen instanziiert und füllt. Das Interface dazu sieht wie folgt aus:
1: public interface ICalendarEntryFactory<T> where T:CalendarEntry, new()
2: {
3: IEnumerable<T> Build(SPListItemCollection calendarEntries);
4: }
Die implementierende Klasse enthält also eine Methode namens Build, die mir aus den Kalendereinträgen von SharePoint eine Liste von typisierten Objekten (IEnumberable deswegen, weil es allgemeiner ist als List<T>) zurückgibt. Um welche Objekte es sich handelt, die die Factory erzeugt, wird beim Instanziieren der Factory angegeben, allerdings habe ich den Objekttyp auf CalendarEntry oder eine Ableitung davon eingeschränkt (über where T:CalendarEntry in Zeile 1). Außerdem muss das Objekt (also mein DTO), welches die Factory später erzeugen können soll, einen parameterlosen, öffentlichen Konstruktor haben. Dies gewährleiste ich über das new() in Zeile 1.
Die Klasse, die nun dieses Interface implementiert, sieht wie folgt aus:
1: public class CalendarEntryFactory<T> : ICalendarEntryFactory<T>
where T : CalendarEntry, new()
2: {
3: private readonly IRulesEngine<T> _rulesEngine;
4:
5: public CalendarEntryFactory(IRulesEngine<T> rulesEngine)
6: {
7: _rulesEngine = rulesEngine;
8: }
9:
10: public IEnumerable<T> Build(SPListItemCollection calendarEntries)
11: {
12: ...
13: }
14: }
Noch kurz einen Hinweis zu dem Konstruktorparameter rulesEngine in Zeile 5: Da DTOs niemals Logik enthalten sollten und die Factory, wie der Name schon sagt, lediglich die Funktion erfüllt, dass es Objekte erzeugen kann, benötige ich noch ein Regelwerk, welches die erzeugten DTOs prüft, ob diese korrekt sind. Nur, wenn diese korrekt sind, werden sie der Liste, welche in der Build-Methode zurückgegeben wird, hinzugefügt. Meiner Meinung ist das sehr sinnvoll, um eine stärkere Code Kohäsion zu erhalten. Das entspricht auch den Clean Code Prinzipien Single Responsibility Principle und Seperation of Concerns. Die RulesEngine ist wie man sieht ebenfalls generisch gehalten. Logischerweise mit dem gleichen Typparameter T!
Ein Beispiel, was ich unter anderem mit der RulesEngine prüfe: Ist der Urlaubseintrag vom Typ JAZ (= Jahresarbeitszeit, ohne näher zu erklären, was JAZ bedeutet), so handelt es sich um einen Eintrag, der nicht in das Lohnprogramm exportiert werden darf.
Aber zurück zu der Build-Methode, die mir generisch die Objekte erzeugen soll: Da die Objekte teilweise unterschiedliche Eigenschaften haben (nämlich die, die in den ableitenden Klassen selbst definiert sind), muss ich in der Build-Methode eine Fallunterscheidung machen. Der Code sieht wie folgt aus:
1: var entriesResult = new List<T>();
2: T entry;
3:
4: foreach (SPListItem calendarEntry in calendarEntries)
5: {
6: entry = new T();
7:
8: foreach (SPListItem calendarEntry in calendarEntries)
9: {
10: //erledige Zuweisungen, die bei allen Eintragstypen gleich sind
11:
12: if (entry.GetType().Equals(new HolidayEntry().GetType()))
13: {
14: //erledige spezielle Zuweisungen für den Eintragstyp Urlaub
15: }
16: else if (entry.GetType().Equals(new SicknessEntry().GetType())) {
17: //erledige spezielle Zuweisungen für den Eintragstyp Krank
18: }
19:
20: //erledige Anweisungen, die bei allen Eintragstypen gleich sind
21: if (_rulesEngine.Validate(entry)) entriesResult.Add(entry);
22: }
23: }
In Zeile 6 sieht man, warum ich bei meinem generischen Constraint dieses “New()” angegeben hatte: Ich muss zur Laufzeit von dem Typ, für den die Factory instanziiert wurde, ein Objekt erzeugen können. Die eigentliche Problematik tritt auf, wenn man die Fallunterscheidung durchführen will (Zeile 12 und 16):
Eine Prüfung auf den Namen des Typs wollte ich nicht durchführen, das ich dann den Namen hardcodiert als Zeichenfolge hätte hinterlegen müssen. Deshalb Instanziiere ich mir in den Zeilen 12 und 16 ein leeres Objekt und hol mir dessen Typ. Der Vorteil ist, dass Refactoring Tools und natürlich auch der Compiler das erkennen. Statt der If-Else-Verschachtelung wollte ich eigentlich eine Switch-Anweisung nehmen (macht natürlich v.a. dann Sinn, wenn man viele solcher Typen hat), allerdings ist hier das Problem, dass der Compiler eine Konstante in den Case-Anweisungen erwartet. Das ist leider nicht gegeben, da ja erst zur Laufzeit bekannt ist, was “entry.GetType()” eigentlich ist.
Peter Hallam, Entwickler bei MS, hatte den Hintergrund dazu bereits 2005 gebloggt. Das macht natürlich Sinn, dass man kein switch auf einen Typen anwenden kann.
Wer allerdings keines der genannten Szenarien aus zuvor erwähntem Blogeintrag bei sich in der Anwendung vorliegen hat, kann sich die Klasse aus dem Blogeintrag switching on types hernehmen, sodass er nicht mit if-else-Verzweigungen arbeiten muss.