Pourquoi le mot clé "final" serait-il utile?

54

Il semble que Java ait eu le pouvoir de déclarer des classes non-dérivables depuis des siècles, et maintenant, C ++ l’a aussi. Cependant, à la lumière du principe Open / Close de SOLID, pourquoi cela serait-il utile? Pour moi, le finalmot - clé sonne juste friend- il est légal, mais si vous l'utilisez, le design est probablement faux. Veuillez fournir quelques exemples dans lesquels une classe non-dérivable ferait partie d'une excellente architecture ou modèle de conception.

Vorac
la source
43
Pourquoi pensez-vous qu'une classe est mal conçue si elle est annotée final? Beaucoup de gens (y compris moi-même) trouvent que c'est un bon design pour faire chaque classe non abstraite final.
Repéré
20
Privilégiez la composition à l'héritage et vous pouvez avoir toutes les classes non abstraites final.
Andy
24
Le principe d'ouverture / fermeture est en quelque sorte un anachronisme du XXe siècle, alors que le mantra devait établir une hiérarchie de classes héritées de classes héritées d'autres classes. C’était bien d’enseigner la programmation orientée objet, mais il s’est avéré utile de créer des dégâts incontrôlables et incontrôlables lorsqu’il s’applique à des problèmes concrets. Concevoir une classe pour qu'elle soit extensible est difficile.
David Hammen
34
@DavidArno Ne soyez pas ridicule. L'héritage est la condition sine qua non de la programmation orientée objet, et il est loin d'être aussi compliqué ou désordonné que certains individus trop dogmatiques aiment prêcher. C'est un outil, comme un autre, et un bon programmeur sait utiliser le bon outil pour le travail.
Mason Wheeler
48
En tant que développeur en rétablissement, il me semble me rappeler que final est un outil brillant pour empêcher les comportements préjudiciables d’entrer dans des sections critiques. Je semble aussi me souvenir que l'héritage est un outil puissant à bien des égards. Il est presque comme si gasp différents outils ont des avantages et des inconvénients et nous comme les ingénieurs ont un équilibre entre ces facteurs que nous produisons notre logiciel!
CorsiKa

Réponses:

136

final exprime 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 souhaitez le modifier, vous n'avez pas compris la conception existante."

Ceci est important car l'architecture du programme serait très difficile si vous deviez anticiper que chaque classe et chaque méthode que vous écrivez pourraient être modifiées pour faire quelque chose de complètement différent avec une sous-classe. Il est beaucoup mieux de décider dès le départ quels éléments sont supposés être modifiables et lesquels ne le sont pas, et de faire en sorte que rien ne change 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 faire respecter ce qu’il peut, au lieu d’espérer que les futurs utilisateurs liront et respecteront la documentation.

Kilian Foth
la source
14
Mais quiconque a déjà écrit une classe de base largement réutilisée (comme un framework ou une médiathèque) sait qu'il ne faut pas s'attendre à ce que les programmeurs d'applications se comportent de manière saine. Ils vont subvertir, mal utiliser et déformer votre invention d'une manière que vous n'auriez même pas imaginée comme étant possible, à moins que vous ne la verrouilliez avec une poignée de fer.
Kilian Foth,
8
@KilianFoth Ok, mais honnêtement, comment se passe votre problème avec ce que font les programmeurs d'applications?
Coredump
20
@coredump Les personnes qui utilisent ma bibliothèque créent mal de mauvais systèmes. Les mauvais systèmes génèrent de mauvaises réputations. Les utilisateurs ne pourront pas distinguer le bon code de Kilian de l'application monstrueusement instable de Fred Random. Résultat: je perds en crédits de programmation et en clients. Rendre votre code difficile à utiliser est une question d’argent et de centimes.
Kilian Foth,
21
La déclaration "Cet élément n'est pas censé changer, et si vous voulez le changer, vous n'avez pas compris la conception existante." est incroyablement arrogant et pas l'attitude que je voudrais dans n'importe quelle bibliothèque avec laquelle j'ai travaillé. Si seulement j'avais un sou pour chaque fois qu'une bibliothèque ridiculement surencapsulée ne me laissait aucun mécanisme pour changer un élément de l'état intérieur qui devait être changé parce que l'auteur n'avait pas anticipé un cas d'utilisation important ...
Mason Wheeler
31
@ JimmyB Règle de base: Si vous pensez savoir à quoi servira votre création, vous vous trompez déjà. Bell a conçu le téléphone comme un système essentiellement de Muzak. Kleenex a été inventé dans le but de retirer le maquillage plus facilement. Thomas Watson, président d'IBM, a déjà déclaré: "Je pense qu'il existe un marché mondial pour peut-être cinq ordinateurs." Le représentant Jim Sensenbrenner, qui a présenté la loi PATRIOT en 2001, a déclaré publiquement que le but était spécifiquement d' empêcher la NSA de faire ce qu'elle "fait avec l'autorité de la loi PATRIOT". Et ainsi de suite ...
Mason Wheeler
59

Cela évite le problème de la 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, établissons 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 l'accès:

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

Cela n’est généralement pas acceptable, car les sous-classes ont maintenant un comportement très incompatible avec le comportement initial. Si nous souhaitons réellement que la classe soit étendue à un autre comportement, une chaîne de responsabilité serait préférable:

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 évident quand plus d'une classe compliquée appelle ses propres méthodes, et ces méthodes peuvent être remplacées. Je rencontre parfois cela lorsque joliment imprimer une structure de données ou écrire du 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>");
  }
}

Ignorant pour l'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 souhaite modifier à nouveau la mise en page? Je devrais créer une SpiffyPagesous-classe qui en quelque sorte encapsule ce contenu. Ce que nous pouvons voir ici est une application accidentelle du modèle de méthode template. Les méthodes de modèle sont des points d'extension bien définis dans une classe de base 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 endommager la mise en page fournie par les sous-classes. Il n’est donc pas très prudent de changer de classe de base par la suite. Cela n'est pas évident 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 d’échanger la manière dont chaque partie est générée. Soit une stratégie pour chaque bloc pouvant être fournie à l’extérieur. Ou, nous pourrions nidifier les décorateurs. Cela 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 topparamètre supplémentaire est nécessaire pour garantir que les appels writeMainContentpassent par le haut de la chaîne de décorateurs. Cela émule une fonctionnalité de sous-classement appelée récursion ouverte .)

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

Bien 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 pouvez ajouter des éléments et les parcourir tous. 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 soudainement que toute l'interface de ArrayListest devenue une partie de notre interface. Les utilisateurs peuvent des remove()choses, ou des get()choses à des index spécifiques. C'était prévu comme ça? D'ACCORD. Mais souvent, nous ne réfléchissons pas soigneusement à toutes les conséquences.

Il est donc conseillé de

  • jamais extendune classe sans réflexion.
  • marquez toujours vos classes comme finalsauf si vous souhaitez que l'une des méthodes soit remplacée.
  • créez des interfaces sur lesquelles vous souhaitez échanger une implémentation, par exemple pour les tests unitaires.

Il existe de nombreux exemples dans lesquels cette «règle» doit être enfreinte, mais elle vous guide généralement vers une conception flexible et de bonne qualité, et évite les bogues dus à des modifications inattendues dans les classes de base (ou à des utilisations non intentionnelles de la sous-classe en tant qu'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 seulement de la mise en œuvre.
  • Ils exigent que les méthodes de la classe de base soient marquées comme virtuelles et que tous les remplacements soient également marqués. Cela évite les problèmes dans lesquels une sous-classe définit une nouvelle méthode, mais une méthode avec la même signature est ensuite ajoutée à la classe de base, mais n'est pas conçue comme virtuelle.
Amon
la source
3
Vous méritez au moins +100 pour avoir mentionné le "problème de la classe de base fragile". :)
David Arno
6
Je ne suis pas convaincu par les points soulevés ici. Oui, la classe de base fragile est un problème, mais final ne résout pas tous les problèmes liés à la modification d'une implémentation. Votre premier exemple est mauvais parce que vous supposez que vous connaissez tous les cas d'utilisation possibles de PasswordChecker ("verrouiller tout le monde ou autoriser l'accès de tout le monde ... n'est pas correct" - dit qui?). Votre dernière liste "donc souhaitable ..." est vraiment mauvaise - vous préconisez fondamentalement de ne rien étendre et de tout marquer comme finale - ce qui efface complètement l'utilité de la POO, de l'héritage et de la réutilisation de code.
Adelphus
4
Votre premier exemple n'est pas un exemple du problème de la classe de base fragile. Dans le problème de la classe de base fragile, les modifications apportées à une classe de base cassent une sous-classe. Mais dans cet exemple, votre sous-classe ne suit pas le contrat d'une sous-classe. Ce sont deux problèmes différents. (En outre, il est raisonnable de désactiver un vérificateur de mot de passe (par exemple pour le développement) dans certaines circonstances.)
Winston Ewert
5
"Cela évite le problème de la classe de base fragile" - de la même manière que de se tuer évite d'avoir faim.
user253751
9
@immibis, c’est plus éviter de manger pour éviter l’intoxication alimentaire. Bien sûr, ne jamais manger serait un problème. Mais manger uniquement dans les endroits où vous faites confiance a beaucoup de sens.
Winston Ewert
33

Je suis surpris que personne n'ait encore mentionné Effective Java, 2e édition de Joshua Bloch (qui devrait au moins être lu par tous les développeurs Java). Le point 17 du livre traite de cette question en détail et s'intitule: " Concevez et documentez en vue de l'héritage ou interdisez-le ".

Je ne répéterai pas tous les bons conseils du livre, mais ces paragraphes 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'une modification est apportée dans une telle classe, il y a une chance que les classes clientes qui étendent la classe se cassent. Ce n'est pas simplement un problème théorique. Il n'est pas rare de recevoir des rapports de bogues liés aux sous-classes après avoir modifié les éléments internes d'une classe concrète non finale qui n'a pas été conçue ni documentée pour l'héritage.

La meilleure solution à ce problème consiste à interdire les sous-classes dans les classes non conçues ni documentées comme telles. Il y a deux façons d'interdire les sous-classes. Le plus facile des deux est de déclarer la classe finale. L'alternative consiste à rendre tous les constructeurs privés ou paquet-privés et à ajouter des usines statiques publiques à la place des constructeurs. Cette option, qui offre la possibilité d’utiliser des sous-classes en interne, est examinée au point 15. L’une ou l’autre des méthodes est acceptable.

Daniel Pryden
la source
21

L’une des raisons finalest utile, c’est que cela garantit que vous ne pouvez pas sous-classer une classe de manière à violer le contrat de la classe parente. Un tel sous-classement constituerait une violation de SOLID (surtout "L") et le fait qu’une classe l’ finalempêche.

Un exemple typique consiste à rendre 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 avoir des effets très surprenants, par exemple lorsque vous utilisez quelque chose comme clé dans une carte en pensant que la clé est immuable, alors qu'en réalité, vous utilisez une sous-classe qui est modifiable.

En Java, beaucoup de problèmes de sécurité intéressants pourraient être introduits si vous pouviez sous String- classer et le rendre mutable (ou le rappeler à la maison quand quelqu'un appelle ses méthodes, extrayant ainsi éventuellement des données sensibles du système) à mesure que ces objets sont passés. autour d’un code interne lié au chargement et à la sécurité des classes.

Final est aussi 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, il est conseillé d'utiliser uniquement la valvariable correspondant approximativement aux variables finales en Java, et en réalité toute utilisation d'un varou variable non finale est regardé avec suspicion.

Enfin, les compilateurs peuvent, du moins en théorie, effectuer des optimisations supplémentaires s’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 la table de méthode virtuelle pour vérifier l'héritage.

Michał Kosmulski
la source
6
Enfin, les compilateurs peuvent, du moins en théorie => avoir personnellement passé en revue la passe de dévirtualisation de Clang, et je confirme qu’elle est utilisée dans la pratique.
Matthieu M.
Mais le compilateur ne peut-il pas dire à l'avance que personne ne remplace une classe ou une méthode, qu'elle soit ou non marquée finale?
JesseTG
3
@ JesseTG S'il a accès à tout le code à la fois, probablement. Qu'en est-il de la compilation de fichiers séparés, cependant?
Rétablir Monica
3
La dévirtualisation @JesseTG ou la mise en cache inline (monomorphe / polymorphe) est une technique courante dans les compilateurs JIT, car le système sait quelles classes sont actuellement chargées et peut désoptimiser le code si l'hypothèse d'absence de méthode dominante s'avère fausse. Cependant, un compilateur en avance ne le peut pas. Lorsque je compile une classe Java et un code utilisant cette classe, je peux ultérieurement compiler une sous-classe et transmettre une instance au code consommateur. Le moyen le plus simple consiste à ajouter un autre fichier jar à l'avant du classpath. Le compilateur ne peut pas connaître tout cela, car cela se produit au moment de l'exécution.
amon
5
@MTilsted Vous pouvez supposer que les chaînes sont immuables, car la mutation de chaînes par réflexion peut être interdite via a SecurityManager. La plupart des programmes ne l'utilisent pas, mais ils n'exécutent également aucun code non approuvé. +++ Vous devez supposer que les chaînes sont immuables, sinon vous n'obtiendrez aucune sécurité et un tas de bogues arbitraires en prime. Tout programmeur qui suppose que les chaînes Java peuvent changer en code productif est certainement fou.
Maaartinus
7

La deuxième raison est la performance . La première raison est que certaines classes ont des comportements ou des états importants qui ne sont pas supposés être modifiés pour permettre au système de fonctionner. Par exemple, si j'ai une classe "PasswordCheck" et pour la construire, j'ai embauché une équipe d'experts en sécurité et cette classe communique avec des centaines de distributeurs automatiques de billets avec des procols bien étudiés et définis. Permettre à un nouvel employé récemment sorti de l'université de créer une classe "TrustMePasswordCheck" qui étend la classe ci-dessus pourrait être très préjudiciable pour mon système; ces méthodes ne sont pas censées être remplacées, c'est tout.

JoulinRouge
la source
1
bancomat = ATM: en.wikipedia.org/wiki/Cash_machine
tar
La machine virtuelle Java est suffisamment intelligente pour traiter les classes en tant que finales si elles ne possèdent aucune sous-classe, même si elles ne sont pas déclarées finales.
user253751
Moins voté parce que la revendication de «performance» a été réfutée à maintes reprises.
Paul Smith
7

Quand j'ai besoin d'un cours, j'écris 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 que 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 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-groupe pendant que j’écrivais le cours. Vous devez donc prendre le code source de la classe, supprimer le "final", et à partir de là, tout ce qui se passe relève de votre entière responsabilité .

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

(Autre que 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 sait parfaitement quel code un appel de méthode va exécuter, et peut remplacer l’envoi dynamique par l’envoi statique (avantage minime) et remplace souvent l’envoi statique par un alignement (avantage potentiellement énorme)).

adelphus: Qu'est-ce qui est si difficile à comprendre à propos de "si vous voulez le sous-classer, prenez le code source, supprimez le" final ", et c'est votre responsabilité"? "final" équivaut à "fair warning".

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

M4ks: Vous faites toujours tout ce qui est privé et qui n'est pas censé être accessible de l'extérieur. Encore une fois, si vous voulez créer une sous-classe, vous prenez le code source, modifiez les éléments en "protégé" 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 qualifiées de privées, vous devez savoir ce que vous faites.

Les deux: Le sous-classement est une infime partie du code de réutilisation. Créer des blocs de construction qui peuvent être adaptés sans sous-classe est beaucoup plus puissant et profite énormément de "final", car les utilisateurs des blocs peuvent compter sur ce qu'ils obtiennent.

gnasher729
la source
4
-1 Cette réponse décrit tout ce qui ne va pas chez les développeurs de logiciels. Si quelqu'un veut réutiliser votre classe en sous-classant, laissez-le. Pourquoi serait-ce votre responsabilité de savoir comment ils l'utilisent (ou en abusent)? Fondamentalement, vous utilisez final car vous n'utilisez pas ma classe. * "Personne ne m'a payé pour avoir fait une classe qui pourrait être sous-classée" . Es-tu sérieux? C'est exactement pourquoi les ingénieurs logiciels sont employés - pour créer un code solide et réutilisable.
Adelphus
4
-1
Simplifiez
3
@ Philadelphus Bien que le libellé de cette réponse soit brutal, à la limite d'un dur, ce n'est pas un "mauvais" point de vue. En fait, c'est le même point de vue que la majorité des réponses sur cette question jusqu'à présent avec un ton moins clinique.
NemesisX00
+1 pour mentionner que vous pouvez supprimer "final". Il est arrogant de réclamer la connaissance préalable de toutes les utilisations possibles de votre code. Cependant, il est humble de préciser que vous ne pouvez pas maintenir certaines utilisations possibles, et que ces utilisations nécessiteraient de maintenir une fourche.
Mars
4

Imaginons que le SDK d'une plate-forme embarque 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 qu'il getest ridicule de transmettre une méthode à l'interface et améliore l'interface en veillant à ce que la compatibilité en amont soit appliquée.

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 aller bien, jusqu'à ce que l'application d'en haut soit recompilée avec le nouveau SDK. Soudainement, la méthode get overriden n'est plus appelée et la demande n'est pas comptabilisée.

C'est ce qu'on appelle le problème de la classe de base fragile, car un changement apparemment inoffensif entraîne la rupture d'une sous-classe. Toute modification des méthodes appelées à l'intérieur de la classe peut entraîner la rupture d'une sous-classe. Cela tend à dire que presque tous les changements peuvent causer la rupture d’une sous-classe.

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

Winston Ewert
la source
1

Final signifie effectivement que votre classe pourra être modifiée à l'avenir sans impact sur les classes basées sur l'héritage en aval (car il n'y en a pas), ou sur tout problème lié à la sécurité des threads de la classe (je pense qu'il existe des cas où le mot clé final sur un champ empêche certains threads à haute jinx).

Final signifie que vous êtes libre de changer le fonctionnement de votre classe sans que des changements de comportement involontaires ne se glissent dans le code d’autrui reposant sur le vôtre.

À titre d’exemple, j’écris une classe appelée HobbitKiller, ce qui est excellent, car tous les hobbits sont des fous et devraient probablement mourir. Grattez ça, ils ont tous définitivement besoin de mourir.

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

Trois mois plus tard, je change d'implémentation de ma méthode de ciblage. Désormais, à un moment ultérieur de votre mise à niveau de votre bibliothèque, l'implémentation d'exécution réelle de votre classe a été fondamentalement modifiée à cause d'une modification de la méthode de la super classe dont vous dépendez (et que vous ne contrôlez généralement pas).

Donc, pour que je sois un développeur consciencieux et que je veille à la disparition sans heurts des hobbits en utilisant ma classe, je dois faire très attention aux modifications que je fais aux classes qui peuvent être étendues.

En supprimant la possibilité d’agrandir, sauf dans les cas où j’ai précisément l’intention d’allonger la classe, je sauve beaucoup de maux de tête (ainsi que d’autres).

Scott Taylor
la source
2
Si votre méthode de ciblage va changer, pourquoi l'avez-vous déjà rendue publique? Et si vous modifiez le comportement d'une classe de manière significative, vous avez besoin d'une autre version plutôt que d'une surprotectionfinal
M4ks
Je ne sais pas pourquoi cela changerait. Les Hobbits sont astucieux et le changement était nécessaire. Le fait est que si je le construis en tant que final, j'empêche l'héritage, qui protège les autres personnes contre le fait que mes modifications infectent leur code.
Scott Taylor
0

L'utilisation de finaln'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 à l’extension mais fermées à la modification") comme signifiant "plutôt que de modifier une classe, de la sous-classer et d’ajouter de nouvelles fonctionnalités". Ce n’est pas ce qu’on voulait au départ, et on considère généralement que ce n’est pas la meilleure approche pour atteindre ses objectifs.

Le meilleur moyen de se conformer à OCP est de concevoir des points d’extension dans une classe, en fournissant spécifiquement des comportements abstraits paramétrés en injectant une dépendance à l’objet (par exemple, à l’aide du modèle de conception 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 ensuite produire une implémentation entièrement nouvelle pouvant être connectée aux mêmes clients. Si votre nouvelle interface nécessite un comportement globalement similaire à celui de l'original, vous pouvez soit:

  • utilisez le motif de conception du décorateur pour réutiliser le comportement existant de l'original, ou
  • refactoriser les parties du comportement que vous souhaitez conserver dans un objet utilitaire et utiliser le même utilitaire dans votre nouvelle implémentation (le refactoring n'est pas une modification).
Jules
la source
0

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 à chaque entreprise pour que chaque entreprise dispose d'un jour de congé gratuit.

Pourquoi? Facile. Supposons qu'un développeur veuille hériter de la classe de base WorkingDaysUSA d'une classe WorkingDaysUSAmyCompany et le modifie pour refléter le fait que son entreprise sera fermée pour grève / maintenance / quelle que soit la raison le 2 mars.

Les calculs pour les commandes clients et la livraison reflèteront le retard et fonctionneront en conséquence lorsqu'ils appelleront WorkingDaysUSAmyCompany.getWorkingDays (), mais que se passera-t-il lorsque je calculerai le temps des vacances? Devrais-je ajouter le 2 mars comme jour férié pour tout le monde? Non, mais comme le programmeur a utilisé l'héritage et que je n'ai pas protégé la classe, cela peut prêter à confusion.

Ou bien, disons qu'ils héritent et modifient la classe pour refléter le fait que cette entreprise ne travaille pas le samedi, alors que dans le pays, ils travaillent à mi-temps le samedi. Puis un tremblement de terre, une crise de l’électricité ou une circonstance quelconque oblige le président à déclarer trois jours non ouvrables comme il s’est passé récemment au Venezuela. Si la méthode de la classe héritée est déjà soustraite chaque samedi, mes modifications sur la classe d'origine pourraient conduire à soustraire deux fois le même jour. Je devrais aller à chaque sous-classe sur chaque client et vérifier 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 certain que lorsque vous appelez une classe WorkingDaysCountry, c'est votre classe principale et non une sous-classe.

bns
la source