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 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:
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 top
Parameter ist erforderlich, um sicherzustellen, dass die Aufrufe writeMainContent
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:
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 ArrayList
zu 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
extend
eine Klasse ohne sorgfältige Überlegungen.
- Markieren Sie Ihre Klassen immer als,
final
auß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.
final
? Viele Leute (einschließlich ich) finden, dass es ein gutes Design ist, jede nicht abstrakte Klasse zu bildenfinal
.