Schnittstellen in C # 8: Gefährliche Annahmen in der Standardimplementierung

Hallo Habr!

Im Rahmen der Erforschung des Themas C # 8 empfehlen wir, den folgenden Artikel zu den neuen Regeln für die Implementierung von Schnittstellen zu diskutieren.



Wenn Sie sich die Struktur der Schnittstellen in C # 8 genau ansehen , müssen Sie berücksichtigen, dass Sie bei der Implementierung von Schnittstellen standardmäßig Brennholz beschädigen können.

Annahmen im Zusammenhang mit der Standardimplementierung können zu beschädigtem Code, Laufzeitausnahmen und schlechter Leistung führen.

Eine der aktiv beworbenen Funktionen von C # 8-Schnittstellen besteht darin, dass Sie einer Schnittstelle Mitglieder hinzufügen können, ohne vorhandene Implementierer zu beschädigen. Aber Unaufmerksamkeit ist in diesem Fall mit großen Problemen behaftet. Betrachten Sie den Code, in dem die falschen Annahmen getroffen werden - dies macht klarer, wie wichtig es ist, solche Probleme zu vermeiden.

Der gesamte Code für diesen Artikel ist auf GitHub veröffentlicht: jeremybytes / interfaces-in-csharp-8 , speziell im DangerousAssumptions- Projekt .

Hinweis: In diesem Artikel werden die Funktionen von C # 8 erläutert, die derzeit nur in .NET Core 3.0 implementiert sind. In den von mir verwendeten Beispielen Visual Studio 16.3.0 und .NET Core 3.0.100 .

Annahmen zu Implementierungsdetails

Der Hauptgrund, warum ich dieses Problem artikuliere, ist folgender: Ich habe im Internet einen Artikel gefunden, in dem der Autor Code mit sehr schlechten Annahmen zur Implementierung anbietet (ich werde den Artikel nicht angeben, weil ich nicht möchte, dass der Autor mit Kommentaren aufgerollt wird; ich werde ihn persönlich kontaktieren). .

Der Artikel beschreibt, wie gut die Standardimplementierung ist, da wir damit die Schnittstellen ergänzen können, auch wenn der Code bereits Implementierer enthält. In diesem Code werden jedoch einige schlechte Annahmen getroffen (der Code befindet sich im Ordner BadInterface of in meinem GitHub-Projekt).

Hier ist die ursprüngliche Oberfläche:



Der Rest des Artikels zeigt die Implementierung der MyFile-Schnittstelle (für mich in der Datei MyFile.cs ):

Der Artikel zeigt dann, wie Sie die Rename Methode mit der Standardimplementierung hinzufügen können, ohne dass die vorhandene MyFile Klasse MyFile .

Hier ist die aktualisierte Schnittstelle (aus der Datei IFileHandler.cs ):



MyFile funktioniert immer noch, also ist alles in Ordnung. Also? Nicht wirklich.

Schlechte Annahmen

Das Hauptproblem bei der Umbenennungsmethode besteht darin, dass eine RIESIGE Annahme damit verbunden ist: Implementierungen verwenden eine physische Datei im Dateisystem.

Betrachten Sie die Implementierung, die ich für die Verwendung in einem Dateisystem im RAM erstellt habe. (Hinweis: Dies ist mein Code. Er stammt nicht aus einem Artikel, den ich kritisiere. Die vollständige Implementierung finden Sie in der Datei MemoryStringFileHandler.cs .)



Diese Klasse implementiert ein formales Dateisystem, das ein Wörterbuch im RAM verwendet, das Textdateien enthält. Hier gibt es keine Auswirkungen auf das physische Dateisystem. Im Allgemeinen gibt es keine Verweise auf System.IO .

Fehlerhafter Implementierer

Nach dem Aktualisieren der Schnittstelle ist diese Klasse beschädigt.

Wenn der Clientcode die Umbenennungsmethode aufruft, wird ein Laufzeitfehler generiert (oder, schlimmer noch, die im Dateisystem gespeicherte Datei umbenannt).

Selbst wenn unsere Implementierung mit physischen Dateien funktioniert, kann sie auf Dateien zugreifen, die sich im Cloud-Speicher befinden, und auf solche Dateien kann nicht über System.IO.File zugegriffen werden.

Es gibt auch ein potenzielles Problem beim Testen von Einheiten. Wenn das simulierte oder gefälschte Objekt nicht aktualisiert wird und der getestete Code aktualisiert wird, versucht es, bei der Durchführung von Komponententests auf das Dateisystem zuzugreifen.
Da die falsche Annahme die Schnittstelle betrifft, sind die Implementierer dieser Schnittstelle beschädigt.
Unangemessene Ängste?

Es ist wertlos, solche Befürchtungen für unbegründet zu halten. Wenn ich über Missbräuche im Code spreche, antworten sie mir: "Nun, es ist nur so, dass eine Person nicht weiß, wie man programmiert." Dem kann ich nicht widersprechen.

Normalerweise mache ich das: Ich warte und schaue, wie das funktionieren wird. Ich hatte zum Beispiel Angst, dass die Möglichkeit der "statischen Verwendung" missbraucht würde. Bisher musste dies nicht überzeugt werden.

Es muss bedacht werden, dass solche Ideen in der Luft liegen, daher liegt es in unserer Macht, anderen zu helfen, einen bequemeren Weg einzuschlagen, dessen Befolgung nicht so schmerzhaft sein wird.


Leistungsprobleme

Ich begann darüber nachzudenken, welche anderen Probleme uns erwarten könnten, wenn wir falsche Annahmen über Schnittstellenimplementierer treffen würden.

Im vorherigen Beispiel wird Code aufgerufen, der sich außerhalb der Schnittstelle selbst befindet (in diesem Fall außerhalb von System.IO). Sie werden wahrscheinlich zustimmen, dass solche Aktionen eine gefährliche Glocke sind. Aber wenn wir die Dinge verwenden, die bereits Teil der Benutzeroberfläche sind, sollte alles in Ordnung sein, oder?

Nicht immer.

Als ausdrückliches Beispiel habe ich die IReader-Schnittstelle erstellt.

Die Quellschnittstelle und ihre Implementierung

Hier ist die ursprüngliche IReader-Oberfläche (aus der Datei IReader.cs - obwohl diese Datei jetzt bereits aktualisiert wurde):



Dies ist eine generische Methodenschnittstelle, mit der Sie eine Sammlung schreibgeschützter Elemente abrufen können.

Eine der Implementierungen dieser Schnittstelle generiert eine Folge von Fibonacci-Zahlen (ja, ich habe ein ungesundes Interesse daran, Fibonacci-Folgen zu erzeugen). Hier ist die FibonacciReader Oberfläche (aus der Datei FibonacciReader.cs - sie wird auch auf meinem Github aktualisiert):



Die FibonacciSequence Klasse ist eine Implementierung von IEnumerable <int> (aus der Datei FibonacciSequence.cs). Es wird eine 32-Bit-Ganzzahl als Datentyp verwendet, sodass ein Überlauf recht schnell auftritt.



Wenn Sie an dieser Implementierung interessiert sind, werfen Sie einen Blick auf mein TDDing in eine Fibonacci-Sequenz in C # -Artikel.

Das DangerousAssumptions-Projekt ist eine Konsolenanwendung, die die Ergebnisse von FibonacciReader (aus der Datei Program.cs ) anzeigt:



Und hier ist die Schlussfolgerung:



Aktualisierte Schnittstelle

Jetzt haben wir also den Arbeitscode. Früher oder später müssen wir möglicherweise ein separates Element von IReader abrufen und nicht die gesamte Sammlung auf einmal. Da wir einen generischen Typ für die Schnittstelle verwenden und dennoch nicht die Eigenschaft "natürliche ID" im Objekt haben, erweitern wir das Element, das sich an einem bestimmten Index befindet.

Hier ist unsere Schnittstelle, zu der die GetItemAt Methode GetItemAt (aus der endgültigen Version der Datei IReader.cs ):



GetItemAt hier eine Standardimplementierung voraus. Auf den ersten Blick - nicht so schlimm. Es wird ein vorhandenes Schnittstellenelement ( GetItems ) verwendet, daher werden hier keine "externen" Annahmen getroffen. Mit den Ergebnissen verwendet er die LINQ-Methode. Ich bin ein großer Fan von LINQ, und dieser Code ist meiner Meinung nach vernünftig aufgebaut.

Leistungsunterschiede

Da die Standardimplementierung GetItems , muss die gesamte Sammlung zurückgegeben werden, bevor ein bestimmtes Element ausgewählt wird.

Im Fall von FibonacciReader dies, dass alle Werte generiert werden. In einer aktualisierten Form enthält die Datei Program.cs den folgenden Code:



Also rufen wir GetItemAt . Hier ist die Schlussfolgerung:



Wenn wir einen Prüfpunkt in die Datei FibonacciSequence.cs einfügen, sehen wir, dass die gesamte Sequenz dafür generiert wird.

Nachdem wir das Programm gestartet haben, werden wir zweimal auf diesen Kontrollpunkt GetItems : zuerst beim Aufrufen von GetItems und dann beim Aufrufen von GetItemAt .

Leistungsschädigende Annahme

Das schwerwiegendste Problem bei dieser Methode besteht darin, dass die gesamte Sammlung von Elementen abgerufen werden muss. Wenn dieser IReader es aus der Datenbank entnehmen will, müssen viele Elemente aus der Datenbank herausgezogen werden, und dann wird nur eines davon ausgewählt. Es wäre viel besser, wenn eine solche endgültige Auswahl in einer Datenbank vorgenommen würde.

In Zusammenarbeit mit unserem FibonacciReader berechnen wir jedes neue Element. Daher muss die gesamte Liste vollständig berechnet werden, um nur ein Element zu erhalten, das wir benötigen. Die Fibonacci-Sequenzberechnung ist eine Operation, die den Prozessor nicht zu stark belastet. Was aber, wenn wir uns beispielsweise mit etwas Komplizierterem befassen, berechnen wir Primzahlen?

Sie könnten sagen: „Nun, wir haben eine GetItems Methode, die alles zurückgibt. Wenn es zu lange funktioniert, sollte es wahrscheinlich nicht hier sein. Und das ist eine ehrliche Aussage.

Der aufrufende Code weiß jedoch nichts darüber. Wenn ich GetItems , weiß ich, dass (wahrscheinlich) meine Informationen über das Netzwerk übertragen werden müssen und dieser Prozess GetItems ist. Wenn ich nach einem einzelnen Artikel frage, warum sollte ich dann mit solchen Kosten rechnen?

Spezifische Leistungsoptimierung

Im Fall von FibonacciReader wir unsere eigene Implementierung hinzufügen, um die Leistung erheblich zu verbessern (in der endgültigen Version der Datei FibonacciReader.cs ):



Die GetItemAt Methode überschreibt die in der Schnittstelle bereitgestellte Standardimplementierung.

Hier verwende ich dieselbe LINQ ElementAt Methode wie in der Standardimplementierung. Ich verwende diese Methode jedoch nicht mit der schreibgeschützten Auflistung, die GetItems zurückgibt, sondern mit FibonacciSequence, die IEnumerable .

Da FibonacciSequence IEnumerable , wird der Aufruf von ElementAt beendet, sobald das Programm das von uns ausgewählte Element erreicht. Wir generieren also nicht die gesamte Sammlung, sondern nur die Elemente, die sich bis zur angegebenen Position im Index befinden.

Lassen Sie dazu den oben angegebenen Kontrollpunkt in der Anwendung und führen Sie die Anwendung erneut aus. Diesmal GetItems wir nur einmal auf einen Haltepunkt (beim Aufrufen von GetItems ). Beim Aufruf von GetItemAt dies nicht passieren.

Ein leicht erfundenes Beispiel

Dieses Beispiel ist etwas weit hergeholt, da Sie in der Regel keine Elemente aus dem Datensatz nach Index auswählen müssen. Sie können sich jedoch etwas Ähnliches vorstellen, das passieren könnte, wenn wir mit der natürlichen ID-Eigenschaft arbeiten würden.

Wenn wir Elemente nach ID und nicht nach Index abgerufen haben, sind möglicherweise dieselben Leistungsprobleme bei der Standardimplementierung aufgetreten. Die Standardimplementierung erfordert die Rückgabe aller Elemente, wonach nur eines aus ihnen ausgewählt wird. Wenn Sie der Datenbank oder einem anderen „Leser“ erlauben, ein bestimmtes Element anhand seiner ID abzurufen, wäre eine solche Operation viel effizienter.

Denken Sie über Ihre Annahmen nach

Annahmen sind unabdingbar. Wenn wir versuchen würden, mögliche Anwendungsfälle unserer Bibliotheken im Code zu berücksichtigen, würde keine Aufgabe jemals abgeschlossen werden. Sie müssen jedoch die Annahmen im Code sorgfältig prüfen.

Dies bedeutet nicht, dass die GetElementAt unbedingt fehlerhaft ist. Ja, es gibt potenzielle Leistungsprobleme. Wenn die Datensätze jedoch klein oder die berechneten Elemente „billig“ sind, kann die Standardimplementierung ein vernünftiger Kompromiss sein.

Ich freue mich jedoch nicht über Änderungen an der Schnittstelle, nachdem sie bereits Implementierer hat. Ich verstehe jedoch, dass es auch solche Szenarien gibt, in denen alternative Optionen bevorzugt werden. Programmierung ist die Lösung von Problemen, und bei der Lösung von Problemen müssen die Vor- und Nachteile der einzelnen von uns verwendeten Tools und Ansätze abgewogen werden.

Die Standardimplementierung kann möglicherweise Schnittstellenimplementierern (und möglicherweise dem Code, der diese Implementierungen aufruft) schaden. Daher müssen Sie besonders vorsichtig mit Annahmen sein, die sich auf Standardimplementierungen beziehen.

Viel Glück bei Ihrer Arbeit!

Source: https://habr.com/ru/post/de471872/


All Articles