web-dev-qa-db-fra.com

Pourquoi le mot-clé «final» serait-il jamais utile?

Il semble Java a eu le pouvoir de déclarer des classes non dérivables pour les âges, et maintenant C++ l'a aussi. Cependant, à la lumière du principe Open/Close dans SOLID, pourquoi serait-ce Pour moi, le mot clé final ressemble à friend - il est légal, mais si vous l'utilisez, la conception est probablement incorrecte. Veuillez fournir quelques exemples où un non-dérivable la classe ferait partie d'un grand modèle d'architecture ou de conception.

54
Vorac

finalexprime l'intention. Il indique à l'utilisateur d'une classe, d'une méthode ou d'une variable "Cet élément n'est pas censé changer, et si vous voulez le changer, vous n'avez pas compris la conception existante."

Ceci est important car l'architecture du programme serait vraiment très difficile si vous deviez prévoir que chaque classe et chaque méthode que vous écrivez pourrait être modifiée pour faire quelque chose complètement différent par une sous-classe. Il est préférable de décider à l'avance quels éléments sont censés être modifiables et lesquels ne le sont pas, et d'appliquer l'inchangeabilité via final.

Vous pouvez également le faire via des commentaires et des documents d'architecture, mais il est toujours préférable de laisser le compilateur appliquer les choses qu'il peut plutôt que d'espérer que les futurs utilisateurs liront et obéiront à la documentation.

136
Kilian Foth

Il évite le Problème de classe de base fragile . Chaque classe est livrée avec un ensemble de garanties et d'invariants implicites ou explicites. Le principe de substitution de Liskov stipule que tous les sous-types de cette classe doivent également fournir toutes ces garanties. Cependant, il est vraiment facile de violer cela si nous n'utilisons pas final. Par exemple, ayons un vérificateur de mot de passe:

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

Si nous permettons à cette classe d'être remplacée, une implémentation pourrait verrouiller tout le monde, une autre pourrait donner à tout le monde un accès:

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

Ce n'est généralement pas correct, car les sous-classes ont maintenant un comportement très incompatible avec l'original. Si nous avons vraiment l'intention d'étendre la classe avec d'autres comportements, une chaîne de responsabilité serait mieux:

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;
  }
}

Le problème devient plus apparent lorsqu'une classe plus compliquée appelle ses propres méthodes, et ces méthodes peuvent être remplacées. Je rencontre parfois ce problème lors de l'impression d'une structure de données ou de l'écriture HTML. Chaque méthode est responsable d'un widget.

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>");
  }

  ...
}

Je crée maintenant une sous-classe qui ajoute un peu plus de style:

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>");
  }
}

Maintenant, en ignorant un instant que ce n'est pas un très bon moyen de générer des pages HTML, que se passe-t-il si je veux encore changer la mise en page? Il faudrait que je crée une sous-classe SpiffyPage qui enveloppe en quelque sorte ce contenu. Ce que nous pouvons voir ici est une application accidentelle du modèle de méthode du modèle. Les méthodes de modèle sont des points d'extension bien définis dans une classe de base qui sont destinés à être remplacés.

Et que se passe-t-il si la classe de base change? Si le contenu HTML change trop, cela pourrait casser la disposition fournie par les sous-classes. Il n'est donc pas vraiment sûr de changer la classe de base par la suite. Cela n'est pas apparent si toutes vos classes sont dans le même projet, mais très visible si la classe de base fait partie d'un logiciel publié sur lequel d'autres personnes s'appuient.

Si cette stratégie d'extension était prévue, nous aurions pu permettre à l'utilisateur de changer la façon dont chaque pièce est générée. Soit, il pourrait y avoir une stratégie pour chaque bloc qui peut être fournie en externe. Ou, nous pourrions imbriquer des décorateurs. Ce serait équivalent au code ci-dessus, mais beaucoup plus explicite et beaucoup plus flexible:

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);
    ...
  }
}

(Le paramètre supplémentaire top est nécessaire pour s'assurer que les appels à writeMainContent passent par le haut de la chaîne de décoration. Cela émule une fonction de sous-classement appelée récursivité ouverte .)

Si nous avons plusieurs décorateurs, nous pouvons désormais les mélanger plus librement.

Beaucoup plus souvent que le désir d'adapter légèrement les fonctionnalités existantes est le désir de réutiliser une partie d'une classe existante. J'ai vu un cas où quelqu'un voulait une classe où vous pourriez ajouter des éléments et répéter sur chacun d'eux. La bonne solution aurait été de:

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
}

Au lieu de cela, ils ont créé une sous-classe:

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

Cela signifie soudain que toute l'interface de ArrayList est devenue une partie de notre interface . Les utilisateurs peuvent remove() choses, ou get() choses à des indices spécifiques. C'était prévu comme ça? D'ACCORD. Mais souvent, nous ne réfléchissons pas soigneusement à toutes les conséquences.

Il convient donc de

  • jamais extend une classe sans réflexion.
  • marquez toujours vos classes comme final sauf si vous avez l'intention de remplacer une méthode.
  • créer des interfaces où vous souhaitez échanger une implémentation, par exemple pour les tests unitaires.

Il existe de nombreux exemples où cette "règle" doit être enfreinte, mais elle vous guide généralement vers une bonne conception flexible et évite les bogues dus à des changements involontaires dans les classes de base (ou à des utilisations involontaires de la sous-classe comme instance de la classe de base ).

Certaines langues ont des mécanismes d'application plus stricts:

  • Toutes les méthodes sont finales par défaut et doivent être marquées explicitement comme virtual
  • Ils fournissent un héritage privé qui n'hérite pas de l'interface mais uniquement de l'implémentation.
  • Ils nécessitent que les méthodes de classe de base soient marquées comme virtuelles et nécessitent également que tous les remplacements soient marqués. Cela évite les problèmes où une sous-classe a défini une nouvelle méthode, mais une méthode avec la même signature a été ajoutée par la suite à la classe de base, mais pas conçue comme virtuelle.
60
amon

Je suis surpris que personne n'ait encore mentionné Java efficace, 2e édition par Joshua Bloch (qui devrait être une lecture obligatoire pour chaque Java développeur au moins). L'article 17 du livre en discute en détail et s'intitule: "Concevoir et documenter pour l'héritage ou bien l'interdire".

Je ne répéterai pas tous les bons conseils du livre, mais ces paragraphes particuliers semblent pertinents:

Mais qu'en est-il des classes de béton ordinaires? Traditionnellement, ils ne sont ni définitifs, ni conçus et documentés pour le sous-classement, mais cet état de fait est dangereux. Chaque fois qu'un changement est effectué dans une telle classe, il y a une chance que les classes clientes qui étendent la classe se cassent. Ce n'est pas seulement un problème théorique. Il n'est pas rare de recevoir des rapports de bogues liés au sous-classement après avoir modifié les éléments internes d'une classe concrète non finale qui n'a pas été conçue et documentée pour l'héritage.

La meilleure solution à ce problème est d'interdire le sous-classement dans les classes qui ne sont pas conçues et documentées pour être sous-classées en toute sécurité. Il existe deux façons d'interdire le sous-classement. Le plus simple des deux est de déclarer la classe finale. L'alternative est de rendre tous les constructeurs privés ou package-privés et d'ajouter des usines publiques statiques à la place des constructeurs. Cette alternative, qui offre la flexibilité d'utiliser des sous-classes en interne, est discutée au point 15. L'une ou l'autre approche est acceptable.

33
Daniel Pryden

L'une des raisons pour lesquelles final est utile est qu'elle garantit que vous ne pouvez pas sous-classer une classe d'une manière qui violerait le contrat de la classe parente. Un tel sous-classement serait une violation de SOLID (surtout "L") et la création d'une classe final l'empêche.

Un exemple typique rend impossible la sous-classe d'une classe immuable d'une manière qui rendrait la sous-classe mutable. Dans certains cas, un tel changement de comportement peut entraîner des effets très surprenants, par exemple lorsque vous utilisez quelque chose comme clés dans une carte en pensant que la clé est immuable alors qu'en réalité vous utilisez une sous-classe qui est mutable.

En Java, de nombreux problèmes de sécurité intéressants pourraient être introduits si vous pouviez sous-classer String et le rendre mutable (ou le faire rappeler à la maison lorsque quelqu'un appelle ses méthodes, ce qui pourrait éventuellement extraire des données sensibles du système ) car ces objets sont transmis autour de certains codes internes liés au chargement et à la sécurité des classes.

Final est également parfois utile pour éviter des erreurs simples telles que la réutilisation de la même variable pour deux choses dans une méthode, etc. Dans Scala, nous vous encourageons à utiliser uniquement val qui correspond à peu près aux variables finales en Java, et en fait, toute utilisation d'une variable var ou non finale est considérée avec suspicion.

Enfin, les compilateurs peuvent, au moins en théorie, effectuer des optimisations supplémentaires lorsqu'ils savent qu'une classe ou une méthode est finale, car lorsque vous appelez une méthode sur une classe finale, vous savez exactement quelle méthode sera appelée et vous n'aurez pas à y aller. via une table de méthode virtuelle pour vérifier l'héritage.

21

La deuxième raison est performance . La première raison est que certaines classes ont des comportements ou des états importants qui ne sont pas censés être modifiés pour permettre au système de fonctionner. Par exemple, si j'ai une classe "PasswordCheck" et pour construire cette classe, j'ai embauché une équipe d'experts en sécurité et cette classe communique avec des centaines de guichets automatiques avec des procols bien étudiés et définis. Autoriser un nouveau type embauché à la sortie de l'université à faire une classe "TrustMePasswordCheck" qui prolonge la classe ci-dessus pourrait être très dangereux pour mon système; ces méthodes ne sont pas censées être remplacées, c'est tout.

7
JoulinRouge

Quand j'ai besoin d'un cours, j'écrirai un cours. Si je n'ai pas besoin de sous-classes, je m'en fiche des sous-classes. Je m'assure que ma classe se comporte comme prévu, et les endroits où j'utilise la classe supposent que la classe se comporte comme prévu.

Si quelqu'un veut sous-classer ma classe, je veux entièrement nier toute responsabilité pour ce qui se passe. J'y parviens en rendant la classe "finale". Si vous voulez le sous-classer, rappelez-vous que je n'ai pas pris en compte le sous-classement pendant que j'écrivais la classe. Vous devez donc prendre le code source de la classe, supprimer le "final", et à partir de là, tout ce qui se passe est entièrement sous votre responsabilité.

Vous pensez que ce n'est "pas orienté objet"? J'ai été payé pour faire un cours qui fait ce qu'il est censé faire. Personne ne m'a payé pour avoir fait un cours qui pouvait être sous-classé. Si vous êtes payé pour rendre ma classe réutilisable, vous pouvez le faire. Commencez par supprimer le mot clé "final".

(En dehors de cela, "final" permet souvent des optimisations substantielles. Par exemple, dans Swift "final" sur une classe publique, ou sur une méthode d'une classe publique, signifie que le compilateur peut pleinement savoir quel code un appel de méthode exécutera et peut remplacer la répartition dynamique par une répartition statique (avantage minime) et souvent remplacer la répartition statique par une réinsertion (peut-être un avantage énorme)).

adelphus: Qu'est-ce qui est si difficile à comprendre "si vous voulez le sous-classer, prendre le code source, supprimer le" final ", et c'est votre responsabilité"? "final" équivaut à "avertissement équitable".

Et je ne suis pas payé pour faire du code réutilisable. Je suis payé pour écrire du code qui fait ce qu'il est censé faire. Si je suis payé pour faire deux bits de code similaires, j'extrais les parties communes parce que c'est moins cher et je ne suis pas payé pour perdre mon temps. Rendre le code réutilisable qui n'est pas réutilisé est une perte de temps.

M4ks: Vous faites toujours tout ce qui n'est pas censé être accessible de l'extérieur. Encore une fois, si vous voulez sous-classer, vous prenez le code source, changez les choses en "protégées" si vous en avez besoin et prenez la responsabilité de ce que vous faites. Si vous pensez avoir besoin d'accéder à des choses que j'ai marquées comme privées, vous savez mieux ce que vous faites.

Les deux: le sous-classement est une toute petite partie de la réutilisation du code. La création de blocs de construction qui peuvent être adaptés sans sous-classement est beaucoup plus puissante et bénéficie énormément de "final" car les utilisateurs des blocs peuvent compter sur ce qu'ils obtiennent.

7
gnasher729

Imaginons que le SDK pour une plateforme soit livré avec la classe suivante:

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

Une application sous-classe cette classe:

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

Tout va bien, mais quelqu'un qui travaille sur le SDK décide que passer une méthode à get est idiot et rend l'interface meilleure en veillant à appliquer la compatibilité descendante.

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);
}

Tout semble bien, jusqu'à ce que l'application d'en haut soit recompilée avec le nouveau SDK. Soudain, la méthode get surchargée n'est plus appelée et la demande n'est pas comptée.

C'est ce qu'on appelle le problème de la classe de base fragile, car un changement apparemment inoffensif entraîne une rupture de sous-classe. À tout moment, le changement des méthodes appelées dans la classe peut entraîner la rupture d'une sous-classe. Cela signifie que presque tout changement peut entraîner la rupture d'une sous-classe.

Final empêche quiconque de sous-classer votre classe. De cette façon, quelles méthodes à l'intérieur de la classe peuvent être modifiées sans se soucier que quelque part quelqu'un dépend exactement des appels de méthode qui sont effectués.

4
Winston Ewert

Final signifie effectivement que votre classe peut changer à l'avenir sans impact sur les classes basées sur l'héritage en aval (car il n'y en a pas), ni sur les problèmes de sécurité des threads de la classe (je pense qu'il y a des cas où le mot-clé final sur un champ empêche certains thread-high jinx).

Final signifie que vous êtes libre de changer le fonctionnement de votre classe sans aucun changement involontaire de comportement se glissant dans le code des autres qui s'appuie sur le vôtre comme base.

À titre d'exemple, j'écris une classe appelée HobbitKiller, ce qui est génial, car tous les hobbits sont astucieux et devraient probablement mourir. Grattez cela, ils ont tous absolument besoin de mourir.

Vous l'utilisez comme classe de base et ajoutez une nouvelle méthode impressionnante pour utiliser un lance-flammes, mais utilisez ma classe comme base parce que j'ai une excellente méthode pour cibler les hobbits (en plus d'être astucieux, ils sont rapides), ce qui vous utilisez pour aider à viser votre lance-flammes.

Trois mois plus tard, je change la mise en œuvre de ma méthode de ciblage. Maintenant, à un moment ultérieur, lorsque vous mettrez à niveau votre bibliothèque, à votre insu, l'implémentation d'exécution réelle de votre classe a fondamentalement changé en raison d'un changement dans la méthode de super classe dont vous dépendez (et ne contrôle généralement pas).

Donc, pour que je sois un développeur consciencieux et que je garantisse la mort du hobbit dans le futur en utilisant ma classe, je dois être très, très prudent avec tous les changements que j'apporte à toutes les classes qui peuvent être étendues.

En supprimant la possibilité d'étendre, sauf dans les cas où j'ai l'intention de prolonger la classe, je me sauve (et j'espère que les autres) beaucoup de maux de tête.

1
Scott Taylor

Pour moi, c'est une question de design.

Supposons que j'ai un programme qui calcule les salaires des employés. Si j'ai une classe qui renvoie le nombre de jours ouvrables entre 2 dates en fonction du pays (une classe pour chaque pays), je mettrai cette dernière et fournirai une méthode pour chaque entreprise de fournir une journée gratuite uniquement pour leurs calendriers.

Pourquoi? Facile. Supposons qu'un développeur souhaite hériter de la classe de base WorkingDaysUSA dans une classe WorkingDaysUSAmyCompany et la modifier pour indiquer que son entreprise sera fermée pour grève/maintenance/pour quelque raison que ce soit le 2 mars.

Les calculs des commandes et des livraisons des clients refléteront le retard et fonctionneront en conséquence lors de l'exécution, ils appellent WorkingDaysUSAmyCompany.getWorkingDays (), mais que se passe-t-il lorsque je calcule le temps de vacances? Dois-je ajouter le 2 mars comme vacances pour tout le monde? Non. Mais puisque le programmeur a utilisé l'héritage et que je n'ai pas protégé la classe, cela peut entraîner une confusion.

Ou disons qu'ils héritent et modifient la classe pour refléter que cette entreprise ne fonctionne pas le samedi alors que dans le pays ils travaillent à mi-temps le samedi. Ensuite, un tremblement de terre, une crise de l'électricité ou une autre circonstance oblige le président à déclarer 3 jours chômés comme cela s'est produit récemment au Venezuela. Si la méthode de la classe héritée a déjà été soustraite chaque samedi, mes modifications sur la classe d'origine pourraient conduire à soustraire deux fois le même jour. Je devrais aller dans chaque sous-classe sur chaque client et vérifier que toutes les modifications sont compatibles.

Solution? Rendez la classe finale et fournissez une méthode addFreeDay (companyID mycompany, Date freeDay). De cette façon, vous êtes sûr que lorsque vous appelez une classe WorkingDaysCountry, c'est votre classe principale et non une sous-classe

0
bns

L'utilisation de final n'est en aucun cas une violation des principes SOLID. Il est malheureusement extrêmement courant d'interpréter le principe Open/Closed ("les entités logicielles doivent être ouvertes pour extension mais fermée pour modification ") comme signifiant" plutôt que de modifier une classe, de la sous-classer et d'ajouter de nouvelles fonctionnalités ". .

La meilleure façon de se conformer à l'OCP est de concevoir des points d'extension dans une classe, en fournissant spécifiquement des comportements abstraits qui sont paramétrés en injectant une dépendance à l'objet (par exemple en utilisant le modèle de conception de stratégie). Ces comportements doivent être conçus pour utiliser une interface afin que les nouvelles implémentations ne reposent pas sur l'héritage.

Une autre approche consiste à implémenter votre classe avec son API publique en tant que classe abstraite (ou interface). Vous pouvez alors produire une implémentation entièrement nouvelle qui peut se connecter aux mêmes clients. Si votre nouvelle interface nécessite un comportement globalement similaire à l'original, vous pouvez soit:

  • utiliser le modèle de conception Decorator pour réutiliser le comportement existant de l'original, ou
  • refactorisez les parties du comportement que vous souhaitez conserver dans un objet d'assistance et utilisez le même assistant dans votre nouvelle implémentation (la refactorisation n'est pas une modification).
0
Jules