Visual C# 2012 Grundkurs

Konkretes Projekt

LinkedIn Learning kostenlos und unverbindlich testen!

Jetzt testen Alle Abonnements anzeigen
Dieses Video gibt anhand eines konkreten Projekts einige Anregungen für das Arbeiten mit abstrakten, objektorientierten Konzepten wie Kapselung, Vererbung und Interfaces in der Praxis.

Transkript

In diesem Abschnitt möchte ich mit Ihnen zusammen einen Blick auf ein konkretes Projekt werfen. Die objektorientierten Konzepte der Kapselung, Vererbung der Interfaces, sind sehr abstrakt. Sie mögen sich daher fragen, wie Sie denn mit diesen Konzepten in der Praxis arbeiten sollen. Dazu möchte ich ein paar Anregungen geben. Sie werden sehen, dass durch die geschickte Aufteilung von Aufgaben auf verschiedene Klassen und durch die konsequente Kapselung der Programmteile durch Interfaces eine übersichtliche Programmstruktur entsteht, die im Übrigen auch sehr gut testbar ist. Ich werde zeigen, dass Programmteile austauschbar werden, wenn man sie durch Interfaces voneinander abgrenzt, und dass diese Austauschbarkeit sehr gut zum Testen geeignet ist, weil man jetzt mithilfe von sogenannten "Mocks", also M O C K geschrieben, Implementierungen von Interfaces erzeugen kann, die in Testszenarien verwendet werden können. Sie tauschen also einfach Teile Ihrer Applikation gegen Mocks aus, und können dadurch in Ihren Testszenarien den Test auf ganz bestimmte Programmteile eingrenzen. Bei diesem Programm, das ich Ihnen zeige, handelt es sich um einen Abgleich-Service, den ich in der Praxis auch so ähnlich geschrieben habe. Der Abgleich-Service ist ein Programm, das mit einem Webservice zusammenarbeitet. Von diesem Webservice erhält der Abgleich-Service aktualisierte Versionen von Dokumenten und Datensätzen, die in einer Datenbank liegen. Dokumente wären in dem Fall Dateien, und die Datensätze liegen in der Datenbank. Der Abgleich-Service fragt jetzt bei diesem Webservice an, ob es irgendwelche Änderungen gibt. Wenn der Webservice das bejaht, dann werden diese Änderungen abgerufen, die Dokumente komplett abgerufen, und dann an der richtigen Stelle gespeichert. Wenn es sich bei dem Dokument um eine Datei handelt, dann wird es im Dateiverzeichnis gespeichert an der richtigen Stelle, und wenn es sich um einen Datensatz handelt, dann kommt der eben in eine lokale Datenbank, die hier die "Cache-Datenbank" genannt wird, weil sie letztendlich ein Cache für eine Applikation ist. Tatsächlich ist es so, dass der Abgleich-Service Bestandteil einer Webanwendung ist, die sich auf diese Art und Weise Daten holt. Diese Daten werden dann umgesetzt in Dokumente, die in der Webapplikation angezeigt werden. Wenn es nun in der Zwischenzeit Änderungen an der Struktur der Datenbank gegeben hat, also z. B. eine Tabelle hat irgendeine Spalte mehr, dann ist der Service in der Lage, mithilfe von Informationen des Webservices die Struktur der Datenbank anzupassen, bevor die neuen Datensätze gespeichert werden. Also der Service ruft beim Webservice die gegenwärtige Struktur der Quell-Datenbank ab, vergleicht diese mit der Struktur der Cache-Datenbank, und wenn es da irgendwelche Differenzen gibt, werden diese Differenzen ausgeglichen. Vielleicht mögen Sie meinen, dass so ein Abgleich-Service ein ziemlich komplexes und unübersichtliches Programm ist. Aber das ist nicht der Fall. In der Tat ist es so, dass diese wenigen Zeilen, die Sie hier sehen, im Grunde genommen, die gesamte Funktionalität beschreiben von diesem Abgleich-Service. Dann wollen wir doch mal gucken, was darin passiert. Was ich tue ist, dass ich zunächst von diesem Webservice, von diesem InformationService die gegenwärtige Zeit abrufe. Jetzt fragt man sich natürlich: Was ist dieser InformationService hier? Man sieht, dieser InformationService kommt über den Konstruktor dieser Klasse "AbgleichPerformer", das ist die Klasse, die diesen Abgleich durchführt. Über diesen Konstruktor kommen diese InformationServices da rein. Man sieht es kommen noch ein paar andere Sachen hier rein, die bestimmte Aufgaben übernehmen können. In dem Fall will ich nur mal auf diesen InformationService eingehen. Das ist nämlich der eigentliche Webservice. Webservices werden als "Interfaces" definiert. Wenn man also in Visual Studio eine Referenz auf so einem Webservice erzeugt, dann erzeugt einem Visual Studio ein Interface, das dann in der Lage ist, dessen Implementierung in der Lage ist, mit dem Webservice zu kommunizieren. Dieses Interface benutzt der Client dann. Und genau um dieses Interface handelt es sich, das bekomme ich hier rein. Der Webservice hatte diese Funktion "GetCurrentDateTime". Diese Zeit rufe ich jetzt ab, kann diese Zeit später lokal speichern, und das dann als die Zeit des letzten Updates abrufen, das heißt, dieser ganze Abgleich-Service arbeitet mit der Serverzeit. Was ich hier tue ist, ich besorge mir die Update-Zeit des letzten Abgleichs und frage dann den InformationService: "Gib mir alle Änderungen seit dieser letzten Zeit, seit diesem letzten Update". Diese Änderungen kommen als ein String Array zurück, und jeder dieser Strings ist eine sogenannte "Uri" — eine "unified resource ID". Es ist also eine ID einer Ressource. Jetzt muss man sich ein System überlegen über die Logik, was so eine Uri bedeutet. Das ist aber für uns im Augenblick nicht von Belang. Wenn es also irgendwelche Uris in diesem Array gibt, müssen wir einen Abgleich vornehmen, und wenn nicht — das ist dann wahrscheinlich relativ häufig der Fall, dass es keine Änderungen gibt im Datenbestand, sind wir mit der Replikation schon wieder am Ende. Jetzt stoße ich erst einmal den Update der Datenbankstruktur an. Das wird jetzt nicht in meinem Code selber gemacht, sondern ich habe eine Klasse, die ich damit beauftrage. Ich sage: "Liebe Cache-Database, mach das mal für mich". Dann kann ich jede einzelne dieser Uris durchlaufen, und dafür den Update machen. Auch hier wird das nicht von dem Programmcode, den Sie hier sehen selbst erzeugt, sondern auch hier gibt es jemanden, an den ich diese Aufgabe delegieren kann. Das ist ein sogenannter ResourceUpdater. Dieser ResourceUpdater macht also genau dieses konkrete Ressourcen-Update. Also wenn ein Dokument rüberkommt, wird das im Dateiverzeichnis gespeichert. Wenn ein XML-Stream rüberkommt, wird der umgewandelt in einen Datensatz, und dieser Datensatz wird dann in der Datenbank gespeichert. Alle Änderungen an Datensätzen werden zunächst einmal in einem Speicherabbild der Datenbank gespeichert, die durch diese Klasse "CacheDatabase" repräsentiert wird. Der sage ich dann, die soll das speichern. Dann wird die "currentTime", die Zeit, die ich vom InformationService abgerufen habe, in der ersten Zeile als "LastUpdateTime" gesetzt. Ganz am Ende, hier sind mit diesem Code alle Daten schon mal upgedatet worden, kann ich mich darum kümmern, dass aus diesen neuen Informationen neue Content-Seiten erzeugt werden in diesem Web-Projekt. Das ist der gesamte Code, die gesamte Logik dieses Abgleichs, die hier sehr übersichtlich vorliegt. Alle Detailaufgaben werden nun, wie gesagt, von so Dingen getan wie DiffGenerator, ContentRenderer, CacheDatabase usw. Der DiffGenerator z. B. nimmt zwei Data-Sets und vergleicht die miteinander. Ein "Data-Set" ist eine Klasse aus dem .NET-Framework, die kann Beschreibungen von Datentabellen enthalten. Und daher ist so ein Data-Set ganz gut geeignet, die Struktur von Datenbanken darzustellen. Und im Fall des DiffGenerators werden einfach 2 so Data-Sets genommen, die eben 2 Strukturen von Datenbanken gespeichert haben. Und überall da, wo Unterschiede auftauchen, erzeugt der DiffGenerator SQL-Befehle, die dann die Struktur der Cache-Datenbank an die Struktur der Quelldatenbank anpassen, auf die der Webservice zugreift. Das Interessante an der Geschichte ist, dass ich in meinem Code nicht die Objekteder beteiligten Klassen selber anlege, sondern ich bekomme diese Objekte über den Konstruktor hereingereicht. Das ist eine sehr gute Idee, weil meine Applikation braucht sich nicht darum zu kümmern, von welcher Art diese Objekte sind und wie sie angelegt werden. Mein Code benutzt sie einfach. Wie man sieht: Jedes dieser Objekte, das hier reinkommt, ist nicht von einem konkreten Typ, sondern alle sind von einem Interface-Typ. Das sieht man an diesem vorangestellten, großen "I". Deswegen ist es immer eine gute Idee, die Namen von Interfaces mit diesem großen "I" am Anfang zu kennzeichnen. Dann sieht man nämlich mit einem Blick, dass ein bestimmter Typname ein Interface bezeichnet, und nicht eine Klasse. Ich habe mir also die Arbeit gemacht, für alle Klassen, die hier im System Aufgaben übernehmen, Interfaces zu definieren, diese Interfaces in die Klassen dann zu implementieren. Hier sieht man das: Ich hab verschiedene Interfaces hier geschrieben, die sind mal größer und mal kleiner, also in dem Fall. Einige der Interfaces tun nur relativ wenig Dinge, und einige haben ein paar Methoden mehr. Die eigentliche Arbeit wird dann in den verschiedenen Klassen gemacht. Ich gehe jetzt in diesen RessourceUpdater rein. Der RessourceUpdater nimmt da jetzt einfach so einen Uri-String, und muss jetzt mal zusehen, wie er diesen Uri-String, wie er dafür irgendwie einen Abgleich gebacken bekommt. Wir sehen, das ist diese Methode "UpdateRessource", die der AbgleichPerformer aufgerufen hat, eben unter Übergabe dieses Uri-Strings. Und wir können sehen, wie dieser RessourceUpdater seinen Job erledigt. Der RessourceUpdater seinerseits ist so ähnlich aufgebaut wie der AbgleichPerformer auf einer Ebene höher. Auch hier werden bestimmte Beteiligte als Interfaces rein gereicht, und diese Beteiligten tun jetzt hier herinnen ihren Job. Deswegen sieht auch dieser RessourceUpdater ziemlich übersichtlich aus. Es wird jetzt erst mal so geguckt: Fängt diese Uri mit einem bestimmten Präfix an? Der fängt dann entweder mit "string" oder "file" an. Dann wissen wir schon mal, wenn es mit "string" anfängt, ist es ein Datensatz. Ist es ein XML-String, aus dem Daten herausgezogen werden und in der Datenbank gespeichert werden? Wenn der Präfix mit "file" anfängt, ist es eine Datei, also das heißt das, was dann rüberkommt von dem Service vom Webservice, speichere ich irgendwo im Dateisystem als Datei ab. Und genau das tue ich dann hier: "UpdateString", "UpdateFile". Man sieht, wenn es ein String ist, dann hole ich mir von diesem InformationService mit dieser Methode "GetDocument" dieses entsprechende Dokument, das zu dieser Uri gehört. Das ist in dem Fall, was da zurückkommt, ist nur ein Byte-Array, einfach nur eine Sammlung an Bytes. Das sehen Sie in diesem grauen IntelliSense, in dieser Information hier unterhalb des Cursors. Ich lasse es mal kurz aufblinken noch mal, dass das ein Byte-Array ist, was da zurückkommt. Und was ich eben mache ist, ich ziehe hier XML raus, und dann, wenn ich dieses XML von diesem Element habe, nehme ich dieses CacheDatabase-Objekt, das meine Cache-Datenbank repräsentiert, sage: "Pass auf, ich habe hier ein Stück XML, mach damit ein Update von einem bestimmten Element!" Nun ziehe ich aus dieser Uri einen n-Detailnamen. Das ist ein Name, mit dem dann diese Cache-Database umgehen kann und entsprechend eine Tabelle raussucht, in der dann diese Informationen gespeichert werden können. Auch hier delegiere ich wieder die Aufgabe an die Cache-Datenbank. Und weiter unten hier, das UpdateFile besteht gerade nur aus zwei Zeilen. Auch hier hole ich mit "GetDocument" von diesem InformationService wieder dieses Byte-Array zurück, delegiere die ganze Aufgabe an einen Dateimanager. Ich sage dem: "Hier gibt es eine Datei zum Updaten. Das ist die Uri und das ist mein Byte-Array und jetzt tu mal!" Und so zieht sich die Delegierung von Aufgaben durch das ganze System. Und damit wird der Code für die einzelne Aufgabe immer nur relativ klein und relativ übersichtlich. Der größte Code, den man wohl braucht, das wäre so in dieser Cache-Datenbank der Update der Struktur. Aber wenn man mal so sieht, arg viel ist hier auch wieder nicht los. Ich suche mal den Update des Elements. Hier passiert dann schon ein bisschen mehr. Ein konkreter Datensatz, um den upzudaten, das ist jetzt der ganze Code, den ich dafür benutze, der ist etwa 40 Zeilen lang. Das ist noch eine übersichtliche Angelegenheit. Das ganze System läuft eingebettet in eine Web-Applikation. Da ist dieser ContentRenderer dabei, der eben auf das API der Web-Applikation zugreift, und dann irgendwelche Seiten erzeugen muss. Dann habe ich die Cache-Datenbank, die auch zu dem Kontext gehört. Da brauche ich eine Datenbank-Connection dazu, und muss eine Verbindung zu dieser Datenbank herstellen können usw. Da stellt sich die Frage: "Wie testet man das ganze überhaupt?" Indem ich jetzt diese ganzen Klassen, die hier die Arbeit tun, durch Interfaces abgekapselt habe, kann ich es jederzeit, einen bestimmten Teil der Funktionalität, die ich hier implementieren will, testen, und andere Teile der Funktionalität ersetzen durch Testinstanzen. Wie so etwas geht, das habe ich hier mal kurz skizziert. In der Solution von diesem Abgleichservice habe ich ein weiteres Projekt angelegt. Das ist ein Testprojekt. Bestimmte Versionen von Visual Studio haben die Möglichkeit, so Testprojekte anzulegen, und das ist was unheimlich Elegantes. Um so ein Testprojekt anzulegen, klickt man mit der rechten Maustaste auf die Solution, sagt "Add new project". Dann hat man verschiedene Möglichkeiten, aus denen man auswählen kann. Und hier gibt es Testprojekte. Da habe ich jetzt ein Unit-Testprojekt ausgewählt und diesen Abgleichtest als Unit-Testprojekt angelegt. Das Visual Studio erstellt dann gleich einen Rahmen so einer Testklasse, und innerhalb dieses Rahmens kann ich jetzt Testmethoden einsetzen. Und jetzt gibt es was ganz was Elegantes. Das ist das Moq-Framework Moq wird M O Q geschrieben. Gehen Sie einfach ins Internet, googeln Sie danach, nach diesem Moq, mit Q hinten geschrieben. Dieses Framework ist in der Lage, Objekte herzustellen von jedem beliebigen Interface. Hier habe ich das mal mit diesem Diff-Generator gemacht. Ich muss ja nicht in jedem Kontext, einen funktionierenden Diff-Generator haben. Ich kann mir einfach durch dieses Moq-Framework eine Implementierung dieses Interfaces, "IDiffGenerator", erzeugen lassen. Ich möchte jetzt nicht so sehr auf das eingehen, was hier steht. Das ist eine sogenannte Lambda Expression. Es ist so, wenn Sie dieses Moq-Framework heruntergeladen haben, schauen Sie sich in Ruhe die Dokumentation an, dann wird sich das erhellen, was man tun muss, um so einen Mock herzustellen. Tatsache aber ist, dieses Interface "IDiffGenerator" hat eine Methode "Generate". Diese Methode "Generate", für die kann ich eine Testimplementierung erstellen. Und zwar kann ich sagen: "Okay, in diesem speziellen Mock von diesem IDiffGenerator, da möchte ich die Methode "Generate" so implementieren, dass sie den Wert "TestString" zurückgibt, wenn sie aufgerufen wird." Nun kann mir dieser Mock von IDiffGenerator eine Implementierung von IDiffGenerator erzeugen, die ich jetzt verwenden kann. Genau das habe ich gemacht. Ich habe einfach zwei leere Data-Sätze erzeugt, und ich rufe von diesem IDiffGenerator die Methode "Generate" auf. Sie werden gleich sehen, dass dann der Test-String zurückkommt. Hier habe ich im Visual Studio so einen Test-Explorer, da ist diese Testmethode gelistet. Ich kann jetzt mit dem Kontextmenü "Debug Selected Tests" aufrufen, dann startet dieser Test im Debugger. Es dauert ein bisschen, bis wir da beim Breakpoint angekommen sind, und siehe da, dieser String enthält jetzt genau diesen Test-String. Jetzt ist die Frage: "Was kann ich mit denen tun?" Um das zu verstehen, müssen wir noch einmal zurück zu diesem AbgleichPerformer. Ich habe diese Klasse "AbgleichPerformer" angelegt, und dieser AbgleichPerformer bekommt jetzt in seinem Konstruktor lauter Implementierungen von Interfaces. Eigentlich ist es diesem AbgleichPerformer völlig egal, ob er eine tatsächliche Implementierung des Interfaces erhält, also eine dieser Klassen, die ich hier beschrieben habe, oder ob der AbgleichPerformer einen Mock erhält. Entsprechend kann ich hergehen, in so einem Unit-Test, und könnte jetzt sagen: "Abgleichservice.AbgleichPerfomer ap = new Abgleichservice.AbgleichPerfomer". Jetzt kann ich ihm hier hereinreichen, was auch immer ich lustig bin. Z. B. als Diff-Generator kann ich hier meinen Mock rein. Beim Diff-Generator macht das vielleicht gar nicht so viel Sinn. Viel mehr Sinn macht es z. B. bei der Cache-Database, da spare ich mir nämlich dann den gesamten Datenbankzugriff. Und bei diesem ContentRenderer spare ich mir dann den Zugriff auf das API von diesem Webservice. Auch bei diesem File-Manager, dessen Implementierung sich aus dem "HttpContext", in dem die Webapplikation läuft, irgendein Verzeichnis herauszieht, wo er nämlich dann die Dateien hin speichern muss. All das kann ich jetzt sozusagen "wegmocken", indem ich diese Implementierung durch einen Mock ersetze. Deswegen ist es eine gute Idee, überall da, wo Sie glauben, eine bestimmte Tätigkeit könnten Sie jetzt einfach durch eine Methode Ihrer Klasse ausführen, sich vielleicht mal kurz zurückzulehnen, zu sagen: "Mensch, wäre das nicht vielleicht ein Fall?" Ich geh noch mal zurück zu diesem AbgleichPerformer. Wenn ich jetzt z. B. diese Struktur update, dass ich das nicht mit einer Methode in der gleichen Klasse "AbgleichPerformer" mache, sondern dass ich sage, "Mensch, gibt es da nicht vielleicht die Möglichkeit, diesen ganzen Vorgang dieses Struktur-Updates so zu delegieren, dass es am Ende ein austauschbares System gibt, sodass ich dadurch eine höhere Testbarkeit meines Systems erreiche?" Ich hoffe, ich habe Ihnen damit einen kleinen Eindruck verschaffen können, von dem, wie Software-Systeme mithilfe von Interfaces zusammengebaut werden, und dass man immer darauf achten muss, dass man möglichst austauschbare Strukturen schafft, damit eben die einzelnen Einheiten für sich testbar werden.

Visual C# 2012 Grundkurs

Schreiben Sie eigene Programme in C# und lernen Sie dazu alle Schlüsselwörter und die meisten Konstrukte kennen, um sicher mit dieser Programmierspreche umzugehen.

7 Std. 1 min (44 Videos)
Derzeit sind keine Feedbacks vorhanden...
 

Dieser Online-Kurs ist als Download und als Streaming-Video verfügbar. Die gute Nachricht: Sie müssen sich nicht entscheiden - sobald Sie das Training erwerben, erhalten Sie Zugang zu beiden Optionen!

Der Download ermöglicht Ihnen die Offline-Nutzung des Trainings und bietet die Vorteile einer benutzerfreundlichen Abspielumgebung. Wenn Sie an verschiedenen Computern arbeiten, oder nicht den ganzen Kurs auf einmal herunterladen möchten, loggen Sie sich auf dieser Seite ein, um alle Videos des Trainings als Streaming-Video anzusehen.

Wir hoffen, dass Sie viel Freude und Erfolg mit diesem Video-Training haben werden. Falls Sie irgendwelche Fragen haben, zögern Sie nicht uns zu kontaktieren!