web-dev-qa-db-fra.com

Existe-t-il des directives sur le nombre de paramètres qu'une fonction doit accepter?

J'ai remarqué que quelques fonctions avec lesquelles je travaille ont 6 paramètres ou plus, alors que dans la plupart des bibliothèques que j'utilise, il est rare de trouver une fonction qui en prend plus de 3.

Souvent, beaucoup de ces paramètres supplémentaires sont des options binaires pour modifier le comportement de la fonction. Je pense que certaines de ces fonctions paramétrables devraient probablement être refactorisées. Existe-t-il une directive pour quel nombre est trop?

134
Darth Egregious

Je n'ai jamais vu de directive, mais d'après mon expérience, une fonction qui prend plus de trois ou quatre paramètres indique l'un des deux problèmes:

  1. La fonction en fait trop. Il doit être divisé en plusieurs fonctions plus petites, chacune ayant un jeu de paramètres plus petit.
  2. Il y a un autre objet qui s'y cache. Vous devrez peut-être créer un autre objet ou structure de données qui inclut ces paramètres. Consultez cet article sur le modèle Parameter Object pour plus d'informations.

Il est difficile de dire ce que vous regardez sans plus d'informations. Il est probable que la refactorisation que vous devez effectuer consiste à diviser la fonction en fonctions plus petites qui sont appelées à partir du parent en fonction des indicateurs actuellement transmis à la fonction.

Il y a de bons gains à faire en faisant ceci:

  • Cela rend votre code plus facile à lire. Personnellement, je trouve beaucoup plus facile de lire une "liste de règles" composée d'une structure if qui appelle beaucoup de méthodes avec des noms descriptifs qu'une structure qui fait tout cela dans une seule méthode.
  • C'est plus testable à l'unité. Vous avez divisé votre problème en plusieurs tâches plus petites qui sont individuellement très simples. La collection de tests unitaires serait alors composée d'une suite de tests comportementaux qui vérifie les chemins à travers la méthode principale et d'une collection de tests plus petits pour chaque procédure individuelle.
119
Michael K

Selon "Clean Code: A Handbook of Agile Software Craftsmanship", zéro est l'idéal, un ou deux sont acceptables, trois dans des cas spéciaux et quatre ou plus, jamais!

Les mots de l'auteur:

Le nombre idéal d'arguments pour une fonction est zéro (niladique). Vient ensuite un (monadique), suivi de près par deux (dyadiques). Trois arguments (triadiques) doivent être évités dans la mesure du possible. Plus de trois (polyadiques) nécessitent une justification très spéciale - et ne devraient donc pas être utilisés de toute façon.

Dans ce livre, il y a un chapitre qui ne parle que des fonctions où les paramètres sont importants, donc je pense que ce livre peut être une bonne indication de la quantité de paramètres dont vous avez besoin.

À mon avis, un paramètre vaut mieux que personne car je pense que ce qui se passe est plus clair.

À titre d'exemple, à mon avis, le deuxième choix est meilleur car il est plus clair ce que la méthode traite:

LangDetector detector = new LangDetector(someText);
//lots of lines
String language = detector.detectLanguage();

vs.

LangDetector detector = new LangDetector();
//lots of lines
String language = detector.detectLanguage(someText);

À propos de nombreux paramètres, cela peut être un signe que certaines variables peuvent être regroupées en un seul objet ou, dans ce cas, de nombreux booléens peuvent représenter que la fonction/méthode fait plus d'une chose, et dans ce cas, est préférable de refactoriser chacun de ces comportements dans une fonction différente.

44
Renato Dinhani

Si les classes de domaine dans l'application sont conçues correctement, le nombre de paramètres que nous transmettons à une fonction sera automatiquement réduit - car les classes savent comment faire leur travail et elles ont suffisamment de données pour faire leur travail.

Par exemple, supposons que vous ayez une classe de gestionnaire qui demande à une classe de 3e année de terminer les devoirs.

Si vous modélisez correctement,

3rdGradeClass.finishHomework(int lessonId) {
    result = students.assignHomework(lessonId, dueDate);
    teacher.verifyHomeWork(result);
}

C’est simple.

Si vous n'avez pas le bon modèle, la méthode sera la suivante

Manager.finishHomework(grade, students, lessonId, teacher, ...) {
    // This is not good.
}

Le modèle correct réduit toujours les paramètres de fonction entre les appels de méthode car les fonctions correctes sont déléguées à leurs propres classes (responsabilité unique) et elles ont suffisamment de données pour faire leur travail.

Chaque fois que je vois le nombre de paramètres augmenter, je vérifie mon modèle pour voir si j'ai conçu mon modèle d'application correctement.

Il existe cependant quelques exceptions: lorsque je dois créer un objet de transfert ou des objets de configuration, j'utiliserai un modèle de générateur pour produire des petits objets construits avant de construire un gros objet de configuration.

26
java_mouse

Un aspect que les autres réponses ne prennent pas en compte est la performance.

Si vous programmez dans un langage de niveau suffisamment bas (C, C++, Assembly), un grand nombre de paramètres peuvent être très préjudiciables aux performances sur certaines architectures, surtout si la fonction est appelée un grand nombre de fois.

Lorsqu'un appel de fonction est effectué en ARM par exemple, les quatre premiers arguments sont placés dans les registres r0 à r3 et les arguments restants doivent être poussés dans la pile. Garder le nombre d'arguments en dessous de cinq peut faire toute la différence pour les fonctions critiques.

Pour les fonctions qui sont appelées très souvent, même le fait que le programme doive configurer les arguments avant chaque appel peut affecter les performances (r0 à r3 peut être écrasé par la fonction appelée et devra être remplacé avant le prochain appel) donc à cet égard, zéro argument est préférable.

Mise à jour:

KjMag évoque le sujet intéressant de l'inline. L'insertion en ligne atténuera à certains égards cela car elle permettra au compilateur d'effectuer les mêmes optimisations que vous seriez en mesure de faire en écrivant en assembleur pur. En d'autres termes, le compilateur peut voir quels paramètres et variables sont utilisés par la fonction appelée et peut optimiser l'utilisation du registre afin que la lecture/écriture de la pile soit minimisée.

Il y a cependant quelques problèmes avec l'inline.

  1. L'inclusion entraîne la croissance du binaire compilé, car le même code est dupliqué sous forme binaire s'il est appelé à partir de plusieurs emplacements. Cela est préjudiciable en ce qui concerne l'utilisation du cache I.
  2. Les compilateurs ne permettent généralement de s'aligner qu'à un certain niveau (3 étapes IIRC?). Imaginez appeler une fonction intégrée à partir d'une fonction intégrée à partir d'une fonction intégrée. La croissance binaire exploserait si inline était traité comme obligatoire dans tous les cas.
  3. Il existe de nombreux compilateurs qui ignoreront complètement inline ou vous donneront des erreurs lorsqu'ils le rencontreront.
16
Leo

Lorsque la liste des paramètres atteint plus de cinq, envisagez de définir une structure ou un objet "contextuel".

Il s'agit essentiellement d'une structure contenant tous les paramètres facultatifs avec des valeurs par défaut raisonnables.

Dans le monde procédural C, une structure simple ferait l'affaire. En Java, C++ un simple objet suffira. Ne jouez pas avec les getters ou les setters car le seul but de l'objet est de conserver des valeurs "publiquement" réglables.

7
James Anderson

Non, il n'y a pas de ligne directrice standard

Mais il existe certaines techniques qui peuvent rendre une fonction avec beaucoup de paramètres plus supportable.

Vous pouvez utiliser un paramètre list-if-args (args *) ou un paramètre dictionary-of-args (kwargs**)

Par exemple, en python:

// Example definition
def example_function(normalParam, args*, kwargs**):
  for i in args:
    print 'args' + i + ': ' + args[i] 
  for key in kwargs:
    print 'keyword: %s: %s' % (key, kwargs[key])
  somevar = kwargs.get('somevar','found')
  missingvar = kwargs.get('somevar','missing')
  print somevar
  print missingvar

// Example usage

    example_function('normal parameter', 'args1', args2, 
                      somevar='value', missingvar='novalue')

Les sorties:

args1
args2
somevar:value
someothervar:novalue
value
missing

Ou vous pouvez utiliser la syntaxe de définition littérale de l'objet

Par exemple, voici un appel jQuery JavaScript pour lancer une AJAX requête GET:

$.ajax({
  type: 'GET',
  url: 'http://someurl.com/feed',
  data: data,
  success: success(),
  error: error(),
  complete: complete(),
  dataType: 'jsonp'
});

Si vous jetez un œil à la classe ajax de jQuery, il y a beaucoup (environ 30) autres propriétés qui peuvent être définies; principalement parce que les communications ajax sont très complexes. Heureusement, la syntaxe littérale de l'objet facilite la vie.


C # intellisense fournit une documentation active des paramètres, il n'est donc pas rare de voir des arrangements très complexes de méthodes surchargées.

Les langages typés dynamiquement comme python/javascript n'ont pas une telle capacité, il est donc beaucoup plus courant de voir des arguments de mots clés et des définitions littérales d'objets.

Je préfère les définitions littérales d'objet ( même en C # ) pour gérer des méthodes complexes, car vous pouvez voir explicitement quelles propriétés sont définies lorsqu'un objet est instancié. Vous devrez faire un peu plus de travail pour gérer les arguments par défaut, mais à long terme, votre code sera beaucoup plus lisible. Avec les définitions littérales d'objets, vous pouvez briser votre dépendance à la documentation pour comprendre ce que fait votre code à première vue.

À mon humble avis, les méthodes surchargées sont très surévaluées.

Remarque: Si je me souviens bien, le contrôle d'accès en lecture seule devrait fonctionner pour les constructeurs d'objets littéraux en C #. Ils fonctionnent essentiellement de la même manière que la définition des propriétés dans le constructeur.


Si vous n'avez jamais écrit de code non trivial dans un langage javaScript typé dynamiquement (python) et/ou fonctionnel/prototype, je vous suggère fortement de l'essayer. Cela peut être une expérience enrichissante.

Il peut être effrayant d'abord de briser votre dépendance aux paramètres pour l'approche globale de l'initialisation des fonctions/méthodes, mais vous apprendrez à en faire beaucoup plus avec votre code sans avoir à ajouter une complexité inutile.

Mise à jour:

J'aurais probablement dû fournir des exemples pour démontrer l'utilisation dans un langage typé statiquement mais je ne pense pas actuellement dans un contexte typé statiquement. Fondamentalement, j'ai fait trop de travail dans un contexte typé dynamiquement pour revenir soudainement en arrière.

Ce que je fais sais que la syntaxe de définition littérale d'objet est complètement possible dans les langages typés statiquement (au moins en C # et Java) car je les ai déjà utilisés. Dans les langages typés statiquement, ils sont appelés "initialiseurs d'objets". Voici quelques liens pour montrer leur utilisation dans Java et C # .

5
Evan Plaice

Personnellement, plus de 2 est l'endroit où mon alarme d'alarme d'odeur se déclenche. Lorsque vous considérez les fonctions comme opérations (c'est-à-dire une traduction d'entrée en sortie), il est rare que plus de 2 paramètres soient utilisés dans une opération. Les procédures (c'est-à-dire une série d'étapes pour atteindre un objectif) nécessiteront plus d'entrées et sont parfois la meilleure approche, mais dans la plupart des langues ces jours-ci ne devraient pas être la norme.

Mais encore une fois, c'est la ligne directrice plutôt qu'une règle. J'ai souvent des fonctions qui prennent plus de deux paramètres en raison de circonstances inhabituelles ou d'une facilité d'utilisation.

3
Telastyn

Tout comme Evan Plaice le dit, je suis un grand fan de simplement passer des tableaux associatifs (ou la structure de données comparable de votre langage) dans les fonctions chaque fois que possible.

Ainsi, au lieu de (par exemple) ceci:

<?php

createBlogPost('the title', 'the summary', 'the author', 'the date of publication, 'the keywords', 'the category', 'etc');

?>

Optez pour:

<?php

// create a hash of post data
$post_data = array(
  'title'    => 'the title',
  'summary'  => 'the summary',
  'author'   => 'the author',
  'pubdate'  => 'the publication date',
  'keywords' => 'the keywords',
  'category' => 'the category',
  'etc'      => 'etc',
);

// and pass it to the appropriate function
createBlogPost($post_data);

?>

Wordpress fait beaucoup de choses de cette façon, et je pense que cela fonctionne bien. (Bien que mon exemple de code ci-dessus soit imaginaire et ne soit pas lui-même un exemple de Wordpress.)

Cette technique vous permet de transmettre facilement un grand nombre de données dans vos fonctions, tout en vous évitant d'avoir à vous rappeler l'ordre dans lequel chacune doit être transmise.

Vous apprécierez également cette technique lorsque vient le temps de refactoriser - au lieu d'avoir à potentiellement changer l'ordre des arguments d'une fonction (comme lorsque vous réalisez que vous devez passer encore un autre argument), vous n'avez pas besoin de changer le paramètre de vos fonctions liste du tout.

Non seulement cela vous évite d'avoir à réécrire la définition de votre fonction, mais cela vous évite d'avoir à changer l'ordre des arguments chaque fois que la fonction est invoquée. C'est une énorme victoire.

2
Chris Allen Lane

Pour fournir un peu plus de contexte autour des conseils pour le nombre idéal d'arguments de fonction étant zéro dans "Clean Code: un manuel de fabrication de logiciels agiles" de Robert Martin, l'auteur dit ce qui suit comme l'un de ses points:

Les arguments sont difficiles. Ils prennent beaucoup de pouvoir conceptuel. C'est pourquoi je me suis débarrassé de presque tous de l'exemple. Considérez, par exemple, le StringBuffer dans l'exemple. Nous aurions pu le faire circuler comme argument plutôt que d'en faire une variable d'instance, mais alors nos lecteurs auraient dû l'interpréter à chaque fois qu'ils le voyaient. Lorsque vous lisez l'histoire racontée par le module, includeSetupPage() est plus facile à comprendre que includeSetupPageInto(newPageContent). L'argument est à un niveau d'abstraction différent du nom de la fonction et vous oblige à connaître un détail (en d'autres termes, StringBuffer) qui n'est pas particulièrement important à ce stade.

Pour son exemple includeSetupPage() ci-dessus, voici un petit extrait de son "code propre" remanié à la fin du chapitre:

// *** NOTE: Commments are mine, not the author's ***
//
// Java example
public class SetupTeardownIncluder {
    private StringBuffer newPageContent;

    // [...] (skipped over 4 other instance variables and many very small functions)

    // this is the zero-argument function in the example,
    // which calls a method that eventually uses the StringBuffer instance variable
    private void includeSetupPage() throws Exception {
        include("SetUp", "-setup");
    }

    private void include(String pageName, String arg) throws Exception {
        WikiPage inheritedPage = findInheritedPage(pageName);
        if (inheritedPage != null) {
            String pagePathName = getPathNameForPage(inheritedPage);
            buildIncludeDirective(pagePathName, arg);
        }
    }

    private void buildIncludeDirective(String pagePathName, String arg) {
        newPageContent
            .append("\n!include ")
            .append(arg)
            .append(" .")
            .append(pagePathName)
            .append("\n");
    }
}

L '"école de pensée" de l'auteur plaide pour de petites classes, un nombre faible (idéalement 0) d'arguments de fonction et de très petites fonctions. Bien que je ne sois pas entièrement d'accord avec lui, je l'ai trouvé stimulant et je pense que l'idée d'arguments à fonction zéro comme idéal peut être considérée. Notez également que même son petit extrait de code ci-dessus a également des fonctions d'argument non nulles, donc je pense que cela dépend du contexte.

(Et comme d'autres l'ont souligné, il soutient également que plus d'arguments rendent plus difficile du point de vue des tests. Mais ici, je voulais principalement mettre en évidence l'exemple ci-dessus et sa justification pour les arguments de fonction zéro.)

0
sonny

Un précédent réponse a mentionné un auteur fiable qui a déclaré que moins vos fonctions ont de paramètres, mieux vous vous portez. La réponse n'a pas expliqué pourquoi, mais les livres l'expliquent, et voici deux des raisons les plus convaincantes pour lesquelles vous devez adopter cette philosophie et avec lesquelles je suis personnellement d'accord:

  • Les paramètres appartiennent à un niveau d'abstraction différent de celui de la fonction. Cela signifie que le lecteur de votre code devra réfléchir à la nature et à la finalité des paramètres de vos fonctions: cette réflexion est "de niveau inférieur" à celle du nom et de la finalité de leurs fonctions correspondantes.

  • La deuxième raison pour avoir le moins de paramètres possible pour une fonction est le test: par exemple, si vous avez une fonction avec 10 paramètres, pensez au nombre de combinaisons de paramètres dont vous disposez pour couvrir tous les cas de test pour, par exemple, une unité tester. Moins de paramètres = moins de tests.

0
Billal Begueradj