web-dev-qa-db-fra.com

Les méthodes privées ont-elles un mauvais style de référence unique?

En général, j'utilise des méthodes privées pour encapsuler des fonctionnalités qui sont réutilisées à plusieurs endroits de la classe. Mais parfois, j'ai une grande méthode publique qui peut être divisée en étapes plus petites, chacune dans sa propre méthode privée. Cela raccourcirait la méthode publique, mais je crains que forcer quiconque lit la méthode à passer à différentes méthodes privées nuira à la lisibilité.

Y a-t-il un consensus à ce sujet? Est-il préférable d'avoir de longues méthodes publiques ou de les diviser en petits morceaux même si chaque pièce n'est pas réutilisable?

141
Jordak

Non, ce n'est pas un mauvais style. En fait c'est un très bon style.

Les fonctions privées n'ont pas besoin d'exister simplement en raison de leur réutilisation. C'est certainement une bonne raison de les créer, mais il y en a une autre: la décomposition.

Considérez une fonction qui en fait trop. Elle est longue de cent lignes et impossible à raisonner.

Si vous divisez cette fonction en petits morceaux, elle "fonctionne" toujours autant, mais en plus petits morceaux. Il appelle d'autres fonctions qui devraient avoir des noms descriptifs. La fonction principale se lit presque comme un livre: faire A, puis faire B, puis faire C, etc. Les fonctions qu'elle appelle ne peuvent être appelées qu'à un seul endroit, mais maintenant elles sont plus petites. Toute fonction particulière est nécessairement en bac à sable des autres fonctions: elles ont des étendues différentes.

Lorsque vous décomposez un gros problème en problèmes plus petits, même si ces petits problèmes (fonctions) ne sont utilisés/résolus qu'une seule fois, vous obtenez plusieurs avantages:

  • Lisibilité. Personne ne peut lire une fonction monolithique et comprendre ce qu'elle fait complètement. Vous pouvez soit continuer à vous mentir, soit le diviser en morceaux de la taille d'une bouchée qui ont du sens.

  • Localité de référence. Il devient désormais impossible de déclarer et d'utiliser une variable, puis de la conserver et de l'utiliser à nouveau 100 lignes plus tard. Ces fonctions ont des portées différentes.

  • Essai. Bien qu'il soit seulement nécessaire de tester à l'unité les membres publics d'une classe, il peut être souhaitable de tester également certains membres privés. S'il existe une section critique d'une fonction longue qui pourrait bénéficier des tests, il est impossible de la tester indépendamment sans l'extraire dans une fonction distincte.

  • Modularité. Maintenant que vous avez des fonctions privées, vous pouvez en trouver une ou plusieurs qui pourraient être extraites dans une classe distincte, qu'elles soient utilisées uniquement ici ou qu'elles soient réutilisables. Au point précédent, cette classe distincte sera probablement plus facile à tester également, car elle aura besoin d'une interface publique.

L'idée de diviser le gros code en morceaux plus petits qui sont plus faciles à comprendre et à tester est un point clé de Clean Code de l'oncle Bob . Au moment de la rédaction de cette réponse, le livre avait neuf ans, mais il est tout aussi pertinent aujourd'hui qu'il l'était à l'époque.

204
user22815

C'est probablement une excellente idée!

je prends problème avec la division de longues séquences linéaires d'action en fonctions distinctes uniquement pour réduire la longueur moyenne des fonctions dans votre base de code:

function step1(){
  // ...
  step2(zarb, foo, biz);
}

function step2(zarb, foo, biz){
  // ...
  step3(zarb, foo, biz, gleep);
}

function step3(zarb, foo, biz, gleep){
  // ...
}

Vous avez maintenant ajouté des lignes de source et considérablement réduit la lisibilité totale. Surtout si vous passez maintenant beaucoup de paramètres entre chaque fonction pour garder une trace de l'état. Oui!

Cependant, si vous avez réussi à extraire une ou plusieurs lignes dans une fonction pure qui sert un seul objectif clair ( même si elle n'est appelée qu'une seule fois ), alors vous avez amélioré la lisibilité:

function foo(){
  f = getFrambulation();
  g = deglorbFramb(f);
  r = reglorbulate(g);
}

Cela ne sera probablement pas facile dans des situations réelles, mais des éléments de fonctionnalité pure peuvent souvent être révélés si vous y réfléchissez assez longtemps.

Vous saurez que vous êtes sur la bonne voie lorsque vous avez des fonctions avec des noms de verbes agréables et lorsque votre fonction parent les appelle et le tout se lit pratiquement comme un paragraphe de prose.

Ensuite, lorsque vous revenez des semaines plus tard pour ajouter plus de fonctionnalités et que vous trouvez que vous pouvez réellement réutiliser l'une de ces fonctions, puis oh, joie ravie! Quelle merveilleuse joie rayonnante!

36
DaveGauer

La réponse est que cela dépend fortement de la situation. @Snowman couvre les aspects positifs de la rupture d'une grande fonction publique, mais il est important de se rappeler qu'il peut également y avoir des effets négatifs, comme vous êtes à juste titre préoccupé.

  • De nombreuses fonctions privées avec des effets secondaires peuvent rendre le code difficile à lire et assez fragile. Cela est particulièrement vrai lorsque ces fonctions privées dépendent des effets secondaires les uns des autres. Évitez d'avoir des fonctions étroitement couplées.

  • Les abstractions fuient . Bien qu'il soit agréable de prétendre comment une opération est effectuée ou comment les données sont stockées, peu importe, il y a des cas où cela se produit et il est important de les identifier.

  • La sémantique et le contexte comptent. Si vous ne capturez pas clairement ce qu'une fonction fait en son nom, vous pouvez à nouveau réduire la lisibilité et augmenter la fragilité de la fonction publique. Cela est particulièrement vrai lorsque vous commencez à créer des fonctions privées avec un grand nombre de paramètres d'entrée et de sortie. Et tandis que de la fonction publique, vous voyez quelles fonctions privées il appelle, de la fonction privée, vous ne voyez pas quelles fonctions publiques l'appellent. Cela peut conduire à un "bug-fix" dans une fonction privée qui rompt la fonction publique.

  • Le code fortement décomposé est toujours plus clair pour l'auteur que pour les autres. Cela ne signifie pas qu'il ne peut pas encore être clair pour les autres, mais il est facile de dire qu'il est parfaitement logique que bar() doit être appelé avant foo() au moment de l'écriture .

  • Réutilisation dangereuse. Votre fonction publique a probablement limité les entrées possibles pour chaque fonction privée. Si ces hypothèses d'entrée ne sont pas correctement capturées (la documentation de TOUTES les hypothèses est difficile), quelqu'un peut réutiliser de manière incorrecte l'une de vos fonctions privées, introduisant des bogues dans la base de code.

Décomposer les fonctions en fonction de leur cohésion interne et de leur couplage, et non de leur longueur absolue.

19
dlasalle

C'est un défi d'équilibre.

Plus fin est meilleur

La méthode privée fournit effectivement un nom pour le code contenu, et parfois une signature significative (à moins que la moitié de ses paramètres soient complètement ad hoc tramp data avec des interdépendances peu claires et non documentées).

Donner des noms aux constructions de code est généralement bon, tant que les noms suggèrent un contrat significatif pour l'appelant, et que le contrat de la méthode privée correspond exactement à ce que le nom suggère.

En se forçant à penser à des contrats significatifs pour de plus petites portions du code, le développeur initial peut repérer certains bugs et les éviter sans douleur avant même tout test de développement. Cela ne fonctionne que si le développeur s'efforce de nommer de manière concise (son simple mais précis) et est prêt à adapter les limites de la méthode privée afin que la dénomination concise soit même possible.

La maintenance ultérieure est également facilitée, car des noms supplémentaires aident à rendre le code auto-documenté .

  • Contrats sur de petits morceaux de code
  • Les méthodes de niveau supérieur se transforment parfois en une courte séquence d'appels, dont chacun fait quelque chose d'important et de nom distinctif - accompagné de la mince couche de gestion générale des erreurs la plus externe. Une telle méthode peut devenir une ressource de formation précieuse pour quiconque doit rapidement devenir un expert de la structure globale du module.

Jusqu'à ce qu'il devienne trop fin

Est-il possible de trop donner des noms à de petits morceaux de code et de se retrouver avec trop de méthodes privées trop petites? Sûr. Un ou plusieurs des symptômes suivants indiquent que les méthodes deviennent trop petites pour être utiles:

  • Trop de surcharges qui ne représentent pas vraiment des signatures alternatives pour la même logique essentielle, mais plutôt une seule pile d'appels fixe.
  • Synonymes utilisés simplement pour faire référence au même concept à plusieurs reprises ("divertissant nommage")
  • Beaucoup de décorations de nommage superficielles telles que XxxInternal ou DoXxx, surtout s'il n'y a pas de schéma unifié pour les introduire.
  • Noms maladroits presque plus longs que l'implémentation elle-même, comme LogDiskSpaceConsumptionUnlessNoUpdateNeeded
2
Jirka Hanika

À mon humble avis, la valeur de l'extraction de blocs de code uniquement comme moyen de briser la complexité serait souvent liée à la différence de complexité entre:

  1. Une description complète et précise en langage humain de ce que fait le code, y compris comment il gère les cas d'angle, et

  2. Le code lui-même.

Si le code est beaucoup plus compliqué que la description, le remplacement du code en ligne par un appel de fonction peut rendre le code environnant plus facile à comprendre. D'un autre côté, certains concepts peuvent être exprimés de façon plus lisible dans un langage informatique que dans un langage humain. Je considérerais w=x+y+z;, Par exemple, comme plus lisible que w=addThreeNumbersAssumingSumOfFirstTwoDoesntOverflow(x,y,z);.

À mesure qu'une grande fonction est divisée, il y aura de moins en moins de différence entre la complexité des sous-fonctions et leurs descriptions, et l'avantage des subdivisions supplémentaires diminuera. Si les choses sont divisées au point que les descriptions seraient plus compliquées que le code, des divisions supplémentaires aggraveront le code.

2
supercat

Contrairement à ce que les autres ont dit, je dirais qu'une longue méthode publique est une odeur de conception qui est pas rectifiée par décomposition en méthodes privées.

Mais parfois, j'ai une grande méthode publique qui pourrait être divisée en étapes plus petites

Si tel est le cas, je dirais que chaque étape devrait être son propre citoyen de première classe avec une seule responsabilité chacun. Dans un paradigme orienté objet, je suggérerais de créer une interface et une implémentation pour chaque étape de manière à ce que chacune ait une responsabilité unique facilement identifiable et puisse être nommée de manière à ce que la responsabilité soit claire. Cela vous permet de tester à l'unité la méthode (anciennement) grand public, ainsi que chaque étape individuelle indépendamment les unes des autres. Ils devraient également tous être documentés.

Pourquoi ne pas se décomposer en méthodes privées? Voici quelques raisons:

  • Couplage serré et testabilité. En réduisant la taille de votre méthode publique, vous avez amélioré sa lisibilité, mais tout le code est toujours étroitement couplé. Vous pouvez tester individuellement les méthodes privées individuelles (en utilisant les fonctionnalités avancées d'un framework de test), mais vous ne pouvez pas facilement tester la méthode publique indépendamment des méthodes privées. Cela va à l'encontre des principes des tests unitaires.
  • Taille et complexité des classes. Vous avez réduit la complexité d'une méthode, mais vous avez augmenté la complexité de la classe. La méthode publique est plus facile à lire, mais la classe est désormais plus difficile à lire car elle a plus de fonctions qui définissent son comportement. Ma préférence va aux petites classes à responsabilité unique, donc une longue méthode est un signe que la classe en fait trop.
  • Ne peut pas être réutilisé facilement. Il arrive souvent qu'à mesure qu'un corps de code arrive à maturité, la réutilisabilité est utile. Si vos étapes sont dans des méthodes privées, elles ne peuvent pas être réutilisées ailleurs sans d'abord les extraire d'une manière ou d'une autre. De plus, cela peut encourager le copier-coller lorsqu'une étape est nécessaire ailleurs.
  • Le fractionnement de cette manière est susceptible d'être arbitraire. Je dirais que le fractionnement d'une longue méthode publique ne prend pas autant de réflexion ou de conception que si vous divisiez les responsabilités en classes. Chaque classe doit être justifiée par un nom, une documentation et des tests appropriés, tandis qu'une méthode privée ne reçoit pas autant d'attention.
  • Cache le problème. Vous avez donc décidé de diviser votre méthode publique en petites méthodes privées. Maintenant, il n'y a plus de problème! Vous pouvez continuer à ajouter de plus en plus d'étapes en ajoutant de plus en plus de méthodes privées! Au contraire, je pense que c'est un problème majeur. Il établit un modèle pour ajouter de la complexité à une classe qui sera suivi par des corrections de bogues et des implémentations de fonctionnalités ultérieures. Bientôt, vos méthodes privées se développeront et elles devront être divisées.

mais je crains que forcer quiconque lit la méthode à passer à différentes méthodes privées nuira à la lisibilité

C'est un argument que j'ai eu récemment avec l'un de mes collègues. Il soutient que le fait d'avoir le comportement entier d'un module dans le même fichier/méthode améliore la lisibilité. Je suis d'accord que le code est plus facile à suivre quand tous ensemble, mais le code est moins facile à raisonner sur à mesure que la complexité augmente. Au fur et à mesure qu'un système grandit, il devient difficile de raisonner sur l'ensemble du module dans son ensemble. Lorsque vous décomposez une logique complexe en plusieurs classes chacune avec une seule responsabilité, il devient alors beaucoup plus facile de raisonner sur chaque partie.

1
Samuel