Dois-je extraire des fonctionnalités spécifiques dans une fonction et pourquoi?

29

J'ai une grande méthode qui fait 3 tâches, chacune d'elles peut être extraite dans une fonction distincte. Si je crée des fonctions supplémentaires pour chacune de ces tâches, cela améliorera-t-il mon code ou non et pourquoi?

Évidemment, cela fera moins de lignes de code dans la fonction principale, mais il y aura des déclarations de fonctions supplémentaires, donc ma classe aura des méthodes supplémentaires, ce qui, je pense, n'est pas bon, car cela rendra la classe plus complexe.

Dois-je faire cela avant d'avoir écrit tout le code ou dois-je le laisser jusqu'à ce que tout soit fait, puis extraire les fonctions?

dhblah
la source
19
"Je le laisse jusqu'à ce que tout soit fait" est généralement synonyme de "Cela ne sera jamais fait".
Euphoric
2
C'est généralement vrai, mais rappelez-vous également le principe opposé de YAGNI (qui ne s'applique pas dans ce cas, car vous en avez déjà besoin).
jhocking
Je voulais juste souligner que je ne me concentre pas tellement sur la réduction des lignes de code. Essayez plutôt de penser en termes d'abstractions. Chaque fonction ne doit avoir qu'un seul travail. Si vous constatez que vos fonctions effectuent plusieurs tâches, vous devez généralement refactoriser la méthode. Si vous suivez ces directives, il devrait être presque impossible d'avoir des fonctions trop longues.
Adrian

Réponses:

35

Il s'agit d'un livre auquel je renvoie souvent, mais je recommence: le code propre de Robert C. Martin , chapitre 3, "Fonctions".

Évidemment, cela fera moins de lignes de code dans la fonction principale, mais il y aura des déclarations de fonctions supplémentaires, donc ma classe aura des méthodes supplémentaires, ce qui, je pense, n'est pas bon, car cela rendra la classe plus complexe.

Préférez-vous lire une fonction avec +150 lignes ou une fonction appelant 3 fonctions +50 lignes? Je pense que je préfère la deuxième option.

Oui , cela rendra votre code meilleur dans le sens où il sera plus "lisible". Faites des fonctions qui effectuent une et une seule chose, elles seront plus faciles à maintenir et à produire un cas de test pour.

Aussi, une chose très importante que j'ai apprise avec le livre susmentionné: choisissez des noms bons et précis pour vos fonctions. Plus la fonction est importante, plus le nom doit être précis. Ne vous inquiétez pas de la longueur du nom, s'il doit être appelé FunctionThatDoesThisOneParticularThingOnly, nommez-le ainsi.

Avant d'effectuer votre refactorisation, écrivez un ou plusieurs cas de test. Assurez-vous qu'ils fonctionnent. Une fois votre refactoring terminé, vous pourrez lancer ces cas de test pour vous assurer que le nouveau code fonctionne correctement. Vous pouvez écrire des tests "plus petits" supplémentaires pour vous assurer que vos nouvelles fonctions fonctionnent correctement de manière séparée.

Enfin, et ce n'est pas contraire à ce que je viens d'écrire, demandez-vous si vous avez vraiment besoin de faire ce refactoring, consultez les réponses à " Quand refactoriser ?" (aussi, recherchez les questions SO sur "refactoring", il y en a plus et les réponses sont intéressantes à lire)

Dois-je le faire avant d'écrire tout le code ou dois-je le laisser jusqu'à ce que tout soit fait, puis extraire les fonctions?

Si le code est déjà là et fonctionne et que vous manquez de temps pour la prochaine version, ne le touchez pas. Sinon, je pense que l'on devrait faire de petites fonctions autant que possible et en tant que tel, refactoriser chaque fois qu'un certain temps est disponible tout en s'assurant que tout fonctionne comme avant (cas de test).

Jalayn
la source
10
En fait, Bob Martin a montré à plusieurs reprises qu'il préfère 7 fonctions de 2 à 3 lignes à une fonction de 15 lignes (voir ici sites.google.com/site/unclebobconsultingllc/… ). Et c'est là que de nombreux développeurs, même expérimentés, vont résister. Personnellement, je pense que beaucoup de ces "développeurs expérimentés" ont du mal à accepter qu'ils pourraient encore améliorer une chose aussi fondamentale que la construction d'abstractions avec des fonctions après> 10 ans de codage.
Doc Brown
+1 juste pour référencer un livre qui, à mon avis modeste, devrait être dans les étagères de n'importe quelle entreprise de logiciels.
Fabio Marcolini
3
Je paraphrase peut-être ici, mais une phrase de ce livre qui résonne dans ma tête presque tous les jours est "chaque fonction ne doit faire qu'une chose, et bien la faire". Cela semble particulièrement pertinent ici puisque le PO a dit que "ma fonction principale fait trois choses"
wakjah
Vous avez absolument raison!
Jalayn
Dépend de combien les trois fonctions distinctes sont entrelacées. Il peut être plus facile de suivre un bloc de code qui est tout en un seul endroit que trois blocs de code qui dépendent à plusieurs reprises les uns des autres.
user253751
13

Oui évidemment. S'il est facile de voir et de séparer les différentes "tâches" d'une seule fonction.

  1. Lisibilité - Les fonctions avec de bons noms expliquent ce que fait le code sans avoir besoin de lire ce code.
  2. Réutilisation - Il est plus facile d'utiliser une fonction qui fait une chose à plusieurs endroits, que d'avoir une fonction qui fait des choses dont vous n'avez pas besoin.
  3. Testabilité - Il est plus facile de tester la fonction, qui a une "fonction" définie, celle qui en a plusieurs

Mais cela pourrait poser des problèmes:

  • Il n'est pas facile de voir comment séparer la fonction. Cela peut nécessiter une refactorisation de l'intérieur de la fonction avant de passer à la séparation.
  • La fonction a un état interne énorme, qui est transmis. Cela nécessite généralement une sorte de solution OOP.
  • Il est difficile de dire quelle fonction devrait faire. Testez-le et refactorisez-le jusqu'à ce que vous le sachiez.
Euphorique
la source
5

Le problème que vous posez n'est pas un problème de codage, de conventions ou de pratique de codage, mais plutôt un problème de lisibilité et de la façon dont les éditeurs de texte affichent le code que vous écrivez. Ce même problème apparaît également dans l'article:

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?

La division d'une fonction en sous-fonctions est logique lors de la mise en œuvre d'un grand système dans le but d'encapsuler les différentes fonctionnalités qui le composeront. Néanmoins, tôt ou tard, vous vous retrouverez avec un certain nombre de grandes fonctions. Certains d'entre eux sont illisibles et difficiles à entretenir que vous les conserviez en tant que fonctions longues simples ou que vous les divisiez en fonctions plus petites. Cela est particulièrement vrai pour les fonctions où les opérations que vous effectuez ne sont nécessaires à aucun autre endroit de votre système. Permet de ramasser une fonction aussi longue et de la considérer dans une vue plus large.

Pro:

  • Une fois que vous l'avez lu, vous avez une idée complète de toutes les opérations de la fonction (vous pouvez la lire comme un livre);
  • Si vous souhaitez le déboguer, vous pouvez l'exécuter étape par étape sans passer à un autre fichier / partie du fichier;
  • Vous avez la liberté d'accéder / d'utiliser n'importe quelle variable déclarée à n'importe quel stade de la fonction;
  • L'algorithme que la fonction implémente est entièrement contenu dans la fonction (encapsulé);

Contra:

  • Cela prend plusieurs pages de votre écran;
  • Il faut du temps pour le lire;
  • Il n'est pas facile de mémoriser toutes les différentes étapes;

Imaginons maintenant de diviser la fonction longue en plusieurs sous-fonctions et de les regarder avec une perspective plus large.

Pro:

  • Hormis les fonctions de congé, chaque fonction décrit avec des mots (noms de sous-fonctions) les différentes étapes effectuées;
  • La lecture de chaque fonction / sous-fonction prend très peu de temps;
  • Il est clair quels paramètres et variables sont affectés à chaque sous-fonction (séparation des préoccupations);

Contra:

  • Il est facile d'imaginer ce qu'une fonction comme "sin ()" fait, mais pas aussi facile d'imaginer ce que font nos sous-fonctions;
  • L'algorithme est maintenant disparu, il est maintenant distribué dans les sous-fonctions de mai (pas de vue d'ensemble);
  • Lorsque vous le déboguez étape par étape, il est facile d'oublier l'appel de fonction de niveau de profondeur dont vous venez (sauter ici et là dans vos fichiers de projet);
  • Vous pouvez facilement perdre le contexte lors de la lecture des différentes sous-fonctions;

Les deux solutions ont des avantages et des inconvénients. La meilleure solution serait d'avoir des éditeurs qui permettent d'étendre, en ligne et pour toute la profondeur, chaque fonction appeler dans son contenu. Ce qui ferait de la division des fonctions en sous-fonctions la seule meilleure solution.

Antonello Ceravola
la source
2

Pour moi, il y a quatre raisons d'extraire des blocs de code en fonctions:

  • Vous le réutilisez : vous venez de copier un bloc de code dans le presse-papiers. Au lieu de simplement le coller, mettez-le dans une fonction et remplacez le bloc par un appel de fonction des deux côtés. Ainsi, chaque fois que vous devez modifier ce bloc de code, vous devez uniquement modifier cette fonction unique au lieu de modifier le code à plusieurs endroits. Ainsi, chaque fois que vous copiez un bloc de code, vous devez créer une fonction.

  • C'est un rappel : c'est un gestionnaire d'événements ou une sorte de code utilisateur une bibliothèque ou un framework appelle. (Je peux difficilement imaginer cela sans faire de fonctions.)

  • Vous pensez qu'il sera réutilisé , dans le projet en cours ou peut-être ailleurs: vous venez d'écrire un bloc qui calcule la plus longue sous-séquence commune de deux tableaux. Même si votre programme n'appelle cette fonction qu'une seule fois, je pense que j'aurai éventuellement besoin de cette fonction dans d'autres projets également.

  • Vous voulez du code auto-documenté : Donc, au lieu d'écrire une ligne de commentaire sur un bloc de code résumant ce qu'il fait, vous extrayez le tout dans une fonction et nommez-le ce que vous écririez dans un commentaire. Bien que je ne sois pas fan de cela, parce que j'aime écrire le nom de l'algorithme utilisé, la raison pour laquelle j'ai choisi cet algorithme, etc. Les noms de fonctions seraient alors trop longs ...

Calmarius
la source
1

Je suis sûr que vous avez entendu l'avis selon lequel les variables doivent être définies aussi étroitement que possible, et j'espère que vous êtes d'accord avec cela. Eh bien, les fonctions sont des conteneurs de portée, et dans les fonctions plus petites, la portée des variables locales est plus petite. Il est beaucoup plus clair comment et quand ils sont censés être utilisés et il est plus difficile de les utiliser dans le mauvais ordre ou avant leur initialisation.

De plus, les fonctions sont des conteneurs de flux logique. Il n'y a qu'une seule entrée, les sorties sont clairement marquées et si la fonction est suffisamment courte, les flux internes devraient être évidents. Cela a pour effet de réduire la complexité cyclomatique qui est un moyen fiable de réduire le taux de défauts.

John Wu
la source
0

En plus: J'ai écrit ceci en réponse à la question de dallin (maintenant fermée) mais je pense toujours que cela pourrait être utile à quelqu'un alors voilà


Je pense que la raison des fonctions d'atomisation est double, et comme @jozefg le mentionne dépend du langage utilisé.

Séparation des préoccupations

La raison principale pour cela est de garder différents morceaux de code séparés, donc tout bloc de code qui ne contribue pas directement au résultat / intention souhaité de la fonction est une préoccupation distincte et pourrait être extrait.

Supposons que vous ayez une tâche d'arrière-plan qui met également à jour une barre de progression, la mise à jour de la barre de progression n'est pas directement liée à la tâche de longue durée et doit donc être extraite, même si c'est le seul morceau de code qui utilise la barre de progression.

Dites en JavaScript que vous avez une fonction getMyData (), qui 1) construit un message de savon à partir de paramètres, 2) initialise une référence de service, 3) appelle le service avec le message de savon, 4) analyse le résultat, 5) renvoie le résultat. Cela semble raisonnable, j'ai écrit cette fonction exacte plusieurs fois - mais cela pourrait vraiment être divisé en 3 fonctions privées, y compris le code pour 3 et 5 (si cela), car aucun des autres codes n'est directement responsable de l'obtention des données du service .

Expérience de débogage améliorée

Si vous avez des fonctions complètement atomiques, votre trace de pile devient une liste de tâches, répertoriant tout le code exécuté avec succès, c'est-à-dire:

  • Obtenir mes données
    • Créer un message de savon
    • Initialiser la référence du service
    • Réponse du service analysé - ERREUR

serait beaucoup plus intéressant que de découvrir qu'il y avait une erreur lors de l'obtention des données. Mais certains outils sont encore plus utiles pour déboguer des arborescences d'appels détaillées que cela, par exemple, Microsofts Debugger Canvas .

Je comprends également vos préoccupations selon lesquelles il peut être difficile de suivre le code écrit de cette façon, car à la fin de la journée, vous devez choisir un ordre de fonctions dans un seul fichier alors que votre arborescence d'appels serait plus complexe que celle . Mais si les fonctions sont bien nommées (intellisense me permet d'utiliser 3-4 mots de casse camal dans n'importe quelle fonction que je veux sans me ralentir) et structurées avec une interface publique en haut du fichier, votre code se lira comme un pseudo-code qui est de loin le moyen le plus simple d'obtenir une compréhension de haut niveau d'une base de code.

Pour info - c'est une de ces choses "fais ce que je dis pas comme je fais", garder le code atomique est inutile à moins que vous ne soyez impitoyablement cohérent avec lui à mon humble avis, ce que je ne suis pas.

Dead.Rabit
la source