Warum sollte das Schlüsselwort "final" jemals nützlich sein?

54

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 finalklingt 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.

Vorac
quelle
43
Warum ist eine Klasse Ihrer Meinung nach falsch gestaltet, wenn sie mit Anmerkungen versehen ist final? Viele Leute (einschließlich ich) finden, dass es ein gutes Design ist, jede nicht abstrakte Klasse zu bilden final.
Spotted
20
Bevorzugen Sie die Komposition gegenüber der Vererbung, und Sie können jede nicht abstrakte Klasse haben final.
Andy
24
Das Open / Close-Prinzip ist in gewisser Weise ein Anachronismus aus dem 20. Jahrhundert, als das Mantra eine Hierarchie von Klassen bilden sollte, die von Klassen geerbt wurden, die wiederum von anderen Klassen geerbt wurden. Dies war gut, um objektorientiertes Programmieren zu lehren, aber es stellte sich heraus, dass es bei der Anwendung auf reale Probleme zu verworrenen, nicht zu wartenden Verwirrungen kam. Es ist schwierig, eine Klasse so zu gestalten, dass sie erweiterbar ist.
David Hammen
34
@DavidArno Sei nicht lächerlich. Vererbung ist die unabdingbare Voraussetzung für objektorientiertes Programmieren, und es ist bei weitem nicht so kompliziert oder chaotisch, wie es manche überdogmatischen Individuen gerne predigen. Es ist ein Werkzeug wie jedes andere, und ein guter Programmierer weiß, wie man das richtige Werkzeug für den Job einsetzt.
Mason Wheeler
48
Als Entwickler, der sich erholt, erinnere ich mich an ein hervorragendes Tool, um zu verhindern, dass schädliches Verhalten in kritische Bereiche gelangt. Ich scheine mich auch daran zu erinnern, dass Vererbung in vielerlei Hinsicht ein mächtiges Werkzeug ist. Es ist fast , als ob Keuchen verschiedene Werkzeuge haben Vor- und Nachteile , und wir als Ingenieure müssen diese Faktoren ausgleichen , wie wir unsere Software produzieren!
corsiKa

Antworten:

136

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.

Kilian Foth
quelle
14
Das würde ich auch. Aber jeder, der jemals eine weit verbreitete Basisklasse geschrieben hat (wie ein Framework oder eine Medienbibliothek), weiß besser, als zu erwarten, dass sich Anwendungsprogrammierer vernünftig verhalten. Sie untergraben, missbrauchen und verfälschen Ihre Erfindung auf eine Weise, die Sie nicht für möglich gehalten hätten, wenn Sie sie nicht mit einem eisernen Griff verschließen.
Kilian Foth
8
@KilianFoth Ok, aber ehrlich, wie ist das dein Problem, was Anwendungsprogrammierer tun?
Coredump
20
@coredump Leute, die meine Bibliothek benutzen, schaffen schlechte Systeme. Schlechte Systeme führen zu schlechten Reputationen. Benutzer werden nicht in der Lage sein, Kilians großartigen Code von Fred Randoms ungeheuer instabiler App zu unterscheiden. Ergebnis: Ich verliere bei der Programmierung von Creds und Kunden. Es ist eine Frage von Dollars und Cents, Ihren Code nur schwer zu missbrauchen.
Kilian Foth
21
Die Aussage "Dieses Element soll sich nicht ändern, und wenn Sie es ändern möchten, haben Sie das vorhandene Design nicht verstanden." ist unglaublich arrogant und nicht die Einstellung, die ich als Teil einer Bibliothek, mit der ich gearbeitet habe, haben möchte. Wenn ich nur für jedes Mal einen Cent hätte, hätte ich durch eine lächerlich überkapselte Bibliothek keinen Mechanismus mehr, um einen Teil des inneren Zustands zu ändern , der geändert werden muss, weil der Autor einen wichtigen Anwendungsfall nicht antizipiert hat ...
Mason Wheeler,
31
@JimmyB Faustregel: Wenn Sie glauben, dass Sie wissen, wofür Ihre Kreation verwendet wird, liegen Sie bereits falsch. Bell stellte sich das Telefon im Wesentlichen als ein Muzak-System vor. Kleenex wurde erfunden, um Make-up bequemer zu entfernen. Thomas Watson, Präsident von IBM, sagte einmal: "Ich glaube, es gibt einen Weltmarkt für vielleicht fünf Computer." Der Vertreter Jim Sensenbrenner, der 2001 das PATRIOT-Gesetz einführte, gibt zu Protokoll, dass es speziell dazu gedacht war, die NSA daran zu hindern , Dinge zu tun, die sie "mit der Autorität des PATRIOT-Gesetzes" tun. Und so weiter ...
Mason Wheeler
59

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:

public class PasswordChecker {
  public boolean passwordIsOk(String password) {
    return password == "s3cret";
  }
}

Wenn wir zulassen, dass diese Klasse überschrieben wird, kann eine Implementierung alle blockieren, eine andere kann allen Zugriff gewähren:

public class OpenDoor extends PasswordChecker {
  public boolean passwordIsOk(String password) {
    return true;
  }
}

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:

PasswordChecker passwordChecker =
  new DefaultPasswordChecker(null);
// or:
PasswordChecker passwordChecker =
  new OpenDoor(null);
// or:
PasswordChecker passwordChecker =
 new DefaultPasswordChecker(
   new OpenDoor(null)
 );

public interface PasswordChecker {
  boolean passwordIsOk(String password);
}

public final class DefaultPasswordChecker implements PasswordChecker {
  private PasswordChecker next;

  public DefaultPasswordChecker(PasswordChecker next) {
    this.next = next;
  }

  @Override
  public boolean passwordIsOk(String password) {
    if ("s3cret".equals(password)) return true;
    if (next != null) return next.passwordIsOk(password);
    return false;
  }
}

public final class OpenDoor implements PasswordChecker {
  private PasswordChecker next;

  public OpenDoor(PasswordChecker next) {
    this.next = next;
  }

  @Override
  public boolean passwordIsOk(String password) {
    return true;
  }
}

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.

public class Page {
  ...;

  @Override
  public String toString() {
    PrintWriter out = ...;
    out.print("<!DOCTYPE html>");
    out.print("<html>");

    out.print("<head>");
    out.print("</head>");

    out.print("<body>");
    writeHeader(out);
    writeMainContent(out);
    writeMainFooter(out);
    out.print("</body>");

    out.print("</html>");
    ...
  }

  void writeMainContent(PrintWriter out) {
    out.print("<div class='article'>");
    out.print(htmlEscapedContent);
    out.print("</div>");
  }

  ...
}

Ich erstelle jetzt eine Unterklasse, die etwas mehr Styling hinzufügt:

class SpiffyPage extends Page {
  ...;


  @Override
  void writeMainContent(PrintWriter out) {
    out.print("<div class='row'>");

    out.print("<div class='col-md-8'>");
    super.writeMainContent(out);
    out.print("</div>");

    out.print("<div class='col-md-4'>");
    out.print("<h4>About the Author</h4>");
    out.print(htmlEscapedAuthorInfo);
    out.print("</div>");

    out.print("</div>");
  }
}

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 SpiffyPageUnterklasse 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:

Page page = ...;
page.decorateLayout(current -> new SpiffyPageDecorator(current));
print(page.toString());

public interface PageLayout {
  void writePage(PrintWriter out, PageLayout top);
  void writeMainContent(PrintWriter out, PageLayout top);
  ...
}

public final class Page {
  private PageLayout layout = new DefaultPageLayout();

  public void decorateLayout(Function<PageLayout, PageLayout> wrapper) {
    layout = wrapper.apply(layout);
  }

  ...
  @Override public String toString() {
    PrintWriter out = ...;
    layout.writePage(out, layout);
    ...
  }
}

public final class DefaultPageLayout implements PageLayout {
  @Override public void writeLayout(PrintWriter out, PageLayout top) {
    out.print("<!DOCTYPE html>");
    out.print("<html>");

    out.print("<head>");
    out.print("</head>");

    out.print("<body>");
    top.writeHeader(out, top);
    top.writeMainContent(out, top);
    top.writeMainFooter(out, top);
    out.print("</body>");

    out.print("</html>");
  }

  @Override public void writeMainContent(PrintWriter out, PageLayout top) {
    ... /* as above*/
  }
}

public final class SpiffyPageDecorator implements PageLayout {
  private PageLayout inner;

  public SpiffyPageDecorator(PageLayout inner) {
    this.inner = inner;
  }

  @Override
  void writePage(PrintWriter out, PageLayout top) {
    inner.writePage(out, top);
  }

  @Override
  void writeMainContent(PrintWriter out, PageLayout top) {
    ...
    inner.writeMainContent(out, top);
    ...
  }
}

(Der zusätzliche topParameter ist erforderlich, um sicherzustellen, dass die Aufrufe writeMainContentdie 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:

final class Thingies implements Iterable<Thing> {
  private ArrayList<Thing> thingList = new ArrayList<>();

  @Override public Iterator<Thing> iterator() {
    return thingList.iterator();
  }

  public void add(Thing thing) {
    thingList.add(thing);
  }

  ... // custom methods
}

Stattdessen haben sie eine Unterklasse erstellt:

class Thingies extends ArrayList<Thing> {
  ... // custom methods
}

Dies bedeutet plötzlich, dass die gesamte Schnittstelle von ArrayListzu einem Teil unserer Schnittstelle geworden ist. Benutzer können remove()Dinge oder get()Dinge an bestimmten Indizes. Das war so gedacht? OKAY. Aber oft überlegen wir uns nicht alle Konsequenzen.

Es ist daher ratsam,

  • Niemals extendeine Klasse ohne sorgfältige Überlegungen.
  • Markieren Sie Ihre Klassen immer als, finalaußer wenn Sie beabsichtigen, eine Methode zu überschreiben.
  • Erstellen Sie Schnittstellen, an denen Sie eine Implementierung austauschen möchten, z. B. für Unit-Tests.

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:

  • Alle Methoden sind standardmäßig final und müssen explizit als gekennzeichnet werden virtual
  • Sie stellen eine private Vererbung bereit, die nicht die Schnittstelle, sondern nur die Implementierung erbt.
  • Sie erfordern, dass Basisklassenmethoden als virtuell markiert werden, und alle Überschreibungen müssen ebenfalls markiert werden. Auf diese Weise werden Probleme vermieden, bei denen eine Unterklasse eine neue Methode definiert, der Basisklasse jedoch später eine Methode mit derselben Signatur hinzugefügt wurde, die jedoch nicht als virtuell gedacht ist.
amon
quelle
3
Sie verdienen mindestens +100, wenn Sie das "fragile Basisklassenproblem" erwähnen. :)
David Arno
6
Die hier gemachten Punkte überzeugen mich nicht. Ja, die fragile Basisklasse ist ein Problem, aber final löst nicht alle Probleme beim Ändern einer Implementierung. Ihr erstes Beispiel ist schlecht, weil Sie davon ausgehen, dass Sie alle möglichen Anwendungsfälle für den PasswordChecker kennen ("alle sperren oder allen Zugriff gewähren ... ist nicht in Ordnung" - sagt wer?). Ihre letzte "daher empfehlenswerte ..." Liste ist wirklich schlecht - Sie befürworten im Grunde, nichts zu erweitern und alles als endgültig zu markieren - was die Nützlichkeit von OOP, Vererbung und Wiederverwendung von Code völlig zunichte macht.
Adelphus
4
Ihr erstes Beispiel ist kein Beispiel für das fragile Basisklassenproblem. In dem fragilen Basisklassenproblem wird durch Änderungen an einer Basisklasse eine Unterklasse getrennt. In diesem Beispiel folgt Ihre Unterklasse jedoch nicht dem Vertrag einer Unterklasse. Das sind zwei verschiedene Probleme. (Außerdem ist es tatsächlich sinnvoll, dass Sie unter bestimmten Umständen einen Kennwortprüfer deaktivieren (z. B. für die Entwicklung).)
Winston Ewert
5
"Es vermeidet das Problem der zerbrechlichen Basisklasse" - auf die gleiche Weise, wie das Selbstmorden den Hunger vermeidet.
user253751
9
@immibis, es ist eher so, als würde man auf das Essen verzichten, um keine Lebensmittelvergiftung zu bekommen. Sicher, niemals essen wäre ein Problem. Aber nur an den Orten zu essen, denen Sie vertrauen, macht sehr viel Sinn.
Winston Ewert
33

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:

Aber was ist mit gewöhnlichen konkreten Klassen? Traditionell sind sie weder endgültig noch für eine Unterklasse vorgesehen und dokumentiert, aber dieser Zustand ist gefährlich. Jedes Mal, wenn eine Änderung an einer solchen Klasse vorgenommen wird, besteht die Möglichkeit, dass Clientklassen, die die Klasse erweitern, nicht mehr funktionieren. Dies ist nicht nur ein theoretisches Problem. Es ist nicht ungewöhnlich, dass Fehlerberichte im Zusammenhang mit Unterklassen erhalten werden, nachdem die Interna einer nicht endgültigen konkreten Klasse geändert wurden, die nicht für die Vererbung entworfen und dokumentiert wurde.

Die beste Lösung für dieses Problem besteht darin, die Unterklasse in Klassen zu unterbinden, die nicht für eine sichere Unterklasse entworfen und dokumentiert wurden. Es gibt zwei Möglichkeiten, Unterklassen zu verbieten. Das einfachere von beiden ist, das Klassenfinale zu erklären. Die Alternative besteht darin, alle Konstruktoren privat oder paketprivat zu machen und anstelle der Konstruktoren öffentliche statische Fabriken hinzuzufügen . Diese Alternative, die die Flexibilität bietet, Unterklassen intern zu verwenden, wird in Punkt 15 erörtert. Beide Ansätze sind akzeptabel.

Daniel Pryden
quelle
21

Einer der Gründe finaldafü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 Klasse finalverhindert.

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 Stringund 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 valdie Variablen verwenden, die in etwa den final-Variablen in Java entsprechen, und tatsächlich die Verwendung von a varoder 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.

Michał Kosmulski
quelle
6
Letztendlich können Compiler, zumindest theoretisch, => Clangs Devirtualisierungspass persönlich überprüfen und ich bestätige, dass er in der Praxis verwendet wird.
Matthieu M.
Aber kann der Compiler nicht im Voraus sagen, dass niemand eine Klasse oder Methode überschreibt, unabhängig davon, ob sie als endgültig markiert ist oder nicht?
JesseTG
3
@JesseTG Wenn es Zugriff auf den gesamten Code auf einmal hat, wahrscheinlich. Was ist jedoch mit der separaten Dateizusammenstellung?
Setzen Sie Monica
3
@JesseTG-Devirtualisierung oder (monomorphes / polymorphes) Inline-Caching ist eine gängige Technik in JIT-Compilern, da das System weiß, welche Klassen derzeit geladen sind, und den Code desoptimieren kann, wenn sich die Annahme, dass keine übergeordneten Methoden verwendet werden, als falsch herausstellt. Ein Compiler, der der Zeit voraus ist, kann dies jedoch nicht. Wenn ich eine Java-Klasse und einen Code kompiliere, der diese Klasse verwendet, kann ich später eine Unterklasse kompilieren und eine Instanz an den konsumierenden Code übergeben. Am einfachsten ist es, ein weiteres Gefäß vor den Klassenpfad zu setzen. Davon kann der Compiler nichts wissen, da dies zur Laufzeit geschieht.
amon
5
@MTilsted Sie können davon ausgehen, dass Strings unveränderlich sind, da mutierende Strings durch Reflektion über a gesperrt werden könnenSecurityManager . 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.
Maaartinus
7

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.

JoulinRouge
quelle
1
bancomat = ATM: en.wikipedia.org/wiki/Cash_machine
tar
Die JVM ist intelligent genug, um Klassen als endgültig zu behandeln, wenn sie keine Unterklassen haben, auch wenn sie nicht als endgültig deklariert wurden.
user253751
Abgelehnt, weil die Behauptung der "Leistung" so oft widerlegt wurde.
Paul Smith
7

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.

gnasher729
quelle
4
-1 Diese Antwort beschreibt alles, was mit Softwareentwicklern nicht stimmt. Wenn jemand Ihre Klasse durch Unterklassen wiederverwenden möchte, lassen Sie sie. Warum liegt es in Ihrer Verantwortung, wie sie es benutzen (oder missbrauchen)? Grundsätzlich verwendest du final, weil du meine Klasse nicht verwendest. "Niemand hat mich dafür bezahlt, dass ich eine Klasse gemacht habe, die untergeordnet werden kann" . Sind Sie im Ernst? Genau deshalb werden Softwareingenieure eingesetzt, um soliden, wiederverwendbaren Code zu erstellen.
Adelphus
4
-1 Mache einfach alles privat, damit niemand an Unterklassen denkt ..
M4ks
3
@adelphus Der Wortlaut dieser Antwort ist zwar stumpf, grenzt aber an Härte, es handelt sich jedoch nicht um einen "falschen" Standpunkt. Tatsächlich ist es der gleiche Standpunkt wie die Mehrheit der Antworten auf diese Frage, jedoch nur mit einem weniger klinischen Ton.
NemesisX00
+1 für die Erwähnung, dass Sie "final" entfernen können. Es ist arrogant, die Kenntnis aller möglichen Verwendungen Ihres Codes vorauszusagen. Es ist jedoch bescheiden, klar zu machen, dass Sie einige mögliche Verwendungen nicht beibehalten können und dass für diese Verwendungen eine Gabel erforderlich wäre.
17.
4

Stellen wir uns vor, dass das SDK für eine Plattform die folgende Klasse enthält:

class HTTPRequest {
   void get(String url, String method = "GET");
   void post(String url) {
       get(url, "POST");
   }
}

Eine Anwendung unterteilt diese Klasse in folgende Unterklassen:

class MyHTTPRequest extends HTTPRequest {
    void get(String url, String method = "GET") {
        requestCounter++;
        super.get(url, method);
    }
}

Alles ist in Ordnung und in Ordnung, aber jemand, der am SDK arbeitet, entscheidet, dass getes dumm ist, eine Methode an zu übergeben , und verbessert die Schnittstelle, um die Abwärtskompatibilität durchzusetzen.

class HTTPRequest {
   @Deprecated
   void get(String url, String method) {
        request(url, method);
   }

   void get(String url) {
       request(url, "GET");
   }
   void post(String url) {
       request(url, "POST");
   }

   void request(String url, String method);
}

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.

Winston Ewert
quelle
1

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.

Scott Taylor
quelle
2
Wenn sich Ihre Targeting-Methode ändert, warum haben Sie sie jemals veröffentlicht? Und wenn Sie das Verhalten einer Klasse erheblich ändern, benötigen Sie eine andere Version, anstatt übermäßig zu final
schützen
Ich habe keine Ahnung, warum sich das ändern würde. Hobbits sind Tricksie und die Änderung war erforderlich. Der Punkt ist, dass ich, wenn ich es als Final erstelle, die Vererbung verhindere, die andere Leute davor schützt, dass meine Änderungen ihren Code infizieren.
Scott Taylor
0

Die Verwendung von finalist 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:

  • Verwenden Sie das Decorator-Entwurfsmuster, um das vorhandene Verhalten des Originals wiederzuverwenden
  • Refaktorieren Sie die Teile des Verhaltens, die Sie in einem Helferobjekt behalten möchten, und verwenden Sie denselben Helfer in Ihrer neuen Implementierung (Refaktorierung ist keine Änderung).
Jules
quelle
0

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

bns
quelle