Nach mehr als 10 Jahren Java / C # -Programmierung erstelle ich entweder:
- Abstrakte Klassen : Vertrag soll nicht wie besehen instanziiert werden.
- final / sealed classes : Implementierung, die nicht als Basisklasse für etwas anderes gedacht ist.
Ich kann mir keine Situation vorstellen, in der eine einfache "Klasse" (dh weder abstrakt noch endgültig / versiegelt) "weise Programmierung" wäre.
Warum sollte eine Klasse nicht "abstrakt" oder "endgültig / versiegelt" sein?
BEARBEITEN
Dieser großartige Artikel erklärt meine Bedenken viel besser als ich kann.
object-oriented
programming-practices
Nicolas Repiquet
quelle
quelle
Open/Closed principle
, nicht dasClosed Principle.
Antworten:
Ironischerweise finde ich das Gegenteil: Die Verwendung von abstrakten Klassen ist eher die Ausnahme als die Regel, und ich neige dazu, finale / versiegelte Klassen zu missbilligen.
Schnittstellen sind ein typischer Design-by-Contract-Mechanismus, da Sie keine Interna angeben - Sie machen sich keine Sorgen darüber. Es ermöglicht, dass jede Ausführung dieses Vertrags unabhängig ist. Dies ist in vielen Bereichen der Schlüssel. Wenn Sie beispielsweise ein ORM erstellen, ist es sehr wichtig, dass Sie eine Abfrage auf einheitliche Weise an die Datenbank übergeben können, die Implementierungen können jedoch sehr unterschiedlich sein. Wenn Sie zu diesem Zweck abstrakte Klassen verwenden, wird die Verdrahtung in Komponenten durchgeführt, die möglicherweise für alle Implementierungen gelten oder nicht.
Für endgültige / versiegelte Klassen kann ich nur dann eine Entschuldigung finden, wenn es tatsächlich gefährlich ist, das Überschreiben zuzulassen - vielleicht ein Verschlüsselungsalgorithmus oder so. Ansonsten wissen Sie nie, wann Sie die Funktionalität aus lokalen Gründen erweitern möchten. Das Versiegeln einer Klasse schränkt Ihre Optionen für Gewinne ein, die in den meisten Szenarien nicht vorhanden sind. Es ist weitaus flexibler, Ihre Klassen so zu schreiben, dass sie später erweitert werden können.
Diese letztere Ansicht wurde für mich durch die Arbeit mit Komponenten von Drittanbietern gefestigt, die Klassen versiegelten, wodurch eine Integration verhindert wurde, die das Leben viel einfacher gemacht hätte.
quelle
Das ist ein großartiger Artikel von Eric Lippert, aber ich denke nicht, dass er Ihre Sichtweise unterstützt.
Er argumentiert, dass alle Klassen, die für die Verwendung durch andere exponiert sind, versiegelt oder auf andere Weise nicht dehnbar gemacht werden sollten.
Sie gehen davon aus, dass alle Klassen abstrakt oder versiegelt sein sollten.
Großer Unterschied.
ELs Artikel sagt nichts über die (vermutlich vielen) Klassen aus, die von seinem Team produziert wurden und von denen Sie und ich nichts wissen. Im Allgemeinen sind die öffentlich zugänglichen Klassen in einem Framework nur eine Teilmenge aller Klassen, die an der Implementierung dieses Frameworks beteiligt sind.
quelle
new List<Foo>()
etwas wie ersetzt werdenList<Foo>.CreateMutable()
.Aus Java-Sicht denke ich, dass die Abschlussklassen nicht so schlau sind, wie es scheint.
Viele Tools (insbesondere AOP, JPA usw.) arbeiten mit Wartezeiten, sodass sie Ihre Klassen erweitern müssen. Die andere Möglichkeit besteht darin, Delegaten (nicht die .NET-Delegaten) zu erstellen und alles an die ursprüngliche Klasse zu delegieren. Dies wäre viel umständlicher als das Erweitern der Benutzerklassen.
quelle
Zwei häufige Fälle, in denen Sie nicht versiegelte Vanille-Klassen benötigen :
Technisch: Wenn Sie eine Hierarchie mit mehr als zwei Ebenen haben und etwas in der Mitte instanziieren möchten.
Prinzip: Manchmal ist es wünschenswert, explizite Klassen zu schreiben , die sicher erweitert werden können . (Dies passiert häufig, wenn Sie eine API wie Eric Lippert schreiben oder wenn Sie in einem Team an einem großen Projekt arbeiten.) Manchmal möchten Sie eine Klasse schreiben, die für sich alleine funktioniert, aber die Erweiterbarkeit berücksichtigt.
Eric Lippert Gedanken über Dichtungs macht Sinn, aber er gibt auch zu, dass sie tun Entwurf für Erweiterbarkeit durch die Klasse „offen“ zu verlassen.
Ja, viele Klassen sind in der BCL versiegelt, aber eine große Anzahl von Klassen nicht. Sie können auf alle möglichen Arten erweitert werden. Ein Beispiel dafür ist Windows Forms, bei dem Sie nahezu allen
Control
über die Vererbung Daten oder Verhalten hinzufügen können . Natürlich hätte dies auch auf andere Weise geschehen können (Dekorationsmuster, verschiedene Arten von Kompositionen usw.), aber die Vererbung funktioniert auch sehr gut.Zwei .NET-spezifische Hinweise:
virtual
mit Ausnahme expliziter Schnittstellenimplementierungen nicht mit Ihrer Nichtfunktionalität in Konflikt geraten können .internal
zu erstellen, anstatt die Klasse zu versiegeln. Dadurch kann die Klasse innerhalb der Codebasis, aber nicht außerhalb der Codebasis vererbt werden.quelle
Ich glaube, der wahre Grund, warum viele Leute der Meinung sind, Klassen sollten
final
/sealed
sind, dass die meisten nicht abstrakten erweiterbaren Klassen nicht richtig dokumentiert sind .Lassen Sie mich näher darauf eingehen. Aus der Ferne gibt es unter einigen Programmierern die Ansicht, dass die Vererbung als Werkzeug in OOP häufig überbeansprucht und missbraucht wird. Wir haben alle das Liskov-Substitutionsprinzip gelesen, obwohl dies uns nicht davon abgehalten hat, es hunderte (vielleicht sogar tausende) Male zu verletzen.
Die Sache ist, dass Programmierer gerne Code wiederverwenden. Auch wenn es keine so gute Idee ist. Und Vererbung ist ein Schlüsselwerkzeug für dieses "erneute Übertragen" von Code. Zurück zur letzten / versiegelten Frage.
Die richtige Dokumentation für eine endgültige / versiegelte Klasse ist relativ klein: Sie beschreiben, was jede Methode tut, was die Argumente sind, den Rückgabewert usw. Alle üblichen Dinge.
Wenn Sie jedoch eine erweiterbare Klasse ordnungsgemäß dokumentieren , müssen Sie mindestens Folgendes einschließen :
super
Implementierung auf. Rufen Sie sie am Anfang der Methode oder am Ende auf. Denken Sie an Konstruktor vs. Destruktor.)Diese sind einfach von der Spitze meines Kopfes. Und ich kann Ihnen ein Beispiel dafür geben, warum all dies wichtig ist, und wenn Sie es überspringen, wird dies eine erweiterte Klasse durcheinander bringen.
Überlegen Sie nun, wie viel Dokumentationsaufwand in die ordnungsgemäße Dokumentation all dieser Dinge investiert werden sollte. Ich glaube, dass eine Klasse mit 7-8 Methoden (die in einer idealisierten Welt zu viel, in der Realität jedoch zu wenig ist) genauso gut eine Dokumentation mit 5 Seiten nur Text enthalten kann. Stattdessen schleichen wir uns auf halbem Weg aus und versiegeln die Klasse nicht, damit andere sie nutzen können, dokumentieren sie jedoch auch nicht richtig, da dies sehr viel Zeit in Anspruch nimmt (und Sie wissen, dass dies möglicherweise der Fall ist) niemals verlängert werden, also warum sich die Mühe machen?).
Wenn Sie eine Klasse entwerfen, verspüren Sie möglicherweise die Versuchung, sie zu versiegeln, damit die Leute sie nicht auf eine Weise verwenden können, die Sie nicht vorausgesehen haben (und auf die Sie vorbereitet sind). Wenn Sie andererseits den Code eines anderen Benutzers verwenden, gibt es manchmal über die öffentliche API keinen sichtbaren Grund für die Endgültigkeit der Klasse, und Sie könnten denken: "Verdammt, das hat mich nur 30 Minuten bei der Suche nach einer Problemumgehung gekostet."
Ich denke, einige der Elemente einer Lösung sind:
Es lohnt sich auch die folgende „philosophische“ Frage zu erwähnen: Ist , ob eine Klasse
sealed
/final
Teil des Vertrages für die Klasse oder eine Implementierung Detail? Jetzt möchte ich nicht dorthin gehen, aber eine Antwort darauf sollte auch Ihre Entscheidung beeinflussen, ob Sie eine Klasse besiegeln oder nicht.quelle
Eine Klasse sollte weder endgültig / versiegelt noch abstrakt sein, wenn:
Nehmen Sie zum Beispiel die
ObservableCollection<T>
Klasse in C # . Es muss nur das Auslösen von Ereignissen zu den normalen Operationen von a hinzugefügt werdenCollection<T>
, weshalb es Unterklassen bildetCollection<T>
.Collection<T>
ist eine tragfähige Klasse für sich, und so ist esObservableCollection<T>
.quelle
CollectionBase
(Zusammenfassung),Collection : CollectionBase
(versiegelt),ObservableCollection : CollectionBase
(versiegelt) tun . Wenn Sie sich Collection <T> genau ansehen , werden Sie feststellen, dass es sich um eine halbherzige abstrakte Klasse handelt.Collection<T>
eine "halbherzige abstrakte Klasse"?Collection<T>
Enthüllt viele Innereien durch geschützte Methoden und Eigenschaften und ist eindeutig als Basisklasse für spezialisierte Sammlungen gedacht. Es ist jedoch nicht klar, wo Sie Ihren Code ablegen sollen, da es keine abstrakten Methoden zum Implementieren gibt.ObservableCollection
erbt vonCollection
und ist nicht versiegelt, sodass Sie wieder von ihm erben können. Und Sie können auf dieItems
geschützte Eigenschaft zugreifen , sodass Sie Elemente in die Sammlung aufnehmen können, ohne Ereignisse auslösen zu müssen.Das Problem mit finalen / versiegelten Klassen ist, dass sie versuchen, ein Problem zu lösen, das noch nicht passiert ist. Dies ist nur dann nützlich, wenn das Problem besteht, aber frustrierend, weil ein Dritter eine Einschränkung auferlegt hat. Dichtungsklasse löst selten ein aktuelles Problem, was es schwierig macht, seine Nützlichkeit zu argumentieren.
Es gibt Fälle, in denen eine Klasse versiegelt werden sollte. Zum Beispiel; Die Klasse verwaltet die zugewiesenen Ressourcen / den zugewiesenen Speicher so, dass sie nicht vorhersagen kann, wie zukünftige Änderungen diese Verwaltung verändern könnten.
Im Laufe der Jahre habe ich festgestellt, dass Kapselung, Rückrufe und Ereignisse weitaus flexibler und nützlicher sind als abstrahierte Klassen. Ich sehe viel zu viel Code mit einer großen Hierarchie von Klassen, in denen die Kapselung und Ereignisse dem Entwickler das Leben einfacher gemacht hätten.
quelle
"Siegeln" oder "Finalisieren" in Objektsystemen ermöglicht bestimmte Optimierungen, da die vollständige Versandgrafik bekannt ist.
Das heißt, wir machen es schwierig, das System in etwas anderes zu verwandeln, als Kompromiss für die Leistung. (Das ist die Essenz der meisten Optimierungen.)
Ansonsten ist es ein Verlust. Systeme sollten standardmäßig geöffnet und erweiterbar sein. Es sollte einfach sein, allen Klassen neue Methoden hinzuzufügen und beliebig zu erweitern.
Wir erhalten heute keine neuen Funktionen, indem wir Maßnahmen ergreifen, um zukünftige Erweiterungen zu verhindern.
Wenn wir es also zum Zwecke der Vorbeugung selbst tun, versuchen wir, das Leben zukünftiger Instandhalter zu kontrollieren. Es geht um das Ego. "Auch wenn ich hier nicht mehr arbeite, wird dieser Code auf meine Art beibehalten, verdammt!"
quelle
Testklassen scheinen in den Sinn zu kommen. Hierbei handelt es sich um Klassen, die automatisch oder "nach Belieben" aufgerufen werden, je nachdem, was der Programmierer / Tester zu erreichen versucht. Ich bin mir nicht sicher, ob ich jemals eine abgeschlossene Klasse von Tests gesehen oder gehört habe, die privat ist.
quelle
Die Zeit, in der Sie eine Klasse in Betracht ziehen müssen, die erweitert werden soll, ist die Zeit, in der Sie eine echte Planung für die Zukunft durchführen. Lassen Sie mich Ihnen ein reales Beispiel aus meiner Arbeit geben.
Ich verbringe viel Zeit damit, Schnittstellen-Tools zwischen unserem Hauptprodukt und externen Systemen zu schreiben. Wenn wir einen neuen Verkauf abschließen, besteht eine große Komponente aus einer Reihe von Exporteuren, die in regelmäßigen Abständen ausgeführt werden und Datendateien mit den Ereignissen an diesem Tag erstellen. Diese Datendateien werden dann vom System des Kunden verwendet.
Dies ist eine hervorragende Gelegenheit, den Unterricht zu erweitern.
Ich habe eine Export-Klasse, die die Basisklasse jedes Exporteurs ist. Es kann eine Verbindung zur Datenbank herstellen, herausfinden, wo sie zuletzt ausgeführt wurde, und Archive der von ihr generierten Datendateien erstellen. Es bietet auch die Verwaltung von Eigenschaftendateien, die Protokollierung und einige einfache Ausnahmebehandlungen.
Darüber hinaus habe ich einen anderen Exporteur, der mit den einzelnen Datentypen arbeitet. Möglicherweise gibt es Benutzeraktivitäten, Transaktionsdaten, Cash-Management-Daten usw.
Auf diesen Stapel lege ich eine kundenspezifische Ebene, die die vom Kunden benötigte Datendateistruktur implementiert.
Auf diese Weise ändert sich der Basis-Exporteur sehr selten. Die wichtigsten Exportprogramme für Datentypen ändern sich manchmal, aber selten, und normalerweise nur, um Datenbankschemaänderungen zu verarbeiten, die ohnehin an alle Kunden weitergegeben werden sollten. Die einzige Arbeit, die ich jemals für jeden Kunden erledigen muss, ist der Teil des Codes, der für diesen Kunden spezifisch ist. Eine perfekte Welt!
Die Struktur sieht also so aus:
Mein wichtigster Punkt ist, dass ich durch die Architektur des Codes auf diese Weise die Vererbung hauptsächlich für die Wiederverwendung von Code nutzen kann.
Ich muss sagen, ich kann mir keinen Grund vorstellen, über drei Schichten hinauszugehen.
Ich habe oft zwei Ebenen verwendet, um zum Beispiel eine gemeinsame
Table
Klasse zu haben, die Abfragen von Datenbanktabellen implementiert, während UnterklassenTable
die spezifischen Details jeder Tabelle implementieren, indem sie ein verwendenenum
, um die Felder zu definieren. Dasenum
Implementieren einer in derTable
Klasse definierten Schnittstelle zuzulassen, ist in jeder Hinsicht sinnvoll.quelle
Ich fand abstrakte Klassen nützlich, aber nicht immer notwendig, und versiegelte Klassen wurden zu einem Problem bei der Anwendung von Komponententests. Sie können eine versiegelte Klasse nicht verspotten oder stummschalten, es sei denn, Sie verwenden so etwas wie Teleriks Justmock.
quelle
Ich stimme Ihrer Ansicht zu. Ich denke, dass in Java Klassen standardmäßig als "final" deklariert werden sollten. Wenn Sie es nicht endgültig machen, bereiten Sie es speziell vor und dokumentieren Sie es für die Erweiterung.
Der Hauptgrund dafür ist, sicherzustellen, dass alle Instanzen Ihrer Klassen der Schnittstelle entsprechen, die Sie ursprünglich entworfen und dokumentiert haben. Andernfalls kann ein Entwickler, der Ihre Klassen verwendet, möglicherweise spröden und inkonsistenten Code erstellen und diesen an andere Entwickler / Projekte weitergeben, wodurch Objekte Ihrer Klassen nicht als vertrauenswürdig eingestuft werden.
Aus praktischer Sicht hat dies in der Tat einen Nachteil, da Clients der Benutzeroberfläche Ihrer Bibliothek keine Anpassungen vornehmen und Ihre Klassen auf flexiblere Weise verwenden können, als Sie es ursprünglich gedacht hatten.
Persönlich halte ich bei all dem schlechten Qualitätscode, der existiert (und da wir dies auf einer praktischeren Ebene diskutieren, würde ich behaupten, dass die Java-Entwicklung dafür anfälliger ist), diese Starrheit für einen geringen Preis, den wir für unsere Suche nach Einfachheit zahlen müssen Code pflegen.
quelle