Verwenden Sie abstrakte Klasse in C # als Definition

21

Als C ++ - Entwickler bin ich an C ++ - Headerdateien gewöhnt und finde es vorteilhaft, eine Art erzwungene "Dokumentation" im Code zu haben. Normalerweise habe ich eine schlechte Zeit, wenn ich etwas C # -Code lesen muss: Ich habe keine solche mentale Karte der Klasse, mit der ich arbeite.

Nehmen wir an, dass ich als Softwareentwickler das Framework eines Programms entwerfe. Wäre es zu verrückt, jede Klasse als abstrakte, nicht implementierte Klasse zu definieren, ähnlich wie wir es mit C ++ - Headern machen würden, und es den Entwicklern überlassen, sie zu implementieren?

Ich vermute, dass es einige Gründe gibt, warum dies für jemanden eine schreckliche Lösung sein könnte, aber ich bin mir nicht sicher, warum. Was müsste man für eine solche Lösung beachten?

Dani Barca Casafont
quelle
13
C # ist eine völlig andere Programmiersprache als C ++. Manche Syntax sieht ähnlich aus, aber Sie müssen C # verstehen, um gute Arbeit zu leisten. Und Sie sollten etwas gegen die schlechte Angewohnheit unternehmen, sich auf Header-Dateien zu verlassen. Ich habe jahrzehntelang mit C ++ gearbeitet, die Header-Dateien nie gelesen, sondern nur geschrieben.
Bent
17
Eines der besten Dinge von C # und Java ist die Freiheit von Header-Dateien! Verwenden Sie einfach eine gute IDE.
Erik Eidt
9
Deklarationen in C ++ - Headerdateien sind nicht Teil der generierten Binärdatei. Sie sind für den Compiler und Linker da. Diese abstrakten C # -Klassen wären Teil des generierten Codes, ohne dass dies irgendwelche Vorteile hätte.
Mark Benningfield
16
Das äquivalente Konstrukt in C # ist eine Schnittstelle, keine abstrakte Klasse.
Robert Harvey
6
@DaniBarcaCasafont "Ich sehe Header als eine schnelle und zuverlässige Methode, um zu sehen, was eine Klasse für Methoden und Attribute sind, welche Parametertypen sie erwartet und was sie zurückgibt." Wenn ich Visual Studio verwende, ist meine Alternative die Tastenkombination Strg-MO.
immer

Antworten:

44

Der Grund, warum dies in C ++ geschah, bestand darin, Compiler schneller und einfacher zu implementieren. Dies war kein Entwurf, um die Programmierung zu vereinfachen.

Der Zweck der Header-Dateien bestand darin, es dem Compiler zu ermöglichen, einen superschnellen ersten Durchlauf durchzuführen, um alle erwarteten Funktionsnamen zu kennen und ihnen Speicherorte zuzuweisen, damit sie beim Aufruf in cp-Dateien referenziert werden können, selbst wenn die Klasse, die sie definiert, diese hat wurde noch nicht analysiert.

Der Versuch, eine Folge alter Hardwareeinschränkungen in einer modernen Entwicklungsumgebung zu replizieren, wird nicht empfohlen!

Das Definieren einer Schnittstelle oder einer abstrakten Klasse für jede Klasse senkt Ihre Produktivität. Was hättest du sonst mit dieser Zeit anfangen können ? Auch andere Entwickler werden dieser Konvention nicht folgen.

Tatsächlich können andere Entwickler Ihre abstrakten Klassen löschen. Wenn ich in Code eine Schnittstelle finde, die diesen beiden Kriterien entspricht, lösche ich sie und überarbeite sie aus dem Code heraus:1. Does not conform to the interface segregation principle 2. Only has one class that inherits from it

Die andere Sache ist, dass Visual Studio Tools enthält, die das tun, was Sie automatisch erreichen möchten:

  1. Das Class View
  2. Das Object Browser
  3. In Solution Explorerkönnen Sie auf Dreiecke klicken, um Klassen zu belegen und deren Funktionen, Parameter und Rückgabetypen anzuzeigen.
  4. Unterhalb der Dateiregisterkarten befinden sich drei kleine Dropdown-Menüs, das rechte listet alle Mitglieder der aktuellen Klasse auf.

Probieren Sie eines der oben genannten Verfahren aus, bevor Sie sich mit der Replikation von C ++ - Headerdateien in C # befassen.


Darüber hinaus gibt es technische Gründe, dies nicht zu tun. Dadurch wird Ihre endgültige Binärdatei größer, als es sein muss. Ich werde diesen Kommentar von Mark Benningfield wiederholen:

Deklarationen in C ++ - Headerdateien sind nicht Teil der generierten Binärdatei. Sie sind für den Compiler und Linker da. Diese abstrakten C # -Klassen wären Teil des generierten Codes, ohne dass dies irgendwelche Vorteile hätte.


Auch, wie von Robert Harvey erwähnt, wäre das technisch nächstliegende Äquivalent eines Headers in C # eine Schnittstelle, keine abstrakte Klasse.

TheCatWhisperer
quelle
2
Sie würden auch mit vielen unnötigen virtuellen Versandanrufen enden (nicht gut, wenn Ihr Code leistungsabhängig ist).
Lucas Trzesniewski
5
hier wird auf das Prinzip der Schnittstellensegregation Bezug genommen.
NH.
2
Man kann auch einfach die gesamte Klasse reduzieren (Rechtsklick -> Gliederung -> Auf Definitionen reduzieren), um sie aus der Vogelperspektive zu betrachten.
jpmc26
"was hättest du sonst mit dieser zeit machen können" -> ähm, kopieren und einfügen und sehr wirklich kleine postarbeiten? Ich brauche ungefähr 3 Sekunden, wenn überhaupt. Natürlich hätte ich diese Zeit nutzen können, um eine Filterspitze herauszunehmen, um mich auf das Ausrollen einer Zigarette vorzubereiten. Nicht wirklich ein relevanter Punkt. [Haftungsausschluss: Ich liebe sowohl idiomatisches C # als auch idiomatisches C ++ und einige weitere Sprachen]
phresnel
1
@phresnel: Ich würde gerne sehen, dass Sie versuchen, die Definitionen einer Klasse zu wiederholen und die ursprüngliche Klasse von der abstrakten Klasse in 3 Sekunden zu erben, die ohne Fehler bestätigt wurde, für jede Klasse, die groß genug ist, um keine einfache Vogelperspektive zu haben it (da dies der vom OP vorgeschlagene Anwendungsfall ist: vermeintlich unkomplizierte, ansonsten komplizierte Klassen). Fast inhärent bedeutet die Komplexität der ursprünglichen Klasse, dass Sie die abstrakte Klassendefinition nicht einfach auswendig schreiben können (da dies bedeuten würde, dass Sie sie bereits auswendig kennen) und dann die dafür erforderliche Zeit für eine gesamte Codebasis in Betracht ziehen .
Flater
14

Verstehen Sie zunächst, dass eine rein abstrakte Klasse eigentlich nur eine Schnittstelle ist, die keine Mehrfachvererbung durchführen kann.

Schreibe Klasse, extrahiere Schnittstelle, ist eine gehirntote Aktivität. So sehr, dass wir ein Refactoring dafür haben. Welches ist schade. Diesem Muster "Jede Klasse bekommt eine Schnittstelle" zu folgen, erzeugt nicht nur Unordnung, sondern verfehlt den Punkt vollständig.

Eine Schnittstelle sollte nicht einfach als formale Neuformulierung dessen betrachtet werden, was die Klasse tun kann. Eine Schnittstelle sollte als Vertrag betrachtet werden, der durch die Verwendung von Client-Code auferlegt wird, in dem die Anforderungen aufgeführt sind.

Ich habe überhaupt keine Probleme damit, ein Interface zu schreiben, das derzeit nur von einer Klasse implementiert wird. Es ist mir eigentlich egal, ob es noch keine Klasse gibt, die das umsetzt. Weil ich darüber nachdenke, was mein Code braucht. Die Schnittstelle drückt aus, was der Verwendungscode erfordert. Was später kommt, kann machen, was es will, solange es diese Erwartungen erfüllt.

Jetzt mache ich das nicht jedes Mal, wenn ein Objekt ein anderes verwendet. Ich mache das, wenn ich eine Grenze überschreite. Ich mache es, wenn ein Objekt nicht genau wissen soll, mit welchem ​​anderen Objekt es spricht. Nur so kann Polymorphismus funktionieren. Ich mache es, wenn ich erwarte, dass sich das Objekt, von dem mein Client-Code spricht, wahrscheinlich ändert. Ich mache das auf keinen Fall, wenn ich die String-Klasse verwende. Die String-Klasse ist nett und stabil und ich habe nicht das Bedürfnis, mich davor zu hüten, dass sie sich auf mich auswirkt.

Wenn Sie sich entscheiden, direkt mit einer konkreten Implementierung zu interagieren, und nicht über eine Abstraktion, gehen Sie davon aus, dass die Implementierung stabil genug ist, um darauf zu vertrauen, dass sie sich nicht ändert.

Genau so tempere ich das Prinzip der Abhängigkeitsinversion . Sie sollten dies nicht blind fanatisch auf alles anwenden. Wenn Sie eine Abstraktion hinzufügen, sagen Sie wirklich, dass Sie nicht darauf vertrauen, dass die implementierende Klasse über die gesamte Laufzeit des Projekts stabil bleibt.

Dies alles setzt voraus, dass Sie versuchen, dem Open Closed-Prinzip zu folgen . Dieses Prinzip ist nur dann wichtig, wenn die Kosten für direkte Änderungen am etablierten Code erheblich sind. Einer der Hauptgründe, warum sich die Leute nicht darüber einig sind, wie wichtig das Entkoppeln von Objekten ist, ist, dass nicht jeder bei direkten Änderungen die gleichen Kosten verursacht. Wenn das erneute Testen, Neukompilieren und Verteilen Ihrer gesamten Codebasis für Sie trivial ist, ist das Lösen eines Änderungsbedarfs durch direkte Änderung wahrscheinlich eine sehr attraktive Vereinfachung dieses Problems.

Es gibt einfach keine gehirntote Antwort auf diese Frage. Eine Schnittstelle oder eine abstrakte Klasse sollten Sie nicht jeder Klasse hinzufügen, und Sie können nicht einfach die Anzahl der implementierenden Klassen zählen und entscheiden, dass sie nicht benötigt werden. Es geht um den Umgang mit Veränderung. Was bedeutet, dass Sie die Zukunft vorwegnehmen. Sei nicht überrascht, wenn du etwas falsch machst. Halte es so einfach wie möglich, ohne dich in eine Ecke zurückzulehnen.

Schreiben Sie also bitte keine Abstraktionen, um uns beim Lesen des Codes zu helfen. Wir haben Werkzeuge dafür. Verwenden Sie Abstraktionen, um zu entkoppeln, was entkoppelt werden muss.

kandierte_orange
quelle
5

Ja, das wäre schrecklich, weil (1) es unnötigen Code einführt. (2) es den Leser verwirren wird.

Wenn Sie in C # programmieren möchten, sollten Sie sich nur daran gewöhnen, C # zu lesen. Code, der von anderen Leuten geschrieben wurde, folgt sowieso nicht diesem Muster.

JacquesB
quelle
1

Unit-Tests

Ich empfehle dringend, Unit-Tests zu schreiben, anstatt den Code mit Interfaces oder abstrakten Klassen zu überfrachten (außer wenn dies aus anderen Gründen gerechtfertigt ist).

Ein gut geschriebener Komponententest beschreibt nicht nur die Schnittstelle Ihrer Klasse (wie es eine Header-Datei, eine abstrakte Klasse oder eine Schnittstelle tun würde), sondern beschreibt auch beispielhaft die gewünschte Funktionalität.

Beispiel: Wo Sie möglicherweise eine Header-Datei myclass.h geschrieben haben:

class MyClass
{
public:
  void foo();
};

Schreiben Sie stattdessen in c # einen Test wie diesen:

[TestClass]
public class MyClassTests
{
    [TestMethod]
    public void MyClass_should_have_method_Foo()
    {
        //Arrange
        var myClass = new MyClass();
        //Act
        myClass.Foo();
        //Verify
        Assert.Inconclusive("TODO: Write a more detailed test");
    }
}

Dieser sehr einfache Test vermittelt die gleichen Informationen wie die Header-Datei. (Wir sollten eine Klasse namens "MyClass" mit einer parameterlosen Funktion "Foo" haben.) Während eine Header-Datei kompakter ist, enthält der Test weitaus mehr Informationen.

Eine Einschränkung: Solch ein Prozess, bei dem ein leitender Softwareentwickler andere Entwickler mit (fehlgeschlagenen) Tests beauftragen muss, Konflikte mit Methoden wie TDD gewaltsam zu lösen, wäre in Ihrem Fall jedoch eine enorme Verbesserung.

Guran
quelle
Wie macht man Unit Testing (Einzeltests) = Mocks benötigt, ohne Schnittstellen? Welches Framework das Verspotten von einer tatsächlichen Implementierung unterstützt, habe ich am häufigsten anhand einer Schnittstelle gesehen, um die Implementierung durch die gespielte Implementierung auszutauschen.
Bulan
Ich denke nicht, dass für die Zwecke des OP unbedingt Spott benötigt wird. Denken Sie daran, dies sind keine Unit-Tests als Tools für TDD, sondern Unit-Tests als Ersatz für Header-Dateien. (Sie könnten sich natürlich zu "normalen" Unit-Tests mit Mocks usw. entwickeln.)
Guran
Ok, wenn ich nur die OP Seite betrachte, verstehe ich es besser. Lesen Sie Ihre Antwort als allgemein zutreffende Antwort. Danke für die Klarstellung!
Bulan
@Bulan die Notwendigkeit einer umfassenden Verspottung ist oft ein Hinweis auf ein schlechtes Design
TheCatWhisperer
@TheCatWhisperer Ja, das ist mir sehr wohl bewusst, daher kann kein Argument erkennen, dass ich diese Aussage irgendwo gemacht habe: DI sprach davon, generell zu testen, dass alle Mock-Frameworks, die ich verwendet habe, Interfaces zum Tauschen verwenden Informieren Sie sich über die tatsächliche Implementierung und darüber, ob es eine andere Technik gab, wie Sie verspotten wollten, wenn Sie keine Schnittstellen haben.
Bulan