Es scheint, als hätte Java die Fähigkeit, Klassen zu deklarieren, die seit Ewigkeiten nicht mehr abgeleitet werden können, und jetzt hat es auch C ++. Warum ist dies jedoch angesichts des Open / Close-Prinzips in SOLID sinnvoll? Für mich final
klingt das Schlüsselwort wie friend
- es ist legal, aber wenn Sie es verwenden, ist das Design höchstwahrscheinlich falsch. Bitte geben Sie einige Beispiele an, in denen eine nicht ableitbare Klasse Teil einer großartigen Architektur oder eines großartigen Entwurfsmusters ist.
54
final
? Viele Leute (einschließlich ich) finden, dass es ein gutes Design ist, jede nicht abstrakte Klasse zu bildenfinal
.final
.Antworten:
final
drückt Absicht aus . Es teilt dem Benutzer eine Klasse, eine Methode oder eine Variable mit. "Dieses Element darf nicht geändert werden. Wenn Sie es ändern möchten, haben Sie das vorhandene Design nicht verstanden."Dies ist wichtig, da die Programmarchitektur sehr, sehr schwierig wäre, wenn Sie damit rechnen müssten, dass jede Klasse und jede Methode, die Sie jemals schreiben, von einer Unterklasse zu etwas völlig anderem geändert werden könnte. Es ist viel besser, im Voraus zu entscheiden, welche Elemente veränderbar sein sollen und welche nicht, und die Unveränderlichkeit durchzusetzen
final
.Sie können dies auch über Kommentare und Architekturdokumente tun. Es ist jedoch immer besser, den Compiler die erforderlichen Schritte ausführen zu lassen, als zu hoffen, dass zukünftige Benutzer die Dokumentation lesen und befolgen.
quelle
Es vermeidet das Fragile Base Class Problem . Jede Klasse enthält implizite oder explizite Garantien und Invarianten. Das Liskov-Substitutionsprinzip schreibt vor, dass alle Subtypen dieser Klasse auch alle diese Garantien bieten müssen. Es ist jedoch sehr einfach, dies zu verletzen, wenn wir es nicht verwenden
final
. Lassen Sie uns zum Beispiel einen Passwort-Prüfer haben:Wenn wir zulassen, dass diese Klasse überschrieben wird, kann eine Implementierung alle blockieren, eine andere kann allen Zugriff gewähren:
Dies ist normalerweise nicht in Ordnung, da die Unterklassen jetzt ein Verhalten aufweisen, das mit dem Original sehr inkompatibel ist. Wenn wir wirklich beabsichtigen, die Klasse um anderes Verhalten zu erweitern, wäre eine Verantwortungskette besser:
Das Problem wird offensichtlicher, wenn eine komplizierte Klasse ihre eigenen Methoden aufruft und diese Methoden überschrieben werden können. Ich stoße manchmal darauf, wenn ich eine Datenstruktur schön drucke oder HTML schreibe. Jede Methode ist für ein Widget verantwortlich.
Ich erstelle jetzt eine Unterklasse, die etwas mehr Styling hinzufügt:
Nun ignoriere ich für einen Moment, dass dies keine sehr gute Möglichkeit ist, HTML-Seiten zu generieren. Was passiert, wenn ich das Layout noch einmal ändern möchte? Ich müsste eine
SpiffyPage
Unterklasse erstellen , die diesen Inhalt irgendwie umschließt. Was wir hier sehen können, ist eine versehentliche Anwendung des Schablonenmethodenmusters. Template-Methoden sind genau definierte Erweiterungspunkte in einer Basisklasse, die überschrieben werden sollen.Und was passiert, wenn sich die Basisklasse ändert? Wenn sich der HTML-Inhalt zu stark ändert, kann dies das von den Unterklassen bereitgestellte Layout beeinträchtigen. Ein nachträglicher Wechsel der Basisklasse ist daher nicht wirklich sicher. Dies ist nicht ersichtlich, wenn sich alle Ihre Klassen in demselben Projekt befinden, aber sehr auffällig, wenn die Basisklasse Teil einer veröffentlichten Software ist, auf der andere Personen aufbauen.
Wenn diese Erweiterungsstrategie beabsichtigt wäre, hätten wir dem Benutzer erlauben können, die Art und Weise, wie jedes Teil generiert wird, auszutauschen. Entweder könnte es für jeden Block eine Strategie geben, die extern bereitgestellt werden kann. Oder wir könnten Dekorateure nisten. Dies wäre äquivalent zu dem obigen Code, aber viel expliziter und viel flexibler:
(Der zusätzliche
top
Parameter ist erforderlich, um sicherzustellen, dass die AufrufewriteMainContent
die oberste Stufe der Decorator-Kette durchlaufen. Dies emuliert eine Funktion der Unterklasse, die als offene Rekursion bezeichnet wird .)Wenn wir mehrere Dekorateure haben, können wir sie jetzt freier mischen.
Weitaus häufiger als der Wunsch, vorhandene Funktionen leicht anzupassen, ist der Wunsch, einen Teil einer vorhandenen Klasse wiederzuverwenden. Ich habe einen Fall gesehen, in dem jemand eine Klasse wollte, in der Sie Elemente hinzufügen und alle durchlaufen können. Die richtige Lösung wäre gewesen:
Stattdessen haben sie eine Unterklasse erstellt:
Dies bedeutet plötzlich, dass die gesamte Schnittstelle von
ArrayList
zu einem Teil unserer Schnittstelle geworden ist. Benutzer könnenremove()
Dinge oderget()
Dinge an bestimmten Indizes. Das war so gedacht? OKAY. Aber oft überlegen wir uns nicht alle Konsequenzen.Es ist daher ratsam,
extend
eine Klasse ohne sorgfältige Überlegungen.final
außer wenn Sie beabsichtigen, eine Methode zu überschreiben.Es gibt viele Beispiele, in denen diese „Regel“ verletzt werden muss, aber sie führt Sie normalerweise zu einem guten, flexiblen Design und vermeidet Fehler aufgrund unbeabsichtigter Änderungen in Basisklassen (oder unbeabsichtigter Verwendung der Unterklasse als Instanz der Basisklasse) ).
Einige Sprachen haben strengere Durchsetzungsmechanismen:
virtual
quelle
Ich bin überrascht, dass noch niemand Effective Java, 2nd Edition von Joshua Bloch erwähnt hat (was zumindest für jeden Java-Entwickler zum Lesen erforderlich sein sollte). Punkt 17 des Buches behandelt dies im Detail und trägt den Titel: " Design und Dokument zur Vererbung oder zum Verbot ".
Ich werde nicht alle guten Ratschläge in dem Buch wiederholen, aber diese besonderen Absätze scheinen relevant zu sein:
quelle
Einer der Gründe
final
dafür ist, dass Sie eine Klasse nicht in einer Weise unterordnen können, die gegen den Vertrag der Elternklasse verstößt. Eine solche Unterklasse wäre eine Verletzung von SOLID (am allermeisten "L") und würde durch das Erstellen einer Klassefinal
verhindert.Ein typisches Beispiel ist, es unmöglich zu machen, eine unveränderliche Klasse in einer Weise zu unterordnen, die die Unterklasse veränderlich machen würde. In bestimmten Fällen kann eine solche Verhaltensänderung zu sehr überraschenden Effekten führen, beispielsweise wenn Sie in einer Karte einen Schlüssel als Schlüssel verwenden und glauben, der Schlüssel sei unveränderlich, während Sie in Wirklichkeit eine veränderbare Unterklasse verwenden.
In Java könnten viele interessante Sicherheitsprobleme auftreten, wenn Sie in der Lage wären, eine Unterklasse zu erstellen
String
und sie veränderbar zu machen (oder sie beim Aufrufen ihrer Methoden nach Hause zurückrufen zu lassen, um möglicherweise vertrauliche Daten aus dem System zu ziehen), während diese Objekte übergeben werden um einige interne Code in Bezug auf Klassenladen und Sicherheit.Final ist manchmal auch hilfreich, um einfache Fehler wie die Wiederverwendung derselben Variablen für zwei Dinge innerhalb einer Methode usw. zu vermeiden. In Scala sollten Sie nur
val
die Variablen verwenden, die in etwa den final-Variablen in Java entsprechen, und tatsächlich die Verwendung von avar
oder nicht endgültige Variable wird mit Argwohn betrachtet.Schließlich können Compiler zumindest theoretisch einige zusätzliche Optimierungen durchführen, wenn sie wissen, dass eine Klasse oder Methode final ist, da Sie beim Aufrufen einer Methode für eine final-Klasse genau wissen, welche Methode aufgerufen wird, und nicht gehen müssen durch die virtuelle Methodentabelle, um die Vererbung zu überprüfen.
quelle
SecurityManager
. Die meisten Programme verwenden es nicht, aber sie führen auch keinen nicht vertrauenswürdigen Code aus. +++ Sie müssen davon ausgehen, dass Zeichenfolgen unveränderlich sind, da Sie sonst keine Sicherheit und eine Reihe von willkürlichen Fehlern als Bonus erhalten. Jeder Programmierer, der davon ausgeht, dass sich Java-Zeichenfolgen im produktiven Code ändern können, ist mit Sicherheit verrückt.Der zweite Grund ist die Leistung . Der erste Grund ist, dass einige Klassen wichtige Verhaltensweisen oder Zustände aufweisen, die nicht geändert werden sollen, damit das System funktioniert. Wenn ich zum Beispiel eine Klasse "PasswordCheck" besitze und diese Klasse aufbauen möchte, habe ich ein Team von Sicherheitsexperten eingestellt. Diese Klasse kommuniziert mit Hunderten von Geldautomaten mit gut untersuchten und definierten Protokollen. Lassen Sie zu, dass ein neuer Angestellter, der gerade die Universität beendet hat, eine "TrustMePasswordCheck" -Klasse durchführt, die die oben genannte Klasse erweitert, was für mein System sehr schädlich sein kann. Diese Methoden dürfen nicht überschrieben werden, das war's.
quelle
Wenn ich eine Klasse brauche, schreibe ich eine Klasse. Wenn ich keine Unterklassen benötige, interessieren mich Unterklassen nicht. Ich stelle sicher, dass sich meine Klasse wie beabsichtigt verhält, und die Orte, an denen ich die Klasse verwende, gehen davon aus, dass sich die Klasse wie beabsichtigt verhält.
Wenn jemand meine Klasse unterordnen möchte, möchte ich jegliche Verantwortung für das, was passiert, vollständig ablehnen. Das erreiche ich, indem ich die Klasse "final" mache. Wenn Sie eine Unterklasse erstellen möchten, denken Sie daran, dass ich die Unterklasse beim Schreiben der Klasse nicht berücksichtigt habe. Sie müssen also den Klassenquellcode nehmen, das "Finale" entfernen und von da an liegt alles, was passiert, in Ihrer Verantwortung .
Sie denken, das ist "nicht objektorientiert"? Ich wurde dafür bezahlt, eine Klasse zu machen, die das tut, was sie tun soll. Niemand hat mich dafür bezahlt, dass ich eine Klasse gemacht habe, die untergeordnet werden kann. Wenn Sie dafür bezahlt werden, dass meine Klasse wiederverwendbar ist, können Sie das gerne tun. Entfernen Sie zunächst das Schlüsselwort "final".
(Abgesehen davon ermöglicht "final" häufig erhebliche Optimierungen. Beispielsweise bedeutet "final" in Swift für eine öffentliche Klasse oder für eine Methode einer öffentlichen Klasse, dass der Compiler vollständig wissen kann, welchen Code ein Methodenaufruf ausführt. und kann dynamischen Versand durch statischen Versand ersetzen (geringer Nutzen) und häufig statischen Versand durch Inlining ersetzen (möglicherweise großer Nutzen).
adelphus: Was ist so schwer zu verstehen über "Wenn du es unterordnen willst, nimm den Quellcode, entferne das 'Finale' und es liegt in deiner Verantwortung"? "final" entspricht "fairer Warnung".
Und ich werde nicht dafür bezahlt, wiederverwendbaren Code zu erstellen. Ich werde dafür bezahlt, Code zu schreiben, der das tut, was er tun soll. Wenn ich dafür bezahlt werde, zwei ähnliche Code-Teile zu erstellen, extrahiere ich die gemeinsamen Teile, weil das billiger ist und ich nicht dafür bezahlt werde, meine Zeit zu verschwenden. Es ist Zeitverschwendung, Code wiederverwendbar zu machen, der nicht wiederverwendet wird.
M4ks: Sie machen immer alles privat, was nicht von außen zugänglich sein soll. Wenn Sie eine Unterklasse erstellen möchten, nehmen Sie den Quellcode, ändern die Einstellungen nach Bedarf in "protected" und übernehmen die Verantwortung für das, was Sie tun. Wenn Sie glauben, auf Dinge zugreifen zu müssen, die ich als privat gekennzeichnet habe, wissen Sie besser, was Sie tun.
Beide: Unterklassen sind ein winziger Teil der Wiederverwendung von Code. Das Erstellen von Bausteinen, die ohne Unterklassen angepasst werden können, ist viel leistungsfähiger und profitiert immens von "final", da sich die Benutzer der Bausteine auf das verlassen können, was sie erhalten.
quelle
Stellen wir uns vor, dass das SDK für eine Plattform die folgende Klasse enthält:
Eine Anwendung unterteilt diese Klasse in folgende Unterklassen:
Alles ist in Ordnung und in Ordnung, aber jemand, der am SDK arbeitet, entscheidet, dass
get
es dumm ist, eine Methode an zu übergeben , und verbessert die Schnittstelle, um die Abwärtskompatibilität durchzusetzen.Alles scheint in Ordnung, bis die Anwendung von oben mit dem neuen SDK neu kompiliert wird. Plötzlich wird die überschriebene get-Methode nicht mehr aufgerufen und die Anfrage wird nicht gezählt.
Dies wird als fragiles Basisklassenproblem bezeichnet, da eine scheinbar harmlose Änderung zum Bruch einer Unterklasse führt. Jede Änderung, zu der Methoden innerhalb der Klasse aufgerufen werden, kann dazu führen, dass eine Unterklasse unterbrochen wird. Das bedeutet, dass fast jede Änderung dazu führen kann, dass eine Unterklasse kaputt geht.
Final verhindert, dass jemand Ihre Klasse unterordnet. Auf diese Weise können Sie festlegen, welche Methoden in der Klasse geändert werden sollen, ohne sich Sorgen machen zu müssen, dass irgendwo jemand davon abhängt, welche Methodenaufrufe ausgeführt werden.
quelle
Final bedeutet effektiv, dass Ihre Klasse in Zukunft sicher geändert werden kann, ohne dass sich dies auf nachgelagerte vererbungsbasierte Klassen auswirkt (da keine vorhanden sind) oder auf Probleme im Zusammenhang mit der Threadsicherheit der Klasse (ich denke, es gibt Fälle, in denen das letzte Schlüsselwort in einem Feld verhindert einige Thread-basierte High-Jinx).
Final bedeutet, dass Sie die Funktionsweise Ihrer Klasse ändern können, ohne dass sich unbeabsichtigte Verhaltensänderungen in den Code anderer Personen einschleichen, der sich auf Ihren Code als Basis stützt.
Als Beispiel schreibe ich eine Klasse namens HobbitKiller, was großartig ist, weil alle Hobbits Tricksie sind und wahrscheinlich sterben sollten. Vergiss das, sie alle müssen definitiv sterben.
Sie verwenden dies als Basisklasse und fügen eine fantastische neue Methode hinzu, um einen Flammenwerfer zu verwenden, aber verwenden Sie meine Klasse als Basis, weil ich eine großartige Methode habe, um die Hobbits anzuvisieren (zusätzlich zum Tricksie, sie sind schnell), die du hilfst dabei, deinen Flammenwerfer zu zielen.
Drei Monate später ändere ich die Implementierung meiner Targeting-Methode. Zu einem späteren Zeitpunkt, zu dem Sie Ihre Bibliothek aktualisieren, hat sich die tatsächliche Laufzeitimplementierung Ihrer Klasse aufgrund einer Änderung der Superklassenmethode, auf die Sie angewiesen sind (und die Sie im Allgemeinen nicht steuern), grundlegend geändert.
Damit ich ein gewissenhafter Entwickler bin und mit meiner Klasse einen reibungslosen Tod des Hobbits in der Zukunft sicherstellen kann, muss ich sehr, sehr vorsichtig mit allen Änderungen sein, die ich an Klassen vornehme, die erweitert werden können.
Durch das Entfernen der Erweiterungsmöglichkeit, es sei denn, ich beabsichtige ausdrücklich, die Klasse zu erweitern, erspare ich mir selbst (und hoffentlich auch anderen) viele Kopfschmerzen.
quelle
final
Die Verwendung von
final
ist in keiner Weise ein Verstoß gegen die SOLID-Grundsätze. Leider ist es sehr verbreitet, das Open / Closed-Prinzip ("Software-Entities sollten für Erweiterungen offen, aber für Änderungen geschlossen sein") so zu interpretieren, dass sie "eine Klasse nicht modifizieren, sondern in Unterklassen unterteilen und neue Features hinzufügen". Dies war ursprünglich nicht damit gemeint, und es wird allgemein angenommen, dass dies nicht der beste Ansatz zur Erreichung seiner Ziele ist.Die beste Methode zur Einhaltung von OCP ist das Entwerfen von Erweiterungspunkten in einer Klasse, indem speziell abstraktes Verhalten bereitgestellt wird, das durch Einfügen einer Abhängigkeit in das Objekt (z. B. mithilfe des Strategieentwurfsmusters) parametrisiert wird. Diese Verhaltensweisen sollten so konzipiert sein, dass sie eine Schnittstelle verwenden, damit neue Implementierungen nicht auf Vererbung beruhen.
Ein anderer Ansatz besteht darin, Ihre Klasse mit ihrer öffentlichen API als abstrakte Klasse (oder Schnittstelle) zu implementieren. Sie können dann eine völlig neue Implementierung erstellen, die sich an dieselben Clients anschließen lässt. Wenn für Ihre neue Benutzeroberfläche ein weitgehend ähnliches Verhalten wie für die ursprüngliche Benutzeroberfläche erforderlich ist, haben Sie folgende Möglichkeiten:
quelle
Für mich ist es eine Frage des Designs.
Nehmen wir an, ich habe ein Programm, das die Gehälter für Mitarbeiter berechnet. Wenn ich eine Klasse habe, die die Anzahl der Arbeitstage zwischen zwei Daten basierend auf dem Land zurückgibt (eine Klasse für jedes Land), stelle ich diese ab und stelle jedem Unternehmen eine Methode zur Verfügung, um einen freien Tag nur für seine Kalender bereitzustellen.
Warum? Einfach. Angenommen, ein Entwickler möchte die Basisklasse "WorkingDaysUSA" in einer Klasse "WorkingDaysUSAmyCompany" erben und sie so ändern, dass sie angibt, dass sein Unternehmen am 2. März wegen Streiks / Wartungsarbeiten / aus irgendeinem Grund geschlossen wird.
Die Berechnungen für Kundenbestellungen und -lieferungen berücksichtigen die Verzögerung und funktionieren entsprechend, wenn sie zur Laufzeit WorkingDaysUSAmyCompany.getWorkingDays () aufrufen. Was passiert jedoch, wenn ich die Urlaubszeit berechne? Sollte ich den 2. März als Feiertag für alle hinzufügen? Nein. Da der Programmierer die Vererbung verwendet hat und ich die Klasse nicht geschützt habe, kann dies zu Verwirrung führen.
Oder lassen Sie uns annehmen, dass sie die Klasse erben und modifizieren, um zu berücksichtigen, dass diese Firma samstags nicht arbeitet, während sie in dem Land samstags in der Halbzeit arbeitet. Ein Erdbeben, eine Stromkrise oder ein anderer Umstand lassen den Präsidenten 3 arbeitsfreie Tage erklären, wie es kürzlich in Venezuela geschehen ist. Wenn die Methode der geerbten Klasse bereits jeden Samstag abgezogen wird, können meine Änderungen an der ursprünglichen Klasse dazu führen, dass derselbe Tag zweimal abgezogen wird. Ich müsste zu jeder Unterklasse auf jedem Client gehen und überprüfen, ob alle Änderungen kompatibel sind.
Lösung? Machen Sie die Klasse endgültig und geben Sie eine addFreeDay-Methode (companyID mycompany, Date freeDay) an. Auf diese Weise können Sie sicher sein, dass es sich beim Aufrufen einer WorkingDaysCountry-Klasse um Ihre Hauptklasse und nicht um eine Unterklasse handelt
quelle