it-swarm.com.de

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

Es scheint Java hatte die Macht, Klassen seit Ewigkeiten als nicht ableitbar zu deklarieren, und jetzt hat C++ es auch. Angesichts des Open/Close-Prinzips in SOLID, warum sollte das so sein? Für mich klingt das Schlüsselwort final genauso wie friend - es ist legal, aber wenn Sie es verwenden, ist das Design höchstwahrscheinlich falsch. Bitte geben Sie einige Beispiele an, bei denen ein nicht ableitbares Schlüsselwort vorhanden ist Klasse wäre ein Teil eines großartigen Architektur- oder Designmusters.

54
Vorac

finaldrückt Absicht aus. Es teilt dem Benutzer einer Klasse, Methode oder Variablen mit, "Dieses Element soll sich nicht ändern, und 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, geändert werden könnte, um etwas zu tun völlig anders durch eine Unterklasse. Es ist viel besser, im Voraus zu entscheiden, welche Elemente veränderbar sein sollen und welche nicht, und die Unveränderlichkeit über final zu erzwingen.

Sie können dies auch über Kommentare und Architekturdokumente tun. Es ist jedoch immer besser, den Compiler die erzwungenen Dinge durchsetzen zu lassen, als zu hoffen, dass zukünftige Benutzer die Dokumentation lesen und befolgen.

136
Kilian Foth

Es vermeidet das Problem der fragilen Basisklasse . Jede Klasse verfügt über eine Reihe impliziter oder expliziter Garantien und Invarianten. Das Liskov-Substitutionsprinzip schreibt vor, dass alle Subtypen dieser Klasse auch alle diese Garantien bieten müssen. Es ist jedoch sehr leicht, dies zu verletzen, wenn wir nicht final verwenden. Lassen Sie uns zum Beispiel eine Passwortprüfung durchführen:

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

Wenn wir zulassen, dass diese Klasse überschrieben wird, kann eine Implementierung alle sperren, 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 ein 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 kompliziertere Klasse ihre eigenen Methoden aufruft und diese Methoden überschrieben werden können. Ich stoße manchmal darauf, wenn ich eine Datenstruktur hübsch 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 Stil 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>");
  }
}

Wenn ich jetzt für einen Moment ignoriere, 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 Unterklasse SpiffyPage erstellen, die diesen Inhalt irgendwie umschließt. Was wir hier sehen können, ist eine zufällige Anwendung des Template-Methodenmusters. Vorlagenmethoden 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 beschädigen. Es ist daher nicht wirklich sicher, die Basisklasse danach zu ändern. Dies ist nicht ersichtlich, wenn sich alle Ihre Klassen im selben 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 verschachteln. Dies wäre gleichbedeutend mit dem obigen Code, aber weitaus expliziter und 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 Parameter top ist erforderlich, um sicherzustellen, dass die Aufrufe von writeMainContent den oberen Rand der Dekorationskette durchlaufen. Dies emuliert eine Funktion der Unterklasse mit dem Namen . offene Rekursion .)

Wenn wir mehrere Dekorateure haben, können wir sie jetzt freier mischen.

Weitaus häufiger als der Wunsch, vorhandene Funktionen geringfügig 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 über alle iterieren 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 Teil unserer Schnittstelle geworden ist. Benutzer können remove() Dinge oder get() Dinge an bestimmten Indizes. Das war so gedacht? OK. Aber oft denken wir nicht sorgfältig über alle Konsequenzen nach.

Es ist daher ratsam,

  • niemals extend eine Klasse ohne sorgfältige Überlegungen.
  • markieren Sie Ihre Klassen immer als final, es sei denn, Sie möchten, dass eine Methode überschrieben wird.
  • erstellen Sie Schnittstellen, an denen Sie eine Implementierung austauschen möchten, z. für Unit-Tests.

Es gibt viele Beispiele, bei denen diese „Regel“ gebrochen 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 endgültig und müssen explizit als virtual markiert werden.
  • Sie bieten eine private Vererbung, die nicht die Schnittstelle, sondern nur die Implementierung erbt.
  • Sie erfordern, dass Basisklassenmethoden als virtuell markiert werden und dass alle Überschreibungen ebenfalls markiert werden. Dies vermeidet Probleme, bei denen eine Unterklasse eine neue Methode definiert, eine Methode mit derselben Signatur jedoch später zur Basisklasse hinzugefügt wurde, jedoch nicht als virtuell gedacht ist.
60
amon

Ich bin überrascht, dass noch niemand Effective Java, 2nd Edition von Joshua Bloch erwähnt hat (was für jedes Java Entwickler mindestens). Punkt 17 im Buch behandelt dies ausführlich und trägt den Titel: "Design und Dokument zur Vererbung oder verbieten es".

Ich werde nicht alle guten Ratschläge in dem Buch wiederholen, aber diese speziellen Absätze scheinen relevant zu sein:

Aber was ist mit gewöhnlichen Betonklassen? Traditionell sind sie weder endgültig noch für Unterklassen konzipiert und dokumentiert, aber dieser Zustand ist gefährlich. Jedes Mal, wenn eine Änderung in einer solchen Klasse vorgenommen wird, besteht die Möglichkeit, dass Clientklassen, die die Klasse erweitern, unterbrochen werden. Dies ist nicht nur ein theoretisches Problem. Es ist nicht ungewöhnlich, Fehlerberichte im Zusammenhang mit Unterklassen zu erhalten, 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, Unterklassen in Klassen zu verbieten, die nicht so konzipiert und dokumentiert sind, dass sie sicher unterklassifiziert werden können. Es gibt zwei Möglichkeiten, Unterklassen zu verbieten. Das einfachere von beiden ist es, 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.

33
Daniel Pryden

Einer der Gründe, warum final nützlich ist, besteht darin, dass sichergestellt wird, dass Sie eine Klasse nicht so unterordnen können, dass dies gegen den Vertrag der übergeordneten Klasse verstößt. Eine solche Unterklasse wäre eine Verletzung von SOLID (vor allem "L") und das Erstellen einer Klasse final verhindert dies.

Ein typisches Beispiel ist, dass es unmöglich ist, eine unveränderliche Klasse so zu unterordnen, dass die Unterklasse veränderlich wird. In bestimmten Fällen kann eine solche Verhaltensänderung zu sehr überraschenden Effekten führen, z. B. wenn Sie etwas als Schlüssel in einer Karte verwenden und denken, 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, String zu unterteilen und veränderlich zu machen (oder es nach Hause zurückrufen zu lassen, wenn jemand seine Methoden aufruft, wodurch möglicherweise vertrauliche Daten aus dem System abgerufen werden ), da diese Objekte um internen Code herum übergeben werden, der sich auf das Laden von Klassen und die Sicherheit bezieht.

Final ist manchmal auch hilfreich, um einfache Fehler wie die Wiederverwendung derselben Variablen für zwei Dinge innerhalb einer Methode usw. zu verhindern. In Scala wird empfohlen, nur val zu verwenden, was in etwa den endgültigen Variablen in Java entspricht. und tatsächlich wird jede Verwendung einer var oder einer nicht endgültigen Variablen mit Argwohn betrachtet.

Schließlich können Compiler zumindest theoretisch einige zusätzliche Optimierungen vornehmen, wenn sie wissen, dass eine Klasse oder Methode endgültig ist, da Sie beim Aufrufen einer Methode für eine endgültige Klasse genau wissen, welche Methode aufgerufen wird und nicht gehen müssen durch virtuelle Methodentabelle, um die Vererbung zu überprüfen.

21

Der zweite Grund ist 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" habe 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. Erlauben Sie einem neuen Mitarbeiter, der gerade die Universität verlassen hat, eine "TrustMePasswordCheck" -Klasse zu erstellen, die die oben genannte Klasse erweitert. Dies kann für mein System sehr schädlich sein. Diese Methoden sollten nicht überschrieben werden, das war's.

7
JoulinRouge

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 an den Stellen, an denen ich die Klasse verwende, wird davon ausgegangen, 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. Ich erreiche das, indem ich die Klasse "endgültig" 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 ist alles, was passiert, voll 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 dies gerne tun. Entfernen Sie zunächst das Schlüsselwort "final".

(Abgesehen davon ermöglicht "final" häufig erhebliche Optimierungen. Beispielsweise bedeutet in Swift "final" für eine öffentliche Klasse oder für eine Methode einer öffentlichen Klasse, dass der Compiler dies vollständig kann Sie wissen, welchen Code ein Methodenaufruf ausführen wird, und können den dynamischen Versand durch den statischen Versand ersetzen (winziger Vorteil) und häufig den statischen Versand durch Inlining ersetzen (möglicherweise großer Vorteil).

adelphus: Was ist so schwer zu verstehen über "Wenn Sie es unterordnen möchten, nehmen Sie den Quellcode, entfernen Sie das 'Finale', und es liegt in Ihrer Verantwortung"? "final" ist gleich "faire 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 Codebits 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, auf das nicht von außen zugegriffen werden soll. Wenn Sie eine Unterklasse erstellen möchten, nehmen Sie den Quellcode, ändern die Einstellungen bei Bedarf in "geschützt" und übernehmen die Verantwortung für das, was Sie tun. Wenn Sie der Meinung sind, dass Sie auf Dinge zugreifen müssen, die ich als privat markiert habe, wissen Sie besser, was Sie tun.

Beides: 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 enorm von "final", da sich die Benutzer der Bausteine ​​auf das verlassen können, was sie erhalten.

7
gnasher729

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

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

Eine Anwendung unterteilt diese Klasse in Unterklassen:

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

Alles ist in Ordnung und gut, aber jemand, der am SDK arbeitet, entscheidet, dass das Übergeben einer Methode an get dumm ist, und verbessert die Benutzeroberfläche, um die Abwärtskompatibilität zu gewährleisten.

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 zu sein, bis die Anwendung von oben mit dem neuen SDK neu kompiliert wird. Plötzlich wird die Overriden-Get-Methode nicht mehr aufgerufen und die Anfrage wird nicht gezählt.

Dies wird als fragiles Basisklassenproblem bezeichnet, da eine scheinbar harmlose Änderung zu einem Unterklassenbruch führt. Jedes Mal, wenn Änderungen an den Methoden innerhalb der Klasse aufgerufen werden, kann eine Unterklasse unterbrochen werden. Dies bedeutet, dass fast jede Änderung dazu führen kann, dass eine Unterklasse unterbrochen wird.

Final verhindert, dass jemand Ihre Klasse unterordnet. Auf diese Weise können die Methoden innerhalb der Klasse geändert werden, ohne befürchten zu müssen, dass irgendwo jemand davon abhängt, welche Methodenaufrufe ausgeführt werden.

4
Winston Ewert

Final bedeutet effektiv, dass Ihre Klasse in Zukunft sicher geändert werden kann, ohne dass sich dies auf nachgelagerte vererbungsbasierte Klassen auswirkt (da es keine gibt) oder auf Probleme mit der Thread-Sicherheit der Klasse (ich denke, es gibt Fälle, in denen das endgültige Schlüsselwort in einem Feld dies 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 als Basis stützt.

Als Beispiel schreibe ich eine Klasse namens HobbitKiller, was großartig ist, weil alle Hobbits Tricks sind und wahrscheinlich sterben sollten. Scratch 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 ins Visier zu nehmen (sie sind nicht nur Tricksie, sondern auch schnell) Sie verwenden, um Ihren Flammenwerfer zu zielen.

Drei Monate später ändere ich die Implementierung meiner Targeting-Methode. Jetzt, zu einem späteren Zeitpunkt, wenn Sie Ihre Bibliothek aktualisieren, ohne dass Sie es merken, hat sich die tatsächliche Laufzeitimplementierung Ihrer Klasse aufgrund einer Änderung der Superklassenmethode, von der Sie abhängig sind (und die Sie im Allgemeinen nicht steuern), grundlegend geändert.

Damit ich ein gewissenhafter Entwickler bin und mit meiner Klasse einen reibungslosen Hobbit-Tod für die Zukunft sicherstellen kann, muss ich sehr, sehr vorsichtig mit allen Änderungen sein, die ich an Klassen vornehme, die erweitert werden können.

Indem ich die Möglichkeit zur Erweiterung entferne, außer in Fällen, in denen ich speziell beabsichtige, die Klasse zu erweitern, erspare ich mir (und hoffentlich auch anderen) viele Kopfschmerzen.

1
Scott Taylor

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), werde ich diese endgültige setzen und jedem Unternehmen eine Methode zur Verfügung stellen, 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 so ändern, dass sein Unternehmen wegen Streiks/Wartung/aus welchem ​​Grund auch immer am 2. März geschlossen wird.

Die Berechnungen für Kundenbestellungen und Lieferungen spiegeln die Verzögerung wider 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 jedoch die Vererbung verwendet hat und ich die Klasse nicht geschützt habe, kann dies zu Verwirrung führen.

Oder nehmen wir an, sie erben und ändern die Klasse, um zu berücksichtigen, dass diese Firma samstags nicht arbeitet, wo im Land sie am Samstag zur Halbzeit arbeiten. Dann erklärt ein Erdbeben, eine Stromkrise oder ein Umstand den Präsidenten zu drei arbeitsfreien Tagen, wie es kürzlich in Venezuela geschehen ist. Wenn die Methode der geerbten Klasse bereits jeden Samstag subtrahiert wird, können meine Änderungen an der ursprünglichen Klasse dazu führen, dass derselbe Tag zweimal subtrahiert 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 beim Aufrufen einer WorkingDaysCountry-Klasse Ihre Hauptklasse und keine Unterklasse ist

0
bns

Die Verwendung von final stellt in keiner Weise eine Verletzung der Prinzipien von SOLID) dar. Leider ist es äußerst üblich, das Open/Closed-Prinzip zu interpretieren ("Software-Entitäten sollten offen sein) zur Erweiterung, aber wegen Änderung geschlossen ") als Bedeutung" anstatt eine Klasse zu ändern, sie in Unterklassen umzuwandeln und neue Funktionen hinzuzufügen ". Dies ist ursprünglich nicht damit gemeint und wird im Allgemeinen nicht als der beste Ansatz zur Erreichung ihrer Ziele angesehen .

Die beste Möglichkeit zur Einhaltung von OCP besteht darin, Erweiterungspunkte in einer Klasse zu entwerfen, indem speziell abstrakte Verhaltensweisen bereitgestellt werden, die durch Einfügen einer Abhängigkeit in das Objekt parametrisiert werden (z. B. unter Verwendung des Strategieentwurfsmusters). 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 Ihre neue Benutzeroberfläche ein weitgehend ähnliches Verhalten wie die ursprüngliche erfordert, können Sie entweder:

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