In meinem Betrieb setzen wir SharePoint als Intranetplattform und GDI als Buchhaltungssystem ein. Kürzlich hatte ich die Anforderung, dass man Urlaubsdaten, welche die Mitarbeiter über SharePoint pflegen, ins GDI Lohnsystem zu überführen. Als ich bei GDI anfragte, gab man mir eine Beschreibung einer Standard CSV-Import-Schnittstelle oder einfach ausgedrückt: Eine Liste wie ich ich eine CSV Datei mit Datensätzen aufbauen muss.
Um das Programm, welches die Daten aus SharePoint extrahiert und GDI als CSV zur Verfügung stellt, für etwaige Dritte, die das Programm später pflegen müssen, einfach zu halten, habe ich mit sogenannten Fluent Interfaces eine rudimentäre API implementiert. Das hat den Vorteil, dass die Logik, welche zum Erstellen einer gültigen Export-Datei benötigt wird, sauber abstrahiert ist. Keiner, der z.B. den Zugriff auf SharePoint ändern will, muss sich noch mit dem Datenformat auseinandersetzen. Wie bin ich vorgegangen?
Zunächst habe ich mir für jedes Feld, welches bei Urlaubseinträgen gefüllt sein muss, ein Interface geschrieben. Der Einfachheit halber habe ich alles in die Datei IGdiApi.cs gepackt. Die Datei sieht so aus:
1: public interface IGdiContracts_Personalnr
2: {
3: IGdiContracts_Fehlzeitennr SetPersonalnr(Int32 personnelNo);
4: }
5:
6: public interface IGdiContracts_Fehlzeitennr
7: {
8: IGdiContracts_Beginn SetFehlzeitennr(Int32 timesAbsentNo);
9: }
10:
11: public interface IGdiContracts_Beginn
12: {
13: IGdiContracts_Ende SetBeginn(DateTime begin);
14: }
15:
16: public interface IGdiContracts_Ende
17: {
18: IGdiContracts_GanzeTage SetEnde(DateTime end);
19: }
20:
21: public interface IGdiContracts_GanzeTage
22: {
23: IGdiContracts_GdiEntry SetGanzeTage(bool allDayEvent);
24: }
25:
26: public interface IGdiContracts_GdiEntry
27: {
28: string GetCsvLine();
29: }
Im Anschluss habe ich die Datei GdiApi.cs erstellt. Die Datei enthält zwei Klassen (wie clean das ist, sei mal dahingestellt). Die erste Klasse bildet den späteren Einstiegspunkt für die API:
1: public class GdiApi : IGdiContracts_Personalnr
2: {
3: public IGdiContracts_Fehlzeitennr SetPersonalnr(int personnelNo)
4: {
5: return new GdiEntry(personnelNo);
6: }
7: }
Es folgte die zweite Klasse, die die restlichen Interfaces implementiert:
1: class GdiEntry : IGdiContracts_Fehlzeitennr, IGdiContracts_Beginn, IGdiContracts_Ende,
IGdiContracts_GanzeTage, IGdiContracts_GdiEntry
2: {
3: Int32 _personnelNummer;
4: Int32 _timesAbsentNo;
5: DateTime _begin;
6: DateTime _end;
7: char _beginIsAHalfDay;
8: char _endIsAHalfDay;
9:
10: public GdiEntry(Int32 personalNummer)
11: {
12: _personnelNummer = personalNummer;
13: }
14:
15: public IGdiContracts_Beginn SetFehlzeitennr(Int32 timesAbsentNo)
16: {
17: _timesAbsentNo = (Int32) timesAbsentNo;
18: return this;
19: }
20:
21: public IGdiContracts_Ende SetBeginn(DateTime begin)
22: {
23: _begin = begin;
24: return this;
25: }
26:
27: public IGdiContracts_GanzeTage SetEnde(DateTime end)
28: {
29: _end = end;
30: return this;
31: }
32:
33: public IGdiContracts_GdiEntry SetGanzeTage(bool allDayEvent)
34: {
35: //Logik zur Berechnung von _beginIsAHalfDay und _endIsAHalfDay
36: return this;
37: }
38:
39: public string GetCsvLine()
40: {
41: System.Text.StringBuilder result = new StringBuilder();
42:
43: result.Append(DateTime.Now.ToString("yyyy") + ";"); //aktuelles Jahr
44: result.Append(DateTime.Now.ToString("MM") + ";"); //aktueller Monat
45: result.Append(_personnelNummer.ToString() + ";"); //Personalnummer
46: //es folgen viele weitere leere Felder
47: result.Append(_timesAbsentNo.ToString() + ";"); //Fehlzeitennummer
48: result.Append(";"); //Bemerkung zur Fehlzeit
49: result.Append(_begin.ToShortDateString() + ";"); //Fehlzeiten Beginn
50: result.Append(_end.ToShortDateString() + ";"); //Fehlzeiten Ende
51: result.Append(_beginIsAHalfDay + ";"); //Halber Tag Beginn
52: result.Append(_endIsAHalfDay + ";"); //Halber Tag Ende
53: result.Append("N;"); //Keine Führung auf der Lohnsteuerkarte
54: result.Append("N;"); //Arbeitsunfähigkeit durch Unfall verursacht
55: result.Append("N;"); //Arbeitunfähigkeitsbescheinigung liegt vor
56: result.Append("N;"); //Fehlzeiten fürs Baugewerbe
57:
58: return result.ToString();
59: }
60: }
Die API wird nun wie folgt genutzt:
1: new GdiApi().
2: SetPersonalnr(iPersonalNr).
3: SetFehlzeitennr(iFehlzeit).
4: SetBeginn(dtStart).
5: SetEnde(dtEnd).
6: SetGanzeTage(bAllDayEvent).
7: GetCsvLine();
Der Vorteil ist, dass der Aufrufer der API genau in dieser Reihenfolge vorgehen muss, um an den benötigten Datensatz zu kommen. Ihm wird genau gesagt wie er Vorgehen muss. Die Logik, wie sich der Datensatz zusammenbaut, ist dem Aufrufer unbekannt. Er weiß beispielsweise nichts von der Berechnung der halben Tage oder den unzähligen zusätzlichen, leeren Feldern.
Warum habe ich nicht einfach alle Interfaces in der ersten Klasse implementiert? Das hätte den Nachteil, dass der Aufrufer zu diesem Zeitpunkt noch alle Methoden gesehen und nicht gewusst hätte, mit welcher er anfangen soll.
Warum habe ich nicht für jedes Interface eine separate Klasse verwendet? Das hätte ich auch machen können, allerdings hätte ich dann die bereits gesetzten Informationen in irgendeiner Form weiter übergeben müssen. Eine Möglichkeit wäre gewesen, ein DTO zu definieren und dieses immer im Konstruktor in die nächste Klasse weiter zu reichen.
Warum habe ich es so gemacht? Weil die Methoden größtenteils nur 2 Zeilen beinhalten und weil eine starke Kohäsion der Daten bzw. Methoden besteht. Vergleicht bitte dazu das Prinzip tell don’t ask.
Zu guter Letzt will ich euch noch auf den Artikel Generische Fluent API mit flexiblen Modulen, den ich soeben noch gefunden habe, aufmerksam machen. Zugegebenermaßen ist er eher advanced und sehr lang, sodass auch ich ihn nicht vollständig durchgelesen habe, aber es ist ein sehr gutes Beispiel.
Gefällt mir:
Like Wird geladen …