Les fonctions longues sont-elles acceptables si elles ont une structure interne?

23

Lorsque je traite des algorithmes compliqués dans des langages prenant en charge les fonctions imbriquées (comme Python et D), j'écris souvent des fonctions énormes (car l'algorithme est compliqué), mais j'atténue cela en utilisant des fonctions imbriquées pour structurer le code compliqué. Les énormes fonctions (plus de 100 lignes) sont-elles toujours considérées comme mauvaises même si elles sont bien structurées en interne via l'utilisation de fonctions imbriquées?

Edit: Pour ceux d'entre vous qui ne sont pas familiers avec Python ou D, les fonctions imbriquées dans ces langages permettent également d'accéder à l'étendue des fonctions externes. En D, cet accès permet la mutation des variables dans la portée externe. En Python, il autorise uniquement la lecture. Dans D, vous pouvez explicitement désactiver l'accès à la portée externe dans une fonction imbriquée en le déclarant static.

dsimcha
la source
5
J'écris régulièrement plus de 400 fonctions / méthodes de ligne. Je dois mettre cette déclaration de changement de cas 500 quelque part :)
Callum Rogers
7
@Callum Rogers: En Python, au lieu d'avoir 500 instructions de changement de cas, vous utiliseriez un dictionnaire / mappage de recherche. Je crois qu'ils sont supérieurs à avoir une déclaration de cas de commutation de 500 ou plus. Vous avez toujours besoin de 500 lignes de définitions de dictionnaire (alternativement, en Python, vous pouvez utiliser la réflexion pour les énumérer dynamiquement), mais la définition du dictionnaire est des données, pas du code; et il y a beaucoup moins de scrupules à avoir une grande définition de données qu'une grande définition de fonction. De plus, les données sont plus robustes que le code.
Lie Ryan
Je grince des dents à chaque fois que je vois des fonctions avec beaucoup de LOC, je me demande quel est le but de la modularité. Il est plus facile de suivre la logique et de déboguer avec des fonctions plus petites.
Aditya P
L'ancien et familier langage Pascal permet également d'accéder à la portée externe, tout comme l'extension de fonction imbriquée dans GNU C. transmis et non retourné, ce qui nécessiterait un support de fermeture lexical complet).
Kaz
Un bon exemple de fonctions qui ont tendance à être longues sont les combinateurs que vous écrivez lorsque vous effectuez une programmation réactive. Ils atteignent facilement trente lignes, mais doubleraient de taille s'ils étaient séparés car vous perdiez les fermetures.
Craig Gidney

Réponses:

21

Souvenez-vous toujours de la règle, une fonction fait une chose et la fait bien! Si vous le pouvez, évitez les fonctions imbriquées.

Il entrave la lisibilité et les tests.

Bryan Harrington
la source
9
J'ai toujours détesté cette règle car une fonction qui fait "une chose" lorsqu'elle est vue à un niveau d'abstraction élevé peut faire "beaucoup de choses" lorsqu'elle est vue à un niveau inférieur. Si elle n'est pas appliquée correctement, la règle "une chose" peut conduire à des API trop fines.
dsimcha
1
@dsimcha: Quelle règle ne peut pas être mal appliquée et entraîner des problèmes? Quand quelqu'un applique des "règles" / platitudes au-delà du point où elles sont utiles, faites-en ressortir une autre: toutes les généralisations sont fausses.
2
@Roger: une plainte légitime, sauf que à mon humble avis, la règle est souvent mal appliquée dans la pratique pour justifier des API trop fines et sur-conçues. Cela a des coûts concrets dans la mesure où l'API devient plus difficile et plus verbeuse à utiliser pour des cas d'utilisation simples et courants.
dsimcha
2
"Oui, la règle est mal appliquée, mais la règle est trop mal appliquée!"? : P
1
Ma fonction fait une chose et elle le fait très bien: à savoir occuper 27 écrans. :)
Kaz
12

Certains ont fait valoir que les fonctions courtes peuvent être plus sujettes aux erreurs que les fonctions longues .

Card et Glass (1990) soulignent que la complexité de la conception implique en réalité deux aspects: la complexité au sein de chaque composant et la complexité des relations entre les composants.

Personnellement, j'ai trouvé que le code en ligne bien commenté est plus facile à suivre (surtout lorsque vous n'êtes pas celui qui l'a écrit à l'origine) que lorsqu'il est divisé en plusieurs fonctions qui ne sont jamais utilisées ailleurs. Mais cela dépend vraiment de la situation.

Je pense que le principal point à retenir est que lorsque vous divisez un bloc de code, vous échangez un type de complexité contre un autre. Il y a probablement un endroit idéal quelque part au milieu.

Jonathan Tran
la source
Avez-vous lu "Clean Code"? Il en discute longuement. amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/…
Martin Wickman
9

Idéalement, la fonction entière devrait être visible sans avoir à faire défiler. Parfois, ce n'est pas possible. Mais si vous pouvez le décomposer en morceaux, cela facilitera la lecture du code.

Je sais que dès que j'appuie sur Page Haut / Bas ou que je passe à une autre section du code, je ne me souviens que de 7 +/- 2 choses de la page précédente. Et malheureusement, certains de ces emplacements vont être utilisés lors de la lecture du nouveau code.

J'aime toujours penser à ma mémoire à court terme comme les registres d'un ordinateur (CISC, pas RISC). Si vous avez toute la fonction sur la même page, vous pouvez aller dans le cache pour obtenir les informations requises d'une autre section du programme. Si la fonction entière ne peut pas tenir sur une page, ce serait l'équivalent de toujours pousser n'importe quelle mémoire sur le disque après chaque opération.

jsternberg
la source
3
Il vous suffit de l'imprimer sur une imprimante matricielle - voyez, 10 mètres de code à la fois :)
Ou obtenez un moniteur avec une résolution plus élevée
frogstarr78
Et utilisez-le en orientation verticale.
Calmarius
4

Pourquoi utiliser des fonctions imbriquées plutôt que des fonctions externes normales?

Même si les fonctions externes ne sont utilisées que dans votre seule fonction, autrefois grande, cela rend tout le désordre plus facile à lire:

DoThing(int x){
    x += ;1
    int y = FirstThing(x);
    x = SecondThing(x, y);
    while(spoo > fleem){
        x = ThirdThing(x+y);
    }
}

FirstThing(int x){...}
SecondThing(int x, int y){...}
ThirdThing(int z){...}
Fishtoaster
la source
2
Deux raisons possibles: encombrer l'espace de noms, et ce que j'appellerai "proximité lexicale". Ces raisons peuvent être bonnes ou mauvaises, selon votre langue.
Frank Shearar
Mais les fonctions imbriquées peuvent être moins efficaces car elles doivent prendre en charge les funargs descendants, et éventuellement des fermetures lexicales complètes. Si le langage est mal optimisé, il peut faire un travail supplémentaire pour créer un environnement dynamique à passer aux fonctions enfants.
Kaz
3

Je n'ai pas le livre devant moi en ce moment (pour citer), mais selon Code Complete, le "sweetspot" pour la durée de la fonction était d'environ 25 à 50 lignes de code selon ses recherches.

Il y a des moments où il est acceptable d'avoir de longues fonctions:

  • Lorsque la complexité cyclomatique de la fonction est faible. Vos collègues développeurs peuvent être un peu frustrés s'ils doivent regarder une fonction qui contient une instruction if géante et l'instruction else pour cela si n'est pas à l'écran en même temps.

Les moments où ce n'est pas ok d'avoir de longues fonctions:

  • Vous avez une fonction avec des conditions profondément imbriquées. Faites plaisir à vos collègues lecteurs de code, améliorez la lisibilité en décomposant la fonction. Une fonction indique à son lecteur que "c'est un bloc de code qui fait une chose". Demandez-vous également si la longueur de la fonction indique qu'elle en fait trop et qu'elle doit être prise en compte dans une autre classe.

L'essentiel est que la maintenabilité devrait être l'une des plus hautes priorités de votre liste. Si un autre développeur ne peut pas regarder votre code et obtenir un "résumé" de ce que le code fait en moins de 5 secondes, votre code ne fournit pas suffisamment de "métadonnées" pour dire ce qu'il fait. Les autres développeurs devraient être capables de dire ce que fait votre classe simplement en regardant le navigateur d'objets dans votre IDE choisi au lieu de lire plus de 100 lignes de code.

Les fonctions plus petites présentent les avantages suivants:

  • Portabilité: il est beaucoup plus facile de déplacer des fonctionnalités (dans la classe lors de la refactorisation vers une autre)
  • Débogage: lorsque vous regardez la trace de pile, il est beaucoup plus rapide de localiser une erreur si vous regardez une fonction avec 25 lignes de code plutôt que 100.
  • Lisibilité - Le nom de la fonction indique ce qu'un bloc de code entier fait. Un développeur de votre équipe peut ne pas vouloir lire ce bloc s'il ne travaille pas avec. De plus, dans la plupart des IDE modernes, un autre développeur peut mieux comprendre ce que fait votre classe en lisant les noms de fonction dans un navigateur d'objets.
  • Navigation - La plupart des IDE vous permettent de rechercher le nom des fonctions. De plus, la plupart des IDE modernes ont la possibilité de voir la source d'une fonction dans une autre fenêtre, cela donne à d'autres développeurs de regarder votre longue fonction sur 2 écrans (si les multi-moniteurs) au lieu de les faire défiler.

La liste continue.....

Korbin
la source
2

La réponse est que cela dépend, mais vous devriez probablement en faire un cours.

Lie Ryan
la source
Sauf si vous n'écrivez pas dans une OOPL, auquel cas peut-être pas :)
Frank Shearar
dsimcha a mentionné qu'ils utilisaient D et Python.
frogstarr78
Les cours sont trop souvent béquilles pour les personnes qui n'ont jamais entendu parler de choses comme les fermetures lexicales.
Kaz
2

Je n'aime pas la plupart des fonctions imbriquées. Les lambdas entrent dans cette catégorie, mais ne me signalent généralement que s'ils contiennent plus de 30 à 40 caractères.

La raison fondamentale est qu'elle devient une fonction très localement dense avec une récursion sémantique interne, ce qui signifie qui qu'il est difficile pour moi d'envelopper mon cerveau, et il est juste plus facile de pousser certaines choses vers une fonction d'aide qui n'encombre pas l'espace de code .

Je considère qu'une fonction doit faire sa chose. Faire d'autres choses, c'est ce que font les autres fonctions. Donc, si vous avez une fonction de 200 lignes faisant son affaire, et que tout coule, c'est A-OK.

Paul Nathan
la source
Parfois, vous devez transmettre tellement de contexte à une fonction externe (à laquelle elle pourrait simplement avoir un accès pratique en tant que fonction locale) qu'il y a plus de complications dans la façon dont la fonction est invoquée que ce qu'elle fait.
Kaz
1

Est ce acceptable? C'est vraiment une question à laquelle vous seul pouvez répondre. La fonction atteint-elle ce dont elle a besoin? Est-il maintenable? Est-ce «acceptable» pour les autres membres de votre équipe? Si c'est le cas, c'est ce qui compte vraiment.

Edit: je n'ai pas vu la chose sur les fonctions imbriquées. Personnellement, je ne les utiliserais pas. J'utiliserais plutôt des fonctions régulières.

GrandmasterB
la source
1

Une longue fonction "en ligne droite" peut être un moyen clair de spécifier une longue séquence d'étapes qui se produisent toujours dans une séquence particulière.

Cependant, comme d'autres l'ont mentionné, cette forme a tendance à avoir des étendues locales de complexité dans lesquelles le flux global est moins évident. Placer cette complexité locale dans une fonction imbriquée (c'est-à-dire: définie ailleurs dans la fonction longue, peut-être en haut ou en bas) peut restaurer la clarté du flux principal.

Une deuxième considération importante est de contrôler la portée des variables qui sont destinées à être utilisées uniquement dans un tronçon local d'une fonction longue. La vigilance est requise pour éviter d'avoir une variable introduite dans une section de code référencée involontairement ailleurs (par exemple, après des cycles d'édition), car ce type d'erreur n'apparaîtra pas comme une erreur de compilation ou d'exécution.

Dans certains langages, ce problème est facilement évité: un tronçon de code local peut être encapsulé dans son propre bloc, comme avec "{...}", dans lequel toutes les variables nouvellement introduites ne sont visibles que pour ce bloc. Certains langages, tels que Python, ne disposent pas de cette fonctionnalité, auquel cas les fonctions locales peuvent être utiles pour appliquer des régions de portée plus petite.

gwideman
la source
1

Non, les fonctions multi-pages ne sont pas souhaitables et ne devraient pas passer en revue le code. J'écrivais aussi de longues fonctions, mais après avoir lu le refactoring de Martin Fowler , je me suis arrêté. Les fonctions longues sont difficiles à écrire correctement, difficiles à comprendre et difficiles à tester. Je n'ai jamais vu une fonction de même 50 lignes qui ne serait pas plus facile à comprendre et à tester si elle était divisée en un ensemble de fonctions plus petites. Dans une fonction multi-page, il y a presque certainement des classes entières qui devraient être éliminées. Il est difficile d'être plus précis. Vous devriez peut-être publier une de vos longues fonctions dans Code Review et quelqu'un (peut-être moi) peut vous montrer comment l'améliorer.

Kevin Cline
la source
0

Quand je programme en python, j'aime prendre du recul après avoir écrit une fonction et me demander si elle adhère au "Zen of Python" (tapez 'import this' dans votre interpréteur python):

Beau, c'est mieux que laid.
Explicite vaut mieux qu'implicite.
Simple, c'est mieux que complexe.
Complexe vaut mieux que compliqué.
L'appartement est meilleur que l'emboîtement.
Clairsemé vaut mieux que dense.
La lisibilité compte.
Les cas spéciaux ne sont pas assez spéciaux pour enfreindre les règles.
Bien que la praticité l'emporte sur la pureté.
Les erreurs ne doivent jamais passer silencieusement.
Sauf si explicitement réduit au silence.
Face à l'ambiguïté, refusez la tentation de deviner.
Il devrait y avoir une - et de préférence une seule - manière évidente de le faire.
Bien que cette façon ne soit pas évidente au début, sauf si vous êtes néerlandais.
C'est mieux que jamais. à présent.
Bien que jamais ne soit souvent mieux que juste
Si l'implémentation est difficile à expliquer, c'est une mauvaise idée.
Si la mise en œuvre est facile à expliquer, ce peut être une bonne idée.
Les espaces de noms sont une excellente idée de klaxon - faisons-en plus!


la source
1
Si explicite vaut mieux qu'implicite, nous devrions coder en langage machine. Pas même en assembleur; il fait des choses implicites désagréables comme remplir automatiquement les intervalles de retard de branche avec des instructions, calculer les décalages de branche relatifs et résoudre les références symboliques parmi les globaux. Mec, ces stupides utilisateurs de Python et leur petite religion ...
Kaz
0

Mettez-les dans un module séparé.

En supposant que votre solution n'est pas plus gonflée que d'avoir besoin, vous n'avez pas trop d'options. Vous avez déjà divisé les fonctions en différentes sous-fonctions, la question est donc de savoir où les mettre:

  1. Mettez-les dans un module avec une seule fonction "publique".
  2. Mettez-les dans une classe avec une seule fonction "publique" (statique).
  3. Les imbriquer dans une fonction (comme vous l'avez décrit).

Maintenant, la deuxième et la troisième alternatives sont imbriquées dans un certain sens, mais la deuxième alternative ne semblerait pas mauvaise pour certains programmeurs. Si vous n'excluez pas la deuxième alternative, je ne vois pas trop de raisons d'exclure la troisième.

skyking
la source