web-dev-qa-db-fra.com

Rendre la récursion facultative: mauvaise pratique?

Disons que j'ai une fonction comme celle-ci:

/*
Mode is a bool that determines whether the function uses 
it's recursive mode or not
*/
public static int func(int n, boolean mode) {
    if (!mode) someCalculation();
    else func(anotherCalculation(), mode);
}

Autrement dit, en fonction de ce qu'est mode, la même fonction décide de faire un calcul récursif ou non récursif, où les deux calculs ne sont pas liés mais doivent être effectués avec la même donnée. En toutes circonstances, est-ce que cela serait une meilleure pratique que de simplement appeler une fonction, deux fonctions différentes qui font les deux tâches?

4
JohnnyApplesauce

Les autres réponses sont généralement parfaites. J'ajouterais seulement que, selon le contexte où vous avez besoin de cette fonction, ou toute fonction qui est "dichotomique" selon votre perspective (je veux dire, récursive ou non n'est pas toujours une préoccupation dans la majorité des cas de calcul dans le monde réel, mais cela vous semble important), vous souhaiterez peut-être masquer l'implémentation derrière une abstraction: (en utilisant C # ici)

public interface IMathCalculator
{
    //....
    int Factorial(int n);
    //....
}

Ensuite, vous pouvez avoir deux implémentations.

public class MathCalculator : IMathCalculator
{
    //....
    public int Factorial(int n)
    {
        //Implement factorial iteratively.
    }
    //....
}

public class RecursionEnabledMathCalculator : IMathCalculator
{
    //....
    public int Factorial(int n)
    {
        //Perform calculation recursively.
    }
    //....
}

De cette façon, vous pouvez même ignorer la vérification du mode. Bien que je sois d'accord avec Christophe note :

Exception: si l'appelant peut savoir quelque chose qui pourrait permettre une manière plus appropriée de faire le calcul, et que l'appelé ne peut pas le découvrir par lui-même, alors un paramètre de stratégie est tout à fait acceptable.

Je suggère que ce premier soit traité comme une odeur de conception. Si une partie du code sait de quelle implémentation spécifique il a besoin, il vaut mieux être bien caché, situé profondément dans une bibliothèque, où de petits détails comptent autant . Les détails d'implémentation ne devraient concerner que les autres détails d'implémentation du même contexte (au mieux). Les détails de mise en œuvre de ce type (programmation/optimisations mathématiques) sont très difficiles à déboguer ou à raisonner et ont très bas maintenabilité dans le cas, lorsque d'autres responsables prennent le relais.

Soit dit en passant, votre fonction publiée ne fonctionne pas comme prévu lorsqu'elle est interprétée selon le principe KISS. Si je l'interprète correctement, func(2, false) renvoie 3 et func(2, true) renvoie 0 (tandis que func(-3, true) provoquera simplement une exception de dépassement de pile). En contournant le fait que tout argument positif passé avec true renverra 0, en fonction de cette partie de votre question:

où les deux calculs ne sont pas liés mais doivent être effectués sur la même donnée

Si vous passez true ou false avec la même entrée int n Retourne des résultats différents , et ceci est intentionnel , alors vous feriez mieux de repenser votre conception car presque personne ne lit seulement la signature de la fonction comprendra tout, à part vous ou quiconque a accès au code source et n'oublie pas de le vérifier .

Aussi, pour répondre directement à votre question:

En toutes circonstances, est-ce que cela serait une meilleure pratique que de simplement appeler une fonction, deux fonctions différentes qui font les deux tâches?

Non, il n'y a presque pas de telles circonstances. Si vous voulez dire lequel est le meilleur, votre exemple affiché ou ceci:

public static int func(int n, boolean mode) {
    if (!mode) return funcNonRecursive(n);
    else return funcRecursive(n);
}

private static int funcNonRecursive(int n) {
    return n + 1;
}

private static int funcRecursive(int n) {
    return n != 0 ? func(n - 1, mode) : 0;
}

alors c'est presque toujours mieux que votre exemple publié, à moins que vous ne vous souciez de la surcharge supplémentaire d'appel de méthode, auquel cas vous:

  • Appartenez à une vaste minorité où vous devez écrire du code extrêmement critique.
  • Ne devrait probablement pas s'inquiéter car les compilateurs modernes optimisent généralement ce type de code (par exemple en les insérant).

Dans tous les cas, les chances sont très faibles, donc, si la lisibilité est votre principale préoccupation, cette méthode est plus conviviale et maintenable.

4
Vector Zita

Les principes directeurs ici devraient être séparation des préoccupations :

  • L'appelant doit invoquer une fonction uniquement avec les paramètres pertinents pour le résultat, quelle que soit la manière dont le calcul est effectué;
  • La fonction appelée doit fournir le résultat le mieux adapté aux besoins, qu'il soit itératif, récursif, mis en cache ou une combinaison de ces éléments.

Exception: si l'appelant peut savoir quelque chose qui pourrait permettre une manière plus appropriée de faire le calcul et que l'appelé ne peut pas le découvrir par lui-même, alors un paramètre de stratégie est complètement acceptable.

Une fonction devrait également faire une seule chose et bien le faire . Le couplage dans une fonction de deux choses indépendantes doit être évité si le seul but de mode est de choisir entre les deux.

19
Christophe

Les fonctions sont l'un de nos mécanismes d'abstraction et l'un des aspects de la qualité d'une abstraction est l'utilité pour le programmeur consommateur, l'appelant.

Essayons d'imaginer l'appelant et puisqu'il y a deux calculs différents en cours, quelles sont les chances que l'appelant utilise une variable pour le booléen, plutôt que de toujours passer une constante (vrai vs faux).

S'il est vrai que toujours, le code appelant sait lequel des deux calculs il veut, de sorte qu'il passe une constante booléenne - alors cette approche ne fait que confondre deux concepts par ailleurs séparés, et donc augmente la complexité sans valeur.

Si, cependant, il y a une raison de passer dynamiquement d'un calcul à l'autre, alors il semblerait mériter de mélanger deux approches ensemble. Cependant, dans ce cas, je fournirais trois versions: deux sans que le booléen fasse son travail individuel, et un troisième avec booléen qui invoque l'un des deux autres. Les appelants qui savent ce qu'ils veulent choisiraient directement.

Dans tous les cas, par exemple, le IDE sera plus utile: il est plus difficile de trouver toutes les méthodes qui transmettent des paramètres spécifiques, plutôt que de trouver une méthode plus spécialisée. En général, la maintenance et tout le reste est plus difficile lorsque nous confondons des concepts qui n'ont pas besoin d'être ensemble.

Vous pouvez voir que la question de savoir si la fonction est récursive ou non n'est pas pertinente pour cette analyse. Le problème pour moi est qu'il n'y a pas vraiment de mérite à confondre deux choses qui autrement pourraient être séparées; aucun mérite à l'idée que ces opérations autrement distinctes devraient être mises en place juste parce qu'elles prennent et renvoient toutes les deux un int (opèrent sur les mêmes données).

5
Erik Eidt

Je répondrai à cela dans les mots de Robert C. Martin de Clean Code:

Arguments de fonction

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.

[...]

Arguments du drapeau

Les arguments de drapeau sont moches. Passer un booléen dans une fonction est une pratique vraiment terrible. Cela complique immédiatement la signature de la méthode, proclamant haut et fort que cette fonction fait plus d'une chose. Cela fait une chose si le drapeau est vrai et une autre si le drapeau est faux!

Dans le listing 3-7, nous n'avions pas le choix car les appelants passaient déjà ce drapeau, et je voulais limiter la portée du refactoring à la fonction et ci-dessous. Pourtant, l'appel de méthode render(true) est tout simplement déroutant pour un mauvais lecteur. Passer la souris sur l'appel et voir render(boolean isSuite) aide un peu, mais pas tant que ça. Nous aurions dû diviser la fonction en deux: renderForSuite() et renderForSingleTest().

Donc, suivant ce conseil, votre fonction devrait être divisée en deux:

public static int func(int n) {
    return n + 1;
}

public static int funcRecursive(int n) {
    return n != 0 ? funcRecursive(n - 1) : 0;
}
1
CJ Dennis

Bien sûr.

Votre exemple particulier n'a pas de sens pour moi, mais il existe de nombreuses opérations qui ont un sens à exécuter de manière récursive ou non .

En particulier, vous pourriez avoir une structure hiérarchique coûteuse à parcourir. Lorsque vous recherchez cette structure ou récupérez des statistiques, il peut y avoir des cas où l'appelant a besoin de certitude (pour que vous recursiez) ou approximation (estimation basée sur des nœuds de niveau supérieur ou des résultats mis en cache).

Et il y a des cas où le niveau précis de récursivité doit être configurable. Prendre en compte --max-depth- types d'arguments pris en charge par les commandes Unix comme du et tree, par exemple. Si vous rendez une hiérarchie à un utilisateur, il est souvent judicieux de fournir un mécanisme pour limiter arbitrairement la proportion de la hiérarchie que vous parcourez et affichez.

0
svidgen

Le client décide deux choses simultanément dans votre exemple:

  1. Que faire (ajouter 1)
  2. Comment ajouter 1 (récursivement ou non)

Je ne peux pas imaginer un cas où un code comme celui-ci n'est pas:

  1. Difficile à comprendre et à entretenir et donc plus sujet aux bugs
  2. Plus difficile à atteindre
0
Tanager4