web-dev-qa-db-fra.com

Quand est-il approprié de créer une fonction distincte alors qu'il n'y aura qu'un seul appel à cette fonction?

Nous concevons des normes de codage et avons des désaccords quant à l'opportunité de décomposer le code en fonctions distinctes au sein d'une classe, lorsque ces fonctions ne seront appelées qu'une seule fois.

Par exemple:

f1()
{
   f2();  
   f4();
}


f2()
{
    f3()
    // Logic here
}


f3()
{
   // Logic here
}

f4()
{
   // Logic here
}

contre:

f1()
{
   // Logic here
   // Logic here
   // Logic here
}

Certains soutiennent qu'il est plus simple à lire lorsque vous divisez une grande fonction à l'aide de sous-fonctions distinctes à usage unique. Cependant, lors de la première lecture de code, je trouve fastidieux de suivre les chaînes logiques et d'optimiser le système dans son ensemble. Existe-t-il des règles généralement appliquées à ce type de disposition de fonction?

Veuillez noter que contrairement à d'autres questions, je demande le meilleur ensemble de conditions pour différencier les utilisations autorisées et non autorisées des fonctions d'appel unique, pas seulement si elles sont autorisées.

46
David

La raison d'être de la séparation des fonctions n'est pas le nombre de fois où elles seront appelées , mais de les garder petites et de les empêcher de faire plusieurs choses différentes.

Bob Martin's book Clean Code donne de bonnes directives sur le moment de diviser une fonction:

  • Les fonctions doivent être petites; comme c'est petit? Voir la balle ci-dessous.
  • Les fonctions ne devraient faire qu'une seule chose.

Donc, si la fonction comporte plusieurs écrans, divisez-la. Si la fonction fait plusieurs choses, divisez-la.

Si la fonction est constituée d'étapes séquentielles visant un résultat final, il n'est pas nécessaire de la diviser, même si elle est relativement longue. Mais si les fonctions font une chose, puis une autre, puis une autre et puis une autre, avec des conditions, des blocs logiquement séparés, etc., elle doit être divisée. En raison de cette logique, les fonctions devraient généralement être petites.

Si f1() fait l'authentification, f2() analyse l'entrée en parties plus petites, si f3() fait des calculs et f4() enregistre ou persiste les résultats, alors ils il faut évidemment les séparer, même si chacun d’entre eux ne sera appelé qu’une seule fois.

De cette façon, vous pouvez les refactoriser et les tester séparément, en plus de l'avantage supplémentaire d'être plus facile à lire .

Par contre si tout ce que fait la fonction est:

a=a+1;
a=a/2;
a=a^2
b=0.0001;
c=a*b/c;
return c;

il n'est alors pas nécessaire de le diviser, même lorsque la séquence d'étapes est longue.

92
Tulains Córdova

Je pense que la dénomination des fonctions est très importante ici.

Une fonction fortement disséquée peut être très auto-documentée. Si chaque processus logique au sein d'une fonction est divisé en sa propre fonction, avec une logique interne minimale, le comportement de chaque instruction peut être déterminé par les noms des fonctions et les paramètres qu'elles prennent.

Bien sûr, il y a un inconvénient: les noms de fonction. Comme les commentaires, ces fonctions très spécifiques peuvent souvent être désynchronisées avec ce que la fonction fait réellement. Mais, en même temps, en lui donnant un nom de fonction approprié, vous le rendez plus difficile pour justifier le fluage de la portée. Il devient plus difficile de faire faire à une sous-fonction quelque chose de plus qu'elle ne le devrait clairement.

Je suggère donc ceci. Si vous pensez qu'une section de code pourrait être divisée, même si personne ne l'appelle, posez-vous cette question: "Quel nom lui donneriez-vous?"

Si la réponse à cette question vous prend plus de 5 secondes, ou si le nom de la fonction que vous choisissez est décidément opaque, il y a de fortes chances que ce ne soit pas une unité logique distincte au sein de la fonction. Ou à tout le moins, que vous n'êtes pas assez sûr de ce que fait réellement cette unité logique pour bien la répartir.

Mais il existe un problème supplémentaire que les fonctions fortement disséquées peuvent rencontrer: la correction de bogues.

Le suivi des erreurs logiques dans une fonction de ligne 200+ est difficile. Mais les suivre à travers plus de 10 fonctions individuelles, tout en essayant de se souvenir des relations entre elles? Cela peut être encore plus difficile.

Cependant, encore une fois, l'auto-documentation sémantique par les noms peut jouer un rôle clé. Si chaque fonction a un nom logique, alors tout ce que vous avez à faire pour valider l'une des fonctions feuilles (en plus des tests unitaires) est de voir si elle fait réellement ce qu'elle dit. Les fonctions foliaires ont tendance à être courtes et ciblées. Donc, si chaque fonction de feuille individuelle fait ce qu'elle dit, le seul problème possible est que quelqu'un leur a transmis les mauvaises choses.

Donc, dans ce cas, il peut être utile de corriger les bogues.

Je pense que cela revient vraiment à la question de savoir si vous pouvez attribuer un nom significatif à une unité logique. Si c'est le cas, cela peut probablement être une fonction.

43
Nicol Bolas

Chaque fois que vous ressentez le besoin d'écrire un commentaire pour décrire ce qu'un bloc de texte fait, vous avez trouvé une opportunité d'extraire une méthode.

Plutôt que

//find eligible contestants
var eligible = contestants.Where(c=>c.Age >= 18)
eligible = eligible.Where(c=>c.Country == US)

essayer

var eligible = FindEligible(contestants)
12
dss539

SEC - Ne vous répétez pas - n'est qu'un des plusieurs principes qui doivent être équilibrés.

Certains autres qui me viennent à l'esprit sont les noms. Si la logique est alambiquée pas évidente pour le lecteur occasionnel, l'extraction dans une méthode/fonction dont le nom résume mieux quoi et pourquoi il le fait peut améliorer la lisibilité du programme.

Le fait de viser moins de 5 à 10 lignes de méthode/fonction de code peut entrer en jeu, selon le nombre de lignes que //logic devient.

En outre, une fonction avec des paramètres peut agir comme une API et peut nommer les paramètres de manière appropriée pour rendre ensuite la logique du code plus claire.

Vous pouvez également constater qu'au fil du temps, les collections de ces fonctions révèlent un regroupement utile du dôme, par exemple administrateur et ils peuvent ensuite être facilement regroupés sous celui-ci.

5
Michael Durrant

Le point sur la division des fonctions est une chose: la simplicité.

Un lecteur de code ne peut avoir plus de sept choses en tête simultanément. Vos fonctions devraient refléter cela.

  • Si vous créez des fonctions trop longues, elles seront illisibles car vous avez bien plus de sept choses à l'intérieur de vos fonctions.

  • Si vous créez une tonne de fonctions d'une ligne, les lecteurs se confondent également dans l'enchevêtrement des fonctions. Vous devez maintenant conserver plus de sept fonctions en mémoire pour comprendre le but de chacune.

  • Certaines fonctions sont simples même si elles sont longues. L'exemple classique est lorsqu'une fonction contient une grande instruction switch avec de nombreux cas. Tant que le traitement de chaque cas est simple, vos fonctions ne sont pas trop longues.

Les deux extrêmes (le Megamoth et la soupe de minuscules fonctions) sont également mauvais, et vous devez trouver un équilibre entre les deux. D'après mon expérience, une fonction Nice a une dizaine de lignes. Certaines fonctions seront à une ligne, certaines dépasseront vingt lignes. L'important est que chaque fonction remplisse une fonction facilement compréhensible tout en étant également compréhensible dans sa mise en œuvre.

Il s'agit de séparation des préoccupations. (ok, pas tout à ce sujet; c'est une simplification).

C'est bon:

function initializeUser(name, job, bye) {
    this.username = name;
    this.occupation = job;
    this.farewell = bye;
    this.gender = Gender.unspecified;
    this.species = Species.getSpeciesFromJob(this.occupation);
    ... etc in the same vein.
}

Cette fonction ne concerne qu'une seule préoccupation: elle définit les propriétés initiales d'un utilisateur à partir des arguments fournis, des valeurs par défaut, de l'extrapolation, etc.

Ce n'est pas bien:

function initializeUser(name, job, bye) {
    // Connect to internet if not already connected.
    modem.getInstance().ensureConnected();
    // Connect to user database
    userDb = connectToDb(USER_DB);
    // Validate that user does not yet exist.
    if (0 != userDb.db_exec("SELECT COUNT(*) FROM `users` where `name` = %d", name)) {
        throw new sadFace("User exists");
    }
    // Configure properties. Don't try to translate names.
    this.username = removeBadWords(name);
    this.occupation = removeBadWords(translate(job));
    this.farewell = removeBadWords(translate(bye));
    this.gender = Gender.unspecified;
    this.species = Species.getSpeciesFromJob(this.occupation);
    userDb.db_exec("INSERT INTO `users` set `name` = %s", this.username);
    // Disconnect from the DB.
    userDb.disconnect();
}

Séparation des préoccupations suggère que cela devrait être traité comme de multiples préoccupations; la gestion des bases de données, la validation de l'existence de l'utilisateur, le paramétrage des propriétés. Chacun de ceux-ci est très facilement testé en tant qu'unité, mais les tester tous dans une seule méthode donne un ensemble de tests unitaires très compliqué, qui serait de tester des choses aussi différentes que la façon dont il a géré la base de données disparaissant, comment il gère la création de titres de travail vides et invalides, et comment il gère la création d'un utilisateur deux fois (réponse: mal, il y a un bug).

Une partie du problème est qu'il va partout en termes de niveau: le réseau de bas niveau et les trucs DB n'ont pas leur place ici. Cela fait partie de la séparation des préoccupations. L'autre partie est que ce qui devrait être la préoccupation de quelque chose d'autre est plutôt la préoccupation de la fonction init. Par exemple, s'il faut traduire ou appliquer des filtres de langue incorrects, il pourrait être plus judicieux de se préoccuper des champs définis.

3
Dewi Morgan

Cela dépend à peu près de ce que votre // Logic Here est.

Si c'est une ligne, alors vous n'avez probablement pas besoin d'une décomposition fonctionnelle.

Si, d'autre part, ce sont des lignes et des lignes de code, alors il vaut mieux le mettre dans une fonction distincte et le nommer de manière appropriée (f1,f2,f3 ne passe pas ici).

Tout cela a à voir avec le cerveau humain en moyenne n'est pas très efficace pour traiter de grandes quantités de données en un coup d'œil. Dans un sens, peu importe les données: fonction multi-lignes, intersection occupée, puzzle de 1000 pièces.

Soyez un ami du cerveau de votre mainteneur de code, réduisez les morceaux dans le point d'entrée. Qui sait, ce mainteneur de code pourrait même être vous quelques mois plus tard.

2

La "bonne" réponse, selon les dogmes de codage répandus, est de diviser les grandes fonctions en petites fonctions faciles à lire, testables et testées avec auto-documentation noms.

Cela dit, définir "grand" en termes de "lignes de code" peut sembler arbitraire, dogmatique et fastidieux, ce qui peut provoquer des désaccords, des scrupules et des tensions inutiles. Mais n'ayez crainte! Car, si nous reconnaissons que les objectifs principaux derrière la limite de lignes de code sont la lisibilité et la testabilité, nous pouvons facilement trouver la limite de ligne appropriée pour une fonction donnée! (Et commencez à jeter les bases d'une limite logicielle pertinente en interne.)

Acceptez en équipe d'autoriser les fonctions mégalithiques et d'extraire des lignes dans des fonctions plus petites et bien nommées au premier signe indiquant que la fonction est soit difficile à lire dans son ensemble, soit lorsque les sous-ensembles ne sont pas exacts.

... Et si tout le monde est honnête lors de la mise en œuvre initiale, et si aucun ne vante un QI supérieur à 200, les limites de la compréhensibilité et de la testabilité peuvent souvent être identifiées avant que quiconque ne voie son code.

1
svidgen