web-dev-qa-db-fra.com

Est-il correct de diviser les fonctions et méthodes longues en plus petites, même si elles ne seront appelées par rien d'autre?

Dernièrement, j'ai essayé de diviser les méthodes longues en plusieurs méthodes courtes.

Par exemple: J'ai une fonction process_url() qui divise les URL en composants puis les affecte à certains objets via leurs méthodes. Au lieu d'implémenter tout cela dans une seule fonction, je prépare uniquement l'URL à fractionner dans process_url(), puis je la transmets à la fonction process_components(), qui passe ensuite les composants à assign_components() une fonction.

Au début, cela semblait améliorer la lisibilité, car au lieu d'énormes méthodes et fonctions de "Dieu", j'en avais de plus petites avec des noms plus descriptifs. Cependant, en parcourant le code que j'ai écrit de cette façon, j'ai constaté que je n'ai maintenant aucune idée si ces petites fonctions sont appelées par d'autres fonctions ou méthodes.

Suite de l'exemple précédent: quelqu'un qui regarde le code peut penser que la fonctionnalité process_components() est abstraite dans une fonction car elle est appelée par différentes méthodes et fonctions, alors qu'en fait il n'est appelé que par process_url().

Cela semble quelque peu faux. L'alternative est de toujours écrire de longues méthodes et fonctions, mais indiquez leurs sections avec des commentaires.

La technique de division des fonctions que j'ai décrite est-elle incorrecte? Quelle est la meilleure façon de gérer les grandes fonctions et méthodes?

MISE À JOUR: Ma principale préoccupation est que l'abstraction du code dans une fonction pourrait impliquer qu'il pourrait être appelé par plusieurs autres fonctions.

VOIR ÉGALEMENT: discussions sur reddit à / r/programmation (fournit une perspective différente plutôt que la plupart des réponses ici) et / r/readablecode .

165
sbichenko

Il est difficile de tester du code qui fait beaucoup de choses.

Le débogage de code qui fait beaucoup de choses est difficile.

La solution à ces deux problèmes est d'écrire du code qui ne fait pas beaucoup de choses. Écrivez chaque fonction pour qu'elle fasse une seule et unique chose. Cela les rend faciles à tester avec un test unitaire (on n'a pas besoin d'une dizaine de douzaines de tests unitaires).

Un de mes collègues a la phrase qu'il utilise pour juger si une méthode donnée doit être divisée en plus petites:

Si, lors de la description de l'activité du code à un autre programmeur, vous utilisez les mots "et", la méthode doit être divisée en au moins une autre partie.

Tu as écrit:

J'ai une fonction process_url () qui divise les URL en composants et puis les affecte à certains objets via leurs méthodes.

Cela devrait être au moins deux méthodes. Il est correct de les envelopper dans une méthode publique, mais le fonctionnement devrait être deux méthodes différentes.

230
user40980

Oui, la division des fonctions longues est normale. C'est une façon de faire qui est encouragée par Robert C. Martin dans son livre Clean Code . En particulier, vous devriez choisir des noms très descriptifs pour vos fonctions, comme une forme de code auto-documenté.

77
Scott Whitlock

Comme les gens l'ont souligné, cela améliore la lisibilité. Une personne qui lit process_url() peut voir plus clairement quel est le processus général pour gérer les URL simplement en lisant quelques noms de méthode.

Le problème est que d'autres personnes peuvent penser que ces fonctions sont utilisées par une autre partie du code, et si certaines d'entre elles doivent être modifiées, elles peuvent choisir de conserver ces fonctions et d'en définir de nouvelles. Cela signifie que certains codes deviennent inaccessibles.

Il existe plusieurs façons de résoudre ce problème. La première est la documentation et les commentaires dans le code. Deuxièmement, des outils qui fournissent des tests de couverture. Dans tous les cas, cela dépend en grande partie du langage de programmation, voici quelques-unes des solutions que vous pouvez appliquer en fonction du langage de programmation:

  • les langages orientés objet peuvent permettre de définir certaines méthodes privées, pour s'assurer qu'elles ne sont pas utilisées ailleurs
  • les modules dans d'autres langues peuvent spécifier quelles fonctions sont visibles de l'extérieur, etc.
  • des langages de très haut niveau comme Python peut éliminer la nécessité de définir plusieurs fonctions car elles seraient de toute façon de simples lignes
  • d'autres langages comme Prolog peuvent exiger (ou suggèrent fortement) la définition d'un nouveau prédicat pour chaque saut conditionnel.
  • dans certains cas, il est courant de définir des fonctions auxiliaires à l'intérieur de la fonction qui les utilise (fonctions locales), parfois ce sont des fonctions anonymes (fermetures de code), cela est courant dans les fonctions de rappel Javascript.

Bref, le fractionnement en plusieurs fonctions est généralement une bonne idée en termes de lisibilité. Cela peut ne pas être vraiment bon si les fonctions sont très courtes et cela crée l'effet goto ou si les noms ne sont pas vraiment descriptifs, dans ce cas, la lecture de code nécessiterait de sauter parmi les fonctions, ce qui peut être désordonné. À propos de vos préoccupations concernant la portée et l'utilisation de ces fonctions, il existe plusieurs façons de les gérer qui dépendent généralement de la langue.

En général, le meilleur conseil est d'utiliser le bon sens. Toute règle stricte est très probablement erronée dans certains cas et, en fin de compte, cela dépend de la personne. Je considérerais cela comme lisible:

process_url = lambda url: dict(re.findall('([^?=&]*)=([^?=&]*)', url))

Personnellement, je préfère une seule ligne même si elle est légèrement complexe plutôt que de sauter et de rechercher dans plusieurs fichiers de code, si cela me prend plus de trois secondes pour trouver une autre partie de code que je ne connais même pas qu'est-ce que je vérifiais de toute façon. Les personnes qui ne souffrent pas de TDAH peuvent préférer des noms plus explicatifs dont elles peuvent se souvenir, mais à la fin ce que vous faites toujours est d'équilibrer la complexité aux différents niveaux du code, des lignes, des paragraphes, des fonctions, des fichiers, des modules, etc.

Le mot-clé est donc solde. Une fonction à mille lignes est un enfer pour quiconque la lit, car il n'y a pas d'encapsulation et le contexte devient tout simplement trop énorme. Une fonction divisée en mille fonctions chacune avec une seule ligne peut être pire:

  • vous avez des noms (que vous auriez pu fournir comme commentaires dans les lignes)
  • vous éliminez (espérons-le) les variables globales et n'avez pas à vous soucier de l'état (avec une transparence référentielle)
  • mais vous forcez les lecteurs à sauter d'avant en arrière.

Il n'y a donc pas de balles d'argent ici, mais de l'expérience et de l'équilibre. À mon humble avis, la meilleure façon d'apprendre à le faire est de lire beaucoup de code écrit par d'autres personnes et d'analyser pourquoi il est difficile pour vous de le lire et comment le rendre plus lisible. Cela fournirait une expérience précieuse.

39
Trylks

Je dirais que ça dépend.

Si vous le divisez simplement pour le fractionner et les appeler des noms comme process_url_partN et ainsi de suite, alors NON , veuillez ne pas le faire. Il est simplement plus difficile de suivre plus tard lorsque vous ou quelqu'un d'autre devez comprendre ce qui se passe.

Si vous extrayez des méthodes à des fins claires qui peuvent être testées par elles-mêmes et qui ont du sens par elles-mêmes (même si personne d'autre ne les utilise) alors OUI .


Pour votre objectif particulier, il semble que vous ayez deux objectifs.

  1. Analyser une URL et renvoyer une liste de ses composants.
  2. Faites quelque chose avec ces composants.

J'écrirais la première partie séparément et lui ferais revenir un résultat assez général qui pourrait être facilement testé et potentiellement réutilisé plus tard. Encore mieux, je chercherais une fonction intégrée qui le fait déjà dans votre langage/framework et que j'utiliserais à la place à moins que votre analyse ne soit super spéciale. Si c'est super spécial, je l'écrirais toujours en tant que méthode distincte, mais probablement en le regroupant en tant que méthode privée/protégée dans la classe qui gère la seconde (si vous écrivez du code orienté objet).

La deuxième partie que j'écrirais comme son propre composant qui utilise la première pour l'analyse d'URL.

17
Svish

Je n'ai jamais rencontré de problème avec d'autres développeurs divisant des méthodes plus grandes en méthodes plus petites car c'est un modèle que je suis moi-même. La méthode "Dieu" est un piège terrible à tomber et d'autres qui sont moins expérimentés ou qui s'en moquent ont tendance à se faire prendre plus souvent qu'autrement. Cela étant dit...

Il est extrêmement important d'utiliser les identifiants d'accès appropriés sur les méthodes plus petites. C'est frustrant de trouver une classe jonchée de petites méthodes publiques car je perds totalement confiance en trouvant où/comment la méthode est utilisée dans l'application.

Je vis en C # -land donc nous avons public, private, protected, internal, et voir ces mots me montre sans l'ombre d'un doute le portée de la méthode et où je dois chercher des appels. Si c'est privé, je sais que la méthode est utilisée dans une seule classe et j'ai pleine confiance lors du refactoring.

Dans le monde Visual Studio, avoir plusieurs solutions (.sln) exacerbe cet anti-modèle car les assistants IDE/Resharper "Find Usages" ne trouveront pas les usages en dehors de la solution ouverte.

14
Ryan Rodemoyer

Si votre langage de programmation le prend en charge, vous pourrez peut-être définir vos fonctions "d'assistance" dans le cadre de votre fonction process_url() pour obtenir les avantages de lisibilité de fonctions distinctes. par exemple.

function process_url(url) {

    function foo(a) {
        // ...
    }

    function bar(a) {
        // ...
    }

    return [ foo(url), bar(url) ];

}

Si votre langage de programmation ne le prend pas en charge, vous pouvez déplacer foo() et bar() hors de la portée de process_url() (afin qu'il soit visible pour les autres fonctions/méthodes) - mais considérez cela comme un "hack" que vous avez mis en place car votre langage de programmation ne prend pas en charge cette fonctionnalité.

La décomposition d'une fonction en sous-fonctions dépendra probablement de l'existence de noms significatifs/utiles pour les parties et de la taille de chacune des fonctions, entre autres considérations.

11
mjs

Si quelqu'un s'intéresse à la littérature sur cette question: c'est exactement ce que Joshua Kerievsky appelle la "méthode de composition" dans son "Refactoring to Patterns" (Addison-Wesley):

Transformez la logique en un petit nombre d'étapes révélatrices d'intentions au même niveau de détail.

Je crois que l'imbrication correcte des méthodes en fonction de leur "niveau de détail" est importante ici.

Voir un extrait sur le site de l'éditeur:

Une grande partie du code que nous écrivons ne commence pas par être simple. Pour le rendre simple, nous devons réfléchir à ce qui n'est pas simple à ce sujet et demander continuellement: "Comment pourrait-il être plus simple?" Nous pouvons souvent simplifier le code en considérant une solution complètement différente. Les refactorisations de ce chapitre présentent différentes solutions pour simplifier les méthodes, les transitions d'états et les arborescences.

Compose Method (123) consiste à produire des méthodes qui communiquent efficacement ce qu'elles font et comment elles font ce qu'elles font. Une méthode composée [Beck, SBPP] consiste en des appels à des méthodes bien nommées qui sont toutes au même niveau de détail. Si vous voulez garder votre système simple, essayez d'appliquer Méthode de composition (123) partout ...

http://ptgmedia.pearsoncmg.com/images/ch7_9780321213358/elementLinks/07fig01.jpg

Addendum: Kent Beck ( "Patterns d'implémentation" ) l'appelle "méthode composée". Il vous conseille de:

[c] ose des méthodes à partir d'appels à d'autres méthodes, chacune étant à peu près au même niveau d'abstraction. Un des signes d'une méthode mal composée est un mélange de niveaux d'abstraction [.]

Là, encore une fois, l'avertissement de ne pas mélanger les différents niveaux d'abstraction (soulignement le mien).

9
Sebastian

Une bonne règle est d'avoir des abstractions à proximité à des niveaux similaires (mieux formulé par sebastian dans cette réponse juste au-dessus.)

C'est à dire. si vous avez une (grande) fonction qui traite des choses de bas niveau, mais faites également des choix de niveau supérieur, essayez de factoriser les choses de bas niveau:

void foo() {

     if(x) {
       y = doXformanysmallthings();
     }

     z = doYforthings(y);

     if (z != y && isFullmoon()) {
         launchSpacerocket()
     }
}

Il est généralement préférable de déplacer des éléments vers des fonctions plus petites que d'avoir beaucoup de boucles et autres à l'intérieur d'une fonction qui se compose de quelques "grandes" étapes conceptuelles. (Sauf si vous pouvez combiner cela en expressions LINQ/foreach/lambda relativement petites ...)

5
Macke

Si vous pouviez concevoir une classe adaptée à ces fonctions, rendez-les privées. Autrement dit, avec une définition de classe appropriée, vous ne pouvez exposer que ce que vous avez besoin d'exposer.

4
Tom Haley

Je suis sûr que ce ne sera pas l'opinion populaire, mais c'est tout à fait correct. La localité de référence peut être une aide précieuse pour vous assurer que vous et les autres comprenez la fonction (dans ce cas, je fais référence au code et non à la mémoire en particulier).

Comme pour tout, c'est un équilibre. Vous devriez être plus préoccupé par quiconque vous dit "toujours" ou "jamais".

4
Fred

Considérez cette fonction simple (j'utilise une syntaxe semblable à Scala mais j'espère que l'idée sera claire sans aucune connaissance de Scala):

def myFun ... {
    ...
    if (condition1) {
        ...
    } else {
        ...
    }
    if (condition2) {
        ...
    } else {
        ...
    }
    if (condition3) {
        ...
    } else {
        ...
    }
    // rest
    ...
}

Cette fonction a jusqu'à 8 chemins possibles pour exécuter votre code, selon l'évaluation de ces conditions.

  • Cela signifie que vous aurez besoin de 8 tests différents juste pour tester cette partie. De plus, il est très probable que certaines combinaisons ne seront pas possibles, et ensuite vous devrez analyser attentivement quelles sont-elles (et assurez-vous de ne pas en manquer certaines qui sont possibles) - beaucoup de travail.
  • Il est très difficile de raisonner sur le code et son exactitude. Étant donné que chacun des blocs if et son état peuvent dépendre de certaines variables locales partagées, afin de savoir ce qui se passe après eux, toute personne travaillant avec le code doit analyser tous ces blocs de code et les 8 chemins d'exécution possibles. Il est très facile de faire une erreur ici et il est fort probable que quelqu'un qui met à jour le code va manquer quelque chose et introduire un bug.

D'un autre côté, si vous structurez le calcul comme

def myFun ... {
    ...
    val result1 = myHelper1(...);
    val result2 = myHelper2(...);
    val result3 = myHelper3(...);
    // rest
    ...
}

private def myHelper1(/* arguments */): SomeResult = {
    if (condition1) {
        ...
    } else {
        ...
    }
}
// Similarly myHelper2 and myHelper3

vous pouvez:

  • Testez facilement chacune des fonctions d'assistance, chacune d'entre elles n'a que deux chemins d'exécution possibles.
  • Lors de l'examen de myFun, il est immédiatement évident si result2 Dépend de result1 (Il suffit de vérifier si l'appel à myHelper2(...) l'utilise pour calculer l'un des arguments. (En supposant que les assistants n'utilisent pas un état global.) Il est également évident comment qu'ils sont dépendants, ce qui est beaucoup plus difficile à comprendre dans le cas précédent. De plus, après les trois appels, il est également clair à quoi ressemble l'état du calcul - il est capturé uniquement dans result1, result2 et result3 - pas besoin de vérifier si/quelles autres variables locales ont été modifiées.
3
Petr Pudlák

La responsabilité plus concrète d'une méthode, plus facile à tester, à lire et à maintenir sera le code. Bien qu'aucun autre ne les appelle.

Si à l'avenir, vous devez utiliser cette fonctionnalité à partir d'autres endroits, vous pouvez ensuite extraire facilement ces méthodes.

2
lucasvc