Pour rédiger un guide de style de codage, comment juger les méthodes finales de conception de logiciels?
Par finale, je veux dire le sens orienté objet qu'une classe qui peut être sous-classée fournit des méthodes, et cette classe utilisant une syntaxe spéciale pour empêcher les sous-classes de modifier certaines méthodes.
Dans des langages comme Java, il existe d'autres façons d'empêcher les modifications de comportement, telles que la finalisation des méthodes ou l'utilisation de méthodes statiques (le cas échéant).
À titre d'exemple, ce guide de style comporte une section "Aucune méthode ou classe finale" https://doc.nuxeo.com/corg/Java-code-style/
Aucune méthode ou classe finale
Cela entrave la réutilisabilité. Nuxeo est une plate-forme et nous ne savons jamais quand il sera utile de sous-classer quelque chose.
Aucune méthode ou champ privé ou package-privé.
Cela entrave la réutilisabilité, pour la même raison que ci-dessus.
D'autre part, Guava a des classes avec des méthodes finales, comme https://guava.dev/releases/19.0/api/docs/com/google/common/collect/AbstractIterator.html
Le JDK (Java) a certaines classes avec très peu de méthodes finales, comme ArrayList, AbstractList, certaines avec plusieurs méthodes finales comme HashMap, et certaines avec de nombreuses méthodes finales, comme AbstractPipeline.
Certaines personnes diront que cela se rapporte au principe ouvert-fermé ( Clarifie le principe ouvert/fermé ), mais les articles sur ce sujet ne parlent généralement pas de final méthodes.
Un autre angle est que cela est lié au débat sur la conception plutôt que sur l'héritage ( Pourquoi devrais-je préférer la composition à l'héritage? ), car l'idée peut être de fournir fonctionnalité via l'héritage lorsque l'on envisage de rendre les méthodes finales.
Assez lié:
Je pense que les arguments présentés dans le billet de blog d'Eric Lippert de 2004 Pourquoi tant de classes de framework sont-elles scellées? s'appliquent aux "méthodes finales" (au sens Java de Java ce terme) également.
Chaque fois que vous écrivez une méthode A (dans un cadre) qui appelle une méthode non finale B, A ne peut plus compter sur B pour faire ce qu'elle faisait à l'origine, donc A doit être conçu de manière plus robuste que si B était final . Par exemple, l'implémentation A peut devoir investir plus d'efforts dans la gestion des exceptions (lors de l'appel de B), le comportement exact de B et les contraintes qui s'appliquent lors de son remplacement doivent être documentés plus précisément, et A doit être testé plus en profondeur avec différents remplacements. variantes de B. De plus, il faut investir davantage dans la distribution exacte des responsabilités entre A et B.
En fait, dans une classe abstraite d'un framework, la façon dont les méthodes surchargeables sont utilisées en interne devient une partie de l'API de ce framework (voir l'exemple dans les commentaires de @wchargin). Une fois le cadre publié dans le monde, il devient beaucoup plus difficile de changer la sémantique de ces méthodes.
Cela en fait donc un compromis: en rendant B final, vous faciliterez la création d'une implémentation correcte, testée et fiable de la méthode A, et vous faciliterez la refactorisation A et B dans le cadre plus tard, mais vous le rendez également plus difficile pour étendre A. Et si le guide d'implémentation d'un framework favorise la réalisation de rien final, je serais très sceptique quant à la fiabilité de ce logiciel.
Permettez-moi de citer le dernier paragraphe d'Eric, qui s'applique parfaitement ici:
Il y a évidemment un compromis ici. Le compromis est entre permettre aux développeurs de gagner un peu de temps en leur permettant de traiter n'importe quel objet ancien comme un sac de propriété d'une part, et de développer un cadre bien conçu, OOPtacular, complet, robuste, sécurisé, prévisible et testable dans un quantité raisonnable de temps - et je vais pencher fortement vers ce dernier. Parce que tu sais quoi? Ces mêmes développeurs vont se plaindre amèrement si le framework que nous leur donnons les ralentit car il est à moitié cuit, cassant, non sécurisé et pas entièrement testé!
Cette question plus ancienne (et sa première réponse) de 2014 peut également être une excellente réponse ici:
En C #, les méthodes sont "finales" par défaut (dans le sens Java de ce terme), et il faut ajouter explicitement le mot clé virtual
pour les rendre redéfinissables. En Java , c'est l'inverse: chaque méthode est "virtuelle" par défaut, et il faut les marquer comme final
pour éviter cela.
La meilleure réponse à cette ancienne question cite Anders Hejlsberg pour expliquer les différentes "écoles de pensée" derrière ces approches:
l'école de pensée qu'il appelle "académique" ("Tout devrait être virtuel, car je pourrais vouloir un jour l'emporter."), vs.
l'école de pensée "pragmatique" ("Nous devons faire très attention à ce que nous rendons virtuel.")
Permettez-moi enfin de dire que les arguments de ce dernier me semblent plus convaincants, mais YMMV.
L'héritage est un outil puissant; et comme tous les outils puissants, il peut être facile d’abuser. Il est donc important de considérer dans chaque cas si quelque chose doit être définitif ou non.
Il est tentant de penser, comme le guide de style cité le semble, qu'en rendant tout ouvert et extensible, vous rendez les choses aussi faciles que possible pour les sous-classes, et en laissant leurs options aussi ouvertes que possible. Cela semble logique (comme le savent tous ceux qui ont essayé d'étendre une classe tierce trop verrouillée). Mais ce n'est pas aussi simple que ça…
L'un des principaux problèmes rencontrés pour rendre tout ouvert et extensible est connu sous le nom de problème de classe de base fragile .
Plus les deux classes sont étroitement couplées, plus il est probable que les modifications apportées à l'une cassent l'autre. Et une sous-classe qui a un accès complet à tous les éléments internes de la superclasse est en effet très étroitement couplée. Des modifications innocentes de la superclasse peuvent conduire à des sous-classes qui se compilent toujours, mais se comportent mal. (En particulier si une méthode de superclasse est modifiée pour appeler ou pour arrêter d'appeler une autre.)
C'est réparable si les deux classes sont sous votre contrôle direct; mais s'ils proviennent de différents modules ou bibliothèques, et surtout si différentes organisations en sont responsables, cela peut être un problème très grave.
C'est pourquoi Joshua Bloch recommande (dans l'article 17 de son livre très apprécié Effective Java ; article 19 dans la 3e édition) que vous devriez Conception et document pour l'héritage ou bien l'interdire . Il approfondit certains de ces problèmes et les actions à entreprendre pour tenter de les éviter, comme éviter d'appeler des méthodes remplaçables dans les constructeurs, examiner soigneusement les champs et les méthodes à exposer, documenter soigneusement leur comportement, y compris tous les appels automatiques, fournir des méthodes d'assistance judicieusement choisies pour des raisons de performances, en écrivant plusieurs sous-classes pour tester la classe et en s'engageant dans ce que vous avez documenté pour la vie de la classe. Il conclut que La meilleure solution à ce problème est d'interdire le sous-classement d'une manière qui n'est pas conçue et documentée pour être sous-classée en toute sécurité.
(C'est l'une des principales raisons pour lesquelles Kotlin inverse la politique de Java et rend les méthodes finales par défaut, nécessitant un mot clé open
pour les rendre redéfinissables.)
Il vaut donc la peine de réfléchir soigneusement à l'héritage lors de l'écriture d'une classe qui pourrait être étendue. Vous pouvez toujours ouvrir plus de choses à l'avenir; c'est beaucoup plus difficile d'aller dans l'autre sens.