Je suis dans ma première classe de programmation au lycée. Nous faisons notre projet de fin du premier semestre. Ce projet implique une seule classe, mais plusieurs méthodes. Ma question est la meilleure pratique avec les variables d'instance et les variables locales. Il me semble qu'il serait beaucoup plus facile pour moi de coder en utilisant presque uniquement des variables d'instance. Mais je ne suis pas sûr que ce soit comme cela que je devrais le faire ou si je devrais utiliser davantage de variables locales (il me faudrait simplement que les méthodes prennent beaucoup plus en valeur les valeurs des variables locales).
Mon raisonnement à cela est aussi parce que bien souvent, je souhaiterai qu'une méthode retourne deux ou trois valeurs, mais ce n'est évidemment pas possible. Ainsi, il semble simplement plus facile d’utiliser simplement des variables d’instance et de ne jamais avoir à s’inquiéter car elles sont universelles dans la classe.
Je n'ai vu personne discuter de cela, alors je vais ajouter plus de matière à réflexion. La réponse courte/conseil est de ne pas utiliser les variables d'instance sur les variables locales simplement parce que vous pensez qu'elles sont plus faciles à renvoyer des valeurs. Vous allez rendre très difficile l'utilisation de votre code si vous n'utilisez pas les variables locales et les variables d'instance de manière appropriée. Vous produirez de sérieux bugs difficiles à détecter. Si vous voulez comprendre ce que je veux dire par bugs sérieux, et à quoi cela pourrait ressembler, lisez la suite.
Essayons d'utiliser uniquement des variables d'instance, comme vous le suggérez pour écrire dans des fonctions. Je vais créer un cours très simple:
public class BadIdea {
public Enum Color { GREEN, RED, BLUE, PURPLE };
public Color[] map = new Colors[] {
Color.GREEN,
Color.GREEN,
Color.RED,
Color.BLUE,
Color.PURPLE,
Color.RED,
Color.PURPLE };
List<Integer> indexes = new ArrayList<Integer>();
public int counter = 0;
public int index = 0;
public void findColor( Color value ) {
indexes.clear();
for( index = 0; index < map.length; index++ ) {
if( map[index] == value ) {
indexes.add( index );
counter++;
}
}
}
public void findOppositeColors( Color value ) {
indexes.clear();
for( index = 0; i < index < map.length; index++ ) {
if( map[index] != value ) {
indexes.add( index );
counter++;
}
}
}
}
C’est un programme stupide que je connais, mais nous pouvons l’utiliser pour illustrer le concept selon lequel l’utilisation de variables d’instance pour des choses comme celle-ci est une très mauvaise idée. La chose la plus importante que vous trouverez est que ces méthodes utilisent toutes les variables d'instance dont nous disposons. Et il modifie les index, les compteurs et les index chaque fois qu'ils sont appelés. Le premier problème que vous trouverez est que l'appel de ces méthodes l'une après l'autre peut modifier les réponses des exécutions précédentes. Donc, par exemple, si vous avez écrit le code suivant:
BadIdea idea = new BadIdea();
idea.findColor( Color.RED );
idea.findColor( Color.GREEN ); // whoops we just lost the results from finding all Color.RED
Puisque findColor utilise des variables d'instance pour suivre les valeurs renvoyées, nous ne pouvons renvoyer qu'un résultat à la fois. Essayons de garder une référence à ces résultats avant de la rappeler:
BadIdea idea = new BadIdea();
idea.findColor( Color.RED );
List<Integer> redPositions = idea.indexes;
int redCount = idea.counter;
idea.findColor( Color.GREEN ); // this causes red positions to be lost! (i.e. idea.indexes.clear()
List<Integer> greenPositions = idea.indexes;
int greenCount = idea.counter;
Dans ce deuxième exemple, nous avons sauvegardé les positions rouges sur la 3ème ligne, mais la même chose s’est produite!? Pourquoi les avons-nous perdues?! Parce que idea.indexes a été effacé au lieu d'être alloué, il ne peut y avoir qu'une seule réponse utilisée à la fois. Vous devez complètement utiliser ce résultat avant de le rappeler. Une fois que vous appelez à nouveau une méthode, les résultats sont effacés et vous perdez tout. Afin de résoudre ce problème, vous devrez attribuer un nouveau résultat à chaque fois afin que les réponses rouges et vertes soient séparées. Alors clonons nos réponses pour créer de nouvelles copies de choses:
BadIdea idea = new BadIdea();
idea.findColor( Color.RED );
List<Integer> redPositions = idea.indexes.clone();
int redCount = idea.counter;
idea.findColor( Color.GREEN );
List<Integer> greenPositions = idea.indexes.clone();
int greenCount = idea.counter;
Ok enfin nous avons deux résultats séparés. Les résultats du rouge et du vert sont maintenant séparés. Mais nous devions en savoir beaucoup sur le fonctionnement interne de BadIdea avant que le programme ne fonctionne, n'est-ce pas? Nous devons nous rappeler de cloner les déclarations chaque fois que nous l'appelons pour nous assurer en toute sécurité que nos résultats ne soient pas masqués. Pourquoi l'appelant est-il obligé de se souvenir de ces détails? Ne serait-il pas plus facile si nous n'avions pas à faire cela?
Notez également que l'appelant doit utiliser des variables locales pour mémoriser les résultats. Ainsi, même si vous n'utilisez pas de variables locales dans les méthodes de BadIdea, l'appelant doit les utiliser pour mémoriser les résultats. Alors qu'est-ce que vous avez vraiment accompli? Vous venez de déplacer le problème à l'appelant, ce qui l'oblige à en faire plus. Et le travail que vous avez poussé sur l'appelant n'est pas une règle facile à suivre car il existe de nombreuses exceptions à la règle.
Essayons maintenant de le faire avec deux méthodes différentes. Notez que j'ai été "intelligent" et que j'ai réutilisé ces mêmes variables d'instance pour "économiser de la mémoire" tout en maintenant le code compact. ;-)
BadIdea idea = new BadIdea();
idea.findColor( Color.RED );
List<Integer> redPositions = idea.indexes;
int redCount = idea.counter;
idea.findOppositeColors( Color.RED ); // this causes red positions to be lost again!!
List<Integer> greenPositions = idea.indexes;
int greenCount = idea.counter;
La même chose est arrivée! Zut mais j'étais tellement "intelligent" et économiser de la mémoire et le code utilise moins de ressources !!! C’est le vrai danger d’utiliser des variables d’instance, comme ceci: l’appel de méthodes dépend maintenant de l’ordre. Si je change l'ordre des appels de méthode, les résultats sont différents même si je n'ai pas vraiment changé l'état sous-jacent de BadIdea. Je n'ai pas changé le contenu de la carte. Pourquoi le programme donne-t-il des résultats différents lorsque j'appelle les méthodes dans un ordre différent?
idea.findColor( Color.RED )
idea.findOppositeColors( Color.RED )
Produit un résultat différent de celui obtenu si je permutais ces deux méthodes:
idea.findOppositeColors( Color.RED )
idea.findColor( Color.RED )
Il est très difficile de détecter ces types d’erreurs, en particulier lorsque ces lignes ne sont pas juxtaposées. Vous pouvez complètement casser votre programme en ajoutant simplement un nouvel appel n'importe où entre ces deux lignes et obtenir des résultats très différents. Bien sûr, lorsque nous traitons un petit nombre de lignes, il est facile de détecter les erreurs. Mais dans un programme plus vaste, vous pouvez perdre des jours à essayer de les reproduire même si les données du programme n’ont pas changé.
Et cela ne concerne que les problèmes à un seul thread. Si BadIdea était utilisé dans une situation multi-thread, les erreurs peuvent devenir vraiment bizarres. Que se passe-t-il si findColors () et findOppositeColors () sont appelés en même temps? Crash, tous tes cheveux tombent, la mort, l'espace et le temps s'effondrent en une singularité et l'univers est englouti? Probablement au moins deux d'entre eux. Les discussions sont probablement au-dessus de votre tête à présent, mais nous espérons pouvoir vous aider à éviter les mauvaises choses afin que ces mauvaises pratiques ne vous causent pas vraiment de chagrin.
Avez-vous remarqué à quel point vous deviez être prudent lorsque vous appelez les méthodes? Ils se sont écrasés les uns les autres, ils ont peut-être partagé la mémoire de façon aléatoire, vous deviez vous rappeler les détails de la façon dont cela fonctionnait à l'intérieur pour que cela fonctionne à l'extérieur, en modifiant l'ordre dans lequel les choses s'appelaient, produisiez de très grands changements dans les lignes suivantes, et cela ne pourrait fonctionner que dans une situation de thread unique. Faire de telles choses produira un code vraiment fragile qui semble s'effondrer à chaque fois que vous le touchez. Ces pratiques que j'ai montrées ont directement contribué à la fragilité du code.
Bien que cela puisse ressembler à une encapsulation, c’est exactement le contraire car les détails techniques de la rédaction doivent être connus de l’appelant . L'appelant doit écrire son code de manière très particulière pour que son code fonctionne, et il ne peut le faire sans connaître les détails techniques de votre code. Ceci est souvent appelé une abstraction qui fuit parce que la classe est supposée cacher les détails techniques derrière une abstraction/interface, mais les détails techniques ont été filtrés, forçant l'appelant à changer de comportement. Chaque solution présente un certain degré de fuite, mais l'utilisation de l'une des techniques ci-dessus, telles que celles-ci, garantit que quel que soit le problème que vous tentez de résoudre, elle sera terriblement perméable si vous les appliquez. Alors regardons le GoodIdea maintenant.
Réécrivons en utilisant des variables locales:
public class GoodIdea {
...
public List<Integer> findColor( Color value ) {
List<Integer> results = new ArrayList<Integer>();
for( int i = 0; i < map.length; i++ ) {
if( map[index] == value ) {
results.add( i );
}
}
return results;
}
public List<Integer> findOppositeColors( Color value ) {
List<Integer> results = new ArrayList<Integer>();
for( int i = 0; i < map.length; i++ ) {
if( map[index] != value ) {
results.add( i );
}
}
return results;
}
}
Cela corrige tous les problèmes dont nous avons parlé ci-dessus. Je sais que je ne surveille pas le compteur ni ne le renvoie, mais si je le faisais, je pourrai créer une nouvelle classe et la renvoyer à la place de la liste. Parfois, j'utilise l'objet suivant pour renvoyer plusieurs résultats rapidement:
public class Pair<K,T> {
public K first;
public T second;
public Pair( K first, T second ) {
this.first = first;
this.second = second;
}
}
Réponse longue, mais sujet très important.
Utilisez des variables d'instance lorsqu'il s'agit d'un concept fondamental de votre classe. Si vous effectuez des itérations, des traitements récurrents ou des traitements, utilisez des variables locales.
Lorsque vous devez utiliser deux variables (ou plus) aux mêmes endroits, il est temps de créer une nouvelle classe avec ces attributs (et les moyens appropriés pour les définir). Cela rendra votre code plus propre et vous aidera à réfléchir aux problèmes (chaque classe est un nouveau terme dans votre vocabulaire).
Une variable peut être transformée en classe s’il s’agit d’un concept fondamental. Par exemple, les identifiants du monde réel: ils peuvent être représentés sous forme de chaînes, mais souvent, si vous les encapsulez dans leur propre objet, ils commencent tout à coup à attirer des fonctionnalités (validation, association à d'autres objets, etc.)
La cohérence d'objet est également (pas entièrement liée) - un objet est capable de s'assurer que son état a un sens. La définition d'une propriété peut en modifier une autre. Il est également beaucoup plus facile de modifier votre programme pour qu'il soit thread-safe plus tard (si nécessaire).
Petite histoire: si et seulement si une variable doit être accédée par plusieurs méthodes (ou en dehors de la classe), créez-la en tant que variable d'instance. Si vous n'en avez besoin que localement, dans une seule méthode, il doit s'agir d'une variable locale. Les variables d'instance sont plus coûteuses que les variables locales.
N'oubliez pas: les variables d'instance sont initialisées sur les valeurs par défaut, alors que les variables locales ne le sont pas.
Les variables locales internes aux méthodes sont toujours préférées, car vous voulez garder la portée de chaque variable aussi petite que possible. Mais si plus d'une méthode a besoin d'accéder à une variable, alors il faudra que ce soit une variable d'instance.
Les variables locales ressemblent davantage à des valeurs intermédiaires utilisées pour atteindre un résultat ou calculer quelque chose à la volée. Les variables d'instance ressemblent davantage aux attributs d'une classe, comme votre âge ou votre nom.
Déclarez que les variables doivent être étendues aussi étroitement que possible. Déclarez les variables locales en premier. Si cela ne suffit pas, utilisez des variables d'instance. Si cela ne suffit pas, utilisez des variables de classe (statiques).
Si vous devez renvoyer plusieurs valeurs, renvoyez une structure composite, comme un tableau ou un objet.
Le moyen le plus simple: si la variable doit être partagée par plusieurs méthodes, utilisez la variable instance, sinon utilisez la variable locale.
Cependant, la bonne pratique consiste à utiliser autant de variables locales que possible. Pourquoi? Pour votre projet simple avec une seule classe, il n'y a pas de différence. Pour un projet qui comprend beaucoup de classes, la différence est grande. La variable d'instance indique l'état de votre classe. Plus il y a de variables d'instance dans votre classe, plus cette classe peut avoir d'états, et plus cette classe est complexe, plus la classe est difficile à maintenir ou plus le projet est sujet aux erreurs. La bonne pratique consiste donc à utiliser la variable la plus locale possible pour conserver l’état de la classe aussi simple que possible.
Essayez de penser à votre problème en termes d'objets. Chaque classe représente un type d'objet différent. Les variables d'instance sont les éléments de données qu'une classe doit garder en mémoire pour pouvoir travailler avec elle-même ou avec d'autres objets. Les variables locales doivent simplement être utilisées pour les calculs intermédiaires, données que vous n'avez pas besoin de sauvegarder une fois que vous quittez la méthode.
Essayez de ne pas renvoyer plus d'une valeur de vos méthodes en premier lieu. Si vous ne pouvez pas, et dans certains cas, vous ne pouvez vraiment pas, alors je recommanderais d'encapsuler cela dans une classe. Juste dans le dernier cas, je vous recommanderais de changer une autre variable dans votre classe (une variable d'instance). Le problème de l’approche des variables d’instance est qu’elle augmente les effets secondaires - par exemple, vous appelez la méthode A dans votre programme et modifiez des variables d’instance (s). Avec le temps, votre code devient de plus en plus complexe et la maintenance devient de plus en plus difficile.
Lorsque je dois utiliser des variables d'instance, j'essaie de rendre final et d'initialiser ensuite dans les constructeurs de classe afin de minimiser les effets secondaires. Ce style de programmation (minimisant les changements d'état dans votre application) devrait conduire à un meilleur code, plus facile à gérer.
Généralement, les variables doivent avoir une portée minimale.
Malheureusement, pour construire des classes avec une portée de variable minimisée, il est souvent nécessaire de passer beaucoup de paramètres de méthode.
Mais si vous suivez ce conseil tout le temps, minimisant parfaitement la portée variable, vous risquez de vous retrouver avec beaucoup de redondance et d'inflexibilité de la méthode avec tous les objets requis passés dans les méthodes.
Imaginez une base de code avec des milliers de méthodes comme celle-ci:
private ClassThatHoldsReturnInfo foo(OneReallyBigClassThatHoldsCertainThings big,
AnotherClassThatDoesLittle little) {
LocalClassObjectJustUsedHere here;
...
}
private ClassThatHoldsReturnInfo bar(OneMediumSizedClassThatHoldsCertainThings medium,
AnotherClassThatDoesLittle little) {
...
}
Et, d'un autre côté, imaginez une base de code avec beaucoup de variables d'instance comme celle-ci:
private OneReallyBigClassThatHoldsCertainThings big;
private OneMediumSizedClassThatHoldsCertainThings medium;
private AnotherClassThatDoesLittle little;
private ClassThatHoldsReturnInfo ret;
private void foo() {
LocalClassObjectJustUsedHere here;
....
}
private void bar() {
....
}
À mesure que le code augmente, le premier moyen peut minimiser au mieux la portée de la variable, mais peut facilement conduire à la transmission de nombreux paramètres de méthode. Le code sera généralement plus détaillé et cela peut conduire à une complexité car on refonte toutes ces méthodes.
L'utilisation d'un plus grand nombre de variables d'instance peut réduire la complexité liée à la transmission d'un grand nombre de paramètres de méthode et peut donner de la flexibilité aux méthodes lorsque vous réorganisez fréquemment les méthodes dans un souci de clarté. Mais cela crée plus d'état d'objet que vous devez maintenir. En règle générale, il est conseillé de faire le premier et de s'abstenir du second.
Cependant, très souvent, et cela dépend de la personne, on peut gérer plus facilement la complexité des états par rapport aux milliers de références d'objet supplémentaires du premier cas. On peut le remarquer lorsque la logique métier au sein des méthodes augmente et que l'organisation doit changer pour maintenir l'ordre et la clarté.
Non seulement que. Lorsque vous réorganisez vos méthodes pour conserver la clarté et effectuez de nombreuses modifications de paramètres de méthode au cours du processus, vous vous retrouvez avec de nombreux diffs de contrôle de version, ce qui n’est pas si bon pour un code de qualité de production stable. Il y a un équilibre. Une façon provoque une sorte de complexité. L'autre façon provoque un autre type de complexité.
Utilisez la méthode qui vous convient le mieux. Vous trouverez cet équilibre dans le temps.
Je pense que ce jeune programmeur a des premières impressions perspicaces pour un code nécessitant peu de maintenance.
Utiliser une variable d'instance lorsque
De la même manière, utilisez une variable locale lorsqu'aucune de ces conditions ne correspond, en particulier le rôle de la variable se terminerait une fois la pile extraite. par exemple: Comparator.compare (o1, o2);