Imaginez un processus long et compliqué, qui est démarré en appelant la fonction foo()
. Il y a plusieurs étapes consécutives dans ce processus, chacune d'elles dépendant du résultat de l'étape précédente. La fonction elle-même est, disons, d'environ 250 lignes. Il est très peu probable que ces étapes soient utiles en elles-mêmes, et non dans le cadre de l'ensemble de ce processus.
Des langages tels que Javascript permettent de créer des fonctions internes à l'intérieur d'une fonction parent, qui sont inaccessibles aux fonctions externes (sauf si elles sont passées en paramètre).
Un de mes collègues suggère que nous pouvons diviser le contenu de foo()
en 5 fonctions internes , chaque fonction étant une étape. Ces fonctions sont inaccessibles aux fonctions externes (signifiant leur inutilité en dehors de la fonction parent). Le corps principal de foo()
appelle simplement ces fonctions internes une par une:
foo(stuff) {
var barred = bar(stuff);
var bazzed = baz(barred);
var confabulated = confabulate(bazzed);
var reticulated = reticulate(confabulated);
var spliced = splice(reticulated);
return spliced;
// function definitions follow ...
}
Est-ce une sorte d'anti-pattern? Ou est-ce une façon raisonnable de diviser les fonctions longues? Une autre question est: est-ce acceptable lorsque vous utilisez OOP paradigme en Javascript? Est-il OK de diviser la méthode d'un objet de cette façon , ou cela justifie-t-il un autre objet, qui contient toutes ces fonctions internes en tant que méthodes privées?
Voir aussi:
Est-ce OK de diviser des fonctions et méthodes longues en plus petites même si elles ne seront pas appelées par autre chose? - une question précédente qui m'amène à celle-ci.
réponse de @ KilianFoth à une question très similaire qui offre une perspective différente par rapport aux réponses données ici.
Les fonctions internes ne sont pas un anti-modèle, elles sont une caractéristique.
Si cela n'a pas de sens de déplacer les fonctions internes à l'extérieur, alors certainement pas.
D'un autre côté, ce serait une bonne idée de les déplacer vers l'extérieur afin de pouvoir les tester à l'unité plus facilement. (Je ne sais pas si un cadre vous permet de tester les fonctions internes.) Lorsque vous avez une fonction avec plus de 250 lignes et que vous apportez des modifications à la logique, êtes-vous sûr de ne rien casser? Vous avez vraiment besoin de tests unitaires là-bas, et ce ne sera pas possible avec une seule fonction géante.
Le fractionnement de fonctions longues en fonctions plus petites est une bonne idée en général. C'est une technique de refactorisation courante pour remplacer un commentaire par une fonction nommée d'après le commentaire, appelée "méthode d'extraction" . Alors oui, faites-le!
Comme @ Kilian l'a souligné, voir également ce post:
Dois-je placer des fonctions qui ne sont utilisées que dans une autre fonction, dans cette fonction?
Ce n'est pas un anti-modèle. C'est la bonne façon de procéder.
C'est plus facile à lire, à entretenir, à tester. Cela permet d'éviter les doublons.
Voir: code propre (la bible pour les développeurs) et cette réponse
L'enjeu le plus important est de gérer la complexité. Les méthodes internes sont l'une des solutions, mais je ne suis pas convaincu que la meilleure.
Vous voulez diviser la méthode parce qu'elle est longue, mais avec les méthodes internes, la méthode est toujours longue, juste divisée en "chapitres". C'est mieux que les "chapitres de commentaires".
Une meilleure solution consiste à utiliser une classe interne qui rassemble toutes les méthodes d'assistance et - ce qui est également important - les données. De plus, cette classe peut être testée séparément.
Il s'agit d'une variante du refactoring "Remplacer la méthode par un objet de méthode" de Fowler.
Ce n'est pas un anti-modèle, mais c'est une décision discutable. Si ces fonctions n'ont pas de valeur en dehors de leur fonction parent, il n'y a aucun avantage à les séparer, juste plus de code. Vous pouvez simplement mettre quelques commentaires dans le code en disant "maintenant nous configurons quelque chose précédemment buzzé", et il aurait la même valeur dans moins de lignes de code.
EDIT: Je reformulerais ma réponse, car j'y ai utilisé un libellé pas très prudent et, apparemment, la communauté ne l'a pas accepté, sur la base des votes négatifs. Ce que je voulais vraiment dire, c'est que l'on devrait prendre en compte la quantité de code supplémentaire et les pauses d'attention lors de la lecture. Lorsque la fonction d'origine est suffisamment petite (sa taille est basée sur l'opinion), il peut être plus facile de la lire comme un seul flux de texte avec quelques commentaires utiles, que de faire des allers-retours entre les fonctions internes. Il se pourrait facilement qu'un seul morceau de code soit plus facile à lire et à comprendre qu'une collection de plusieurs fonctions.
D'un autre côté, lorsque la fonction d'origine est suffisamment grande pour être comprise comme une seule pièce, il est tout à fait logique de la diviser. Le principe d'une portée moindre entre définitivement en jeu ici. Il est juste important de comprendre quand la fonction est "assez grande" pour cela. Il est facile de porter un tel jugement lorsque l'original est 10k LOC, comme cela a été commenté, mais qu'en est-il de l'exemple du PO, 250 LOC? Je dirais que ça devient une opinion pour donner comme réponse.
Il est certainement préférable de séparer chaque "fonction intérieure": les petites pièces combinables sont beaucoup plus faciles à entretenir que les "grosses boules de boue" monolithiques. Attention cependant à votre terminologie: fonctions et méthodes ne sont pas la même chose, d'un point de vue stylistique.
Les méthodes ont tendance à privilégier un objet ("ceci") par rapport aux autres, ce qui limite leur utilité à cet objet. Les fonctions sont indépendantes de toute valeur particulière, ce qui les rend plus flexibles et plus faciles à combiner ensemble.
Dans ce cas, votre fonction "foo" est clairement la composition de ces "fonctions internes", vous pouvez donc tout aussi bien la définir de cette façon et éviter tout le passe-partout intermédiaire ("stuff", "barred", "bazzed", " "," réticulé "," épissé "," fonction (stuff) {"et" return ...;} "):
var foo = [splice, reticulate, confabulate, baz, bar].reduce(compose, id);
Si vous n'avez pas encore de fonctions "compose" et "id", les voici:
var compose = function(f, g) { return function(x) { return f(g(x)); }; };
var id = function(x) { return x; };
Félicitations, vous venez de gagner deux parties incroyablement utiles et réutilisables des internes soi-disant "inutiles" de foo. Je ne peux pas faire beaucoup plus sans voir les définitions des fonctions "internes", mais je suis sûr qu'il y a plus de simplification, de généralisation et de réutilisation disponibles à découvrir.
Je pense que ce type de code peut être facilement converti en chaînage. Comme ça :
foo(stuff)
{
return
stuff
.bar()
.baz()
.confabulate()
.reticulate()
.splice()
}
De mon humble point de vue, il est facile à lire, maintient la séparation des préoccupations et limite le taux d'erreur (si l'une des étapes échoue, tout le processus échoue).
Ce n'est pas un anti-modèle de diviser une grande fonction en fonctions plus petites.
Cependant, déclarer toutes ces fonctions à l'intérieur de la fonction principale présente un certain risque: parce que les fonctions internes ont accès à toutes les variables de la fonction externe, et parce que les variables JavaScript sont mutables, vous ne pouvez pas garantir que les fonctions internes ne modifieront pas les fonctions externes. état de la fonction.
En ce sens, en les imbriquant, vous perdez la plupart des avantages d'avoir de petites fonctions. Puisqu'ils partagent tous un état mutable commun, vous ne pouvez pas facilement raisonner chacun indépendamment.
Alors, gardez une structure plate. Si vous souhaitez masquer les fonctions privées, vous pouvez utiliser le modèle de module ou l'une des nombreuses bibliothèques de modules JavaScript disponibles.
Oui, le fractionnement est bon, ne peut-il être que pour une meilleure lisibilité et testabilité.
Une autre approche pourrait être d'appeler les fonctions les unes dans les autres, vous commencez par:
foo(stuff) {
var spliced = splice(stuff);
return spliced;
}
Étant donné que la fonction épissée a besoin de données réticulées, vous pouvez d'abord appeler cette fonction:
splice(stuff) {
var reticulated = reticulate(stuff);
//do splicing of reticulated
return spliced;
}
Ainsi, toutes les fonctions passeraient à travers et fonctionneraient avec les résultats qu'elles recevraient.