web-dev-qa-db-fra.com

Est-ce que "les variables devraient vivre dans la plus petite étendue possible" incluent le cas "les variables ne devraient pas exister si possible"?

Selon la réponse acceptée sur "Justification de préférer les variables locales aux variables d'instance?", les variables devraient vivre dans la plus petite étendue possible.
Simplifiez le problème dans mon interprétation, cela signifie que nous devons refactoriser ce type de code:

public class Main {
    private A a;
    private B b;

    public ABResult getResult() {
        getA();
        getB();
        return ABFactory.mix(a, b);
    }

    private getA() {
      a = SomeFactory.getA();
    }

    private getB() {
      b = SomeFactory.getB();
    }
}

en quelque chose comme ça:

public class Main {
    public ABResult getResult() {
        A a = getA();
        B b = getB();
        return ABFactory.mix(a, b);
    }

    private getA() {
      return SomeFactory.getA();
    }

    private getB() {
      return SomeFactory.getB();
    }
}

mais selon "l'esprit" de "les variables devraient vivre dans la plus petite étendue possible", "n'est-il jamais de variables" ont une portée plus petite que "ont des variables"? Je pense donc que la version ci-dessus devrait être refactorisée:

public class Main {
    public ABResult getResult() {
        return ABFactory.mix(getA(), getB());
    }

    private getA() {
      return SomeFactory.getA();
    }

    private getB() {
      return SomeFactory.getB();
    }
}

de sorte que getResult() n'a aucune variable locale du tout. Est-ce vrai?

32
ocomfd

Non. Il y a plusieurs raisons pour lesquelles:

  1. Les variables avec des noms significatifs peuvent rendre le code plus facile à comprendre.
  2. La division de formules complexes en étapes plus petites peut faciliter la lecture du code.
  3. Mise en cache.
  4. Tenir des références à des objets afin qu'ils puissent être utilisés plusieurs fois.

Etc.

110
Robert Harvey

D'accord, les variables qui ne sont pas nécessaires et n'améliorent pas la lisibilité du code doivent être évitées. Plus il y a de variables dans la portée à un point donné du code, plus ce code est complexe à comprendre.

Je ne vois pas vraiment l'avantage des variables a et b dans votre exemple, donc j'écrirais la version sans variables. D'un autre côté, la fonction est si simple en premier lieu que je ne pense pas que cela compte beaucoup.

Cela devient plus un problème plus la fonction est longue et plus les variables sont de portée.

Par exemple, si vous avez

    a=getA();
    b=getB();
    m = ABFactory.mix(a,b);

Au sommet d'une fonction plus large, vous augmentez la charge mentale de la compréhension du reste du code en introduisant trois variables plutôt qu'une. Vous devez lire le reste du code pour voir si a ou b sont à nouveau utilisés. Les sections locales dont la portée est plus longue que nécessaire sont mauvaises pour la lisibilité globale.

Bien sûr, dans le cas où une variable est nécessaire (par exemple pour stocker un résultat temporaire) ou lorsqu'une variable ne améliore la lisibilité du code, alors elle doit être conservée.

16
JacquesB

En plus des autres réponses, je voudrais signaler autre chose. L'avantage de garder la portée d'une variable petite n'est pas seulement de réduire la quantité de code ayant un accès syntaxique à la variable, mais également de réduire le nombre de chemins de flux de contrôle possibles qui peuvent potentiellement modifier une variable (soit en lui attribuant une nouvelle valeur ou en appelant une méthode de mutation sur l'objet existant contenu dans la variable).

Les variables de portée de classe (instance ou statique) ont beaucoup plus de chemins de flux de contrôle possibles que les variables de portée locale car elles peuvent être mutées par des méthodes, qui peuvent être appelées dans n'importe quel ordre, n'importe quel nombre de fois et souvent par du code en dehors de la classe .

Jetons un coup d'œil à votre méthode initiale getResult:

public ABResult getResult() {
    getA();
    getB();
    return ABFactory.mix(this.a, this.b);
}

Maintenant, les noms getA et getB peuvent suggérer qu'ils attribueront à this.a et this.b, nous ne pouvons pas savoir avec certitude rien qu'en regardant getResult. Ainsi, il est possible que le this.a et this.b les valeurs passées à la méthode mix proviennent plutôt de l'état de l'objet this d'avant getResult a été invoqué, ce qui est impossible à prévoir car les clients contrôlent comment et quand les méthodes sont appelés.

Dans le code révisé avec les variables locales a et b, il est clair qu'il existe exactement un flux de contrôle (sans exception) entre l'affectation de chaque variable et son utilisation, car le les variables sont déclarées juste avant d'être utilisées.

Ainsi, le déplacement de variables (modifiables) de portée de classe vers portée locale (ainsi que le déplacement de variables (modifiables) de l'extérieur d'une boucle vers l'intérieur) présente un avantage significatif en ce qu'il simplifie le raisonnement du flux de contrôle.

D'un autre côté, l'élimination des variables comme dans votre dernier exemple présente moins d'avantages, car elle n'affecte pas vraiment le raisonnement du flux de contrôle. Vous perdez également les noms donnés aux valeurs, ce qui ne se produit pas lorsque vous déplacez simplement une variable vers une portée interne. Il s'agit d'un compromis que vous devez considérer, donc l'élimination des variables peut être meilleure dans certains cas, et pire dans d'autres.

Si vous ne voulez pas perdre les noms de variables, mais souhaitez tout de même réduire la portée des variables (dans le cas où elles sont utilisées dans une fonction plus grande), vous pouvez envisager d'encapsuler les variables et leur utilisation dans ne instruction de bloc (ou les déplacer vers leur propre fonction ).

11
YawarRaza7349

C'est quelque peu dépendant du langage, mais je dirais que l'un des avantages moins évidents de la programmation fonctionnelle est qu'elle encourage le programmeur et le lecteur de code à ne pas en avoir besoin. Considérer:

(reduce (fn [map string] (assoc map string (inc (map string 0))) 

Ou certains LINQ:

var query2 = mydb.MyEntity.Select(x => x.SomeProp).AsEnumerable().Where(x => x == "Prop");

Ou Node.js:

 return visionFetchLabels(storageUri)
    .then(any(isCat))
    .then(notifySlack(secrets.slackWebhook, `A cat was posted: ${storageUri}`))
    .then(logSuccess, logError)
    .then(callback)

Le dernier est une chaîne d'appel d'une fonction sur le résultat d'une fonction précédente, sans aucune variable intermédiaire. Leur présentation rendrait cela beaucoup moins clair.

Cependant, la différence entre le premier exemple et les deux autres est l'ordre implicite de fonctionnement . Ce n'est peut-être pas la même chose que l'ordre dans lequel il est réellement calculé, mais c'est l'ordre dans lequel le lecteur devrait y penser. Pour les deux derniers, c'est de gauche à droite. Pour l'exemple LISP/Clojure, cela ressemble plus à droite à gauche. Vous devriez vous méfier un peu de l'écriture de code qui n'est pas dans la "direction par défaut" pour votre langue, et les expressions "au milieu" qui mélangent les deux devraient être évitées.

Opérateur de tuyau de F # |> est utile en partie parce qu'il vous permet d'écrire des choses de gauche à droite qui devraient autrement être de droite à gauche.

2
pjc50

Je dirais non, car vous devriez lire "la plus petite portée possible" comme "parmi les portées existantes ou celles qu'il est raisonnable d'ajouter". Sinon, cela impliquerait que vous créiez des étendues artificielles (par exemple, des blocs gratuits {} Dans des langages de type C) juste pour vous assurer que la portée d'une variable ne s'étend pas au-delà de la dernière utilisation prévue, et cela serait généralement mal vu comme obscurcissement/encombrement, sauf s'il existe déjà une bonne raison pour que la portée existe indépendamment.

Considérez fonctions (méthodes). Là, il ne s'agit pas de diviser le code dans la plus petite sous-tâche possible, ni le plus grand morceau de code singleton.

Il s'agit d'une limite de déplacement, avec délimitation des tâches logiques, en pièces consommables.

Il en va de même pour variables. Soulignant les structures de données logiques, en parties compréhensibles. Ou aussi simplement nommer (énoncer) les paramètres:

boolean automatic = true;
importFile(file, automatic);

Mais bien sûr, ayant une déclaration en haut, et deux cents lignes plus loin, le premier usage est aujourd'hui accepté comme un mauvais style. C'est clairement ce que "les variables doivent vivre dans le plus petit champ possible" entend dire. Comme un très proche "ne réutilise pas les variables."

1
Joop Eggen

Ce qui manque quelque peu comme raison de NON est le débogage/la lisibilité. Le code doit être optimisé pour cela, et les noms clairs et concis aident beaucoup, par exemple imaginez un 3 voies si

if (frobnicate(x) && (get_age(x) > 2000 || calculate_duration(x) < 100 )

cette ligne est courte, mais déjà difficile à lire. Ajoutez quelques paramètres supplémentaires, et cela si s'étend sur plusieurs lignes.

can_be_frobnicated = frobnicate(x)
is_short_lived_or_ancient = get_age(x) > 2000 || calculate_duration(x) < 100
if (can_be_frobnicated || is_short_lived_or_ancient )

Je trouve cette façon plus facile à lire et à communiquer le sens - je n'ai donc aucun problème avec les variables intermédiaires.

Un autre exemple serait des langues comme R, où la dernière ligne est automatiquement la valeur de retour:

some_func <- function(x) {
    compute(x)
}

c'est dangereux, le retour est-il expexté ou nécessaire? c'est plus clair:

some_func <- function(x) {
   rv <- compute(x)
   return(rv)
}

Comme toujours, il s'agit d'un appel au jugement - éliminer les variables intermédiaires si elles n'améliorent pas la lecture, sinon les conserver ou les introduire.

Un autre point peut être le débogage: si les résultats intermédiaires sont intéressants, il peut être préférable d'introduire simplement un intermédiaire, comme dans l'exemple R ci-dessus. La fréquence à laquelle cela est demandé est difficile à imaginer et faites attention à ce que vous enregistrez - trop de variables de débogage prêtent à confusion - encore une fois, un appel au jugement.

1
Christian Sauer

En se référant uniquement à votre titre: absolument, si une variable n'est pas nécessaire, elle doit être supprimée.

Mais "inutile" ne signifie pas qu'un programme équivalent peut être écrit sans utiliser la variable, sinon on nous dirait que nous devrions tout écrire en binaire.

Le type de variable inutile le plus courant est une variable inutilisée, plus la portée de la variable est petite, plus il est facile de déterminer qu'elle est inutile. Il est plus difficile de déterminer si une variable intermédiaire est inutile, car ce n'est pas une situation binaire, c'est contextuel. En fait, un code source identique dans deux méthodes différentes pourrait produire une réponse différente par le même utilisateur en fonction de l'expérience passée en résolvant des problèmes dans le code environnant.

Si votre exemple de code était exactement tel que représenté, je suggérerais de vous débarrasser des deux méthodes privées, mais vous vous soucieriez peu de savoir si vous avez enregistré le résultat des appels d'usine dans une variable locale ou si vous les avez simplement utilisés comme arguments du mélange méthode.

La lisibilité du code l'emporte sur tout sauf le fonctionnement correct (inclut correctement des critères de performance acceptables, qui sont rarement "aussi rapides que possible").

0
jmoreno