[Java] comment bien utiliser les getters et setters en général?

Le problème exposé dans ce sujet a été résolu.

Bonjour,

ligne 5 : est ce correct comme ça ou vaut-il mieux this.surnom = surnom; ?

Ca dépend. En utilisant le setter, tu effectues le contrôle de validation dès la construction sans dupliquer le code.

Dans ce petit exemple c’est évidemment un avantage, et si tu suis la logique du DDD comme mode de développement, c’est un très très bon réflexe.

Selon les cas, ça peut toutefois poser problème: si les calculs sont lourds dans le setter, si la validation ne doit pas être faite à la construction mais à un autre moment, ou si tu dois renvoyer les erreurs en une seule fois de manière groupée à l’utilisateur. Dans ces cas tu ne peux pas valider les données directement dans les setters, tu as besoin d’une méthode spécifique pour ça.

ligne 16 : est ce OK de gérer des erreurs comme ça?

Mème réponse que ci-dessus: si c’est OK pour toi de valider un à un les attributs dès leur modification, oui et c’est un très bon réflexe; sinon, non. Pour les besoins de ton cours ça sera sûrement plutôt oui.

A cela près qu’il ne faut pas utiliser RuntimeException mais un type plus spécifique d’exception représentant plus précisément l’erreur: IllegalArgumentException (paramètre incorrect), IndexOutOfBoundsException (paramètre hors bornes admissibles), NoSuchElementException (élément non trouvé), IllegalStateException (modification de l’attribut au mauvais moment), etc. ou un type d’exception perso si tu en as un (ce n’est pas obligatoire, c’est seulement utile d’en créer un si tu peux apporter des informations plus précises sur l’erreur que par les types standards). En principe on n’utilise pas RuntimeException en tant que tel mais uniquement ses sous-classes.

Dans ton cas ici le type correct à utiliser serait donc sans doute IllegalArgumentException.

+1 -0

Bonjour, en java c’est tellement devenu une convention de faire des setter / getter, que l’éditeur Eclipse propose une génération automatique de code pour tous les attributs d’une classe. Et, du coup, c’est tellement devenu du code "redondant" qu’une lib java nous permet maintenant de même plus l’écrire, ça diminue drastiquement la lisibilité du code, qui est généré à la compilation, remplacé par une simple annotation, c’est lombok : https://projectlombok.org/features/Data qq1 sait s’il existe l’équivalent en php?

Bonjour, en java c’est tellement devenu une convention de faire des setter / getter, que l’éditeur Eclipse propose une génération automatique de code pour tous les attributs d’une classe. Et, du coup, c’est tellement devenu du code "redondant" qu’une lib java nous permet maintenant de même plus l’écrire, ça diminue drastiquement la lisibilité du code, qui est généré à la compilation, remplacé par une simple annotation, c’est lombok : https://projectlombok.org/features/Data qq1 sait s’il existe l’équivalent en php?

pifou

Génial, comment faire les accesseurs automatiquement ??

Vu qu’un de mes anciens posts a été référencé, je ne m’attarde pas pour insister sur tout le mal que je pense des setters.

Une autre raison de passer par le niveau d’indirection des getters/setters est la compatibilité : si plus tard on change le nom des champs, leur type, etc., souvent on peut toujours exporter les getter/setter d’avant qui font la conversion au vol.

gasche

Combien de fois as-tu professionnellement eu besoin de ça? Et saurais-tu estimer le temps que tu as gagné?

En ce qui me concerne, je n’ai pas souvenir d’avoir galéré une seule fois en une 20 aine d’années, et le temps gagné est conséquence du nombre de setters que j’ai su éviter (pour penser OO, i.e. services) depuis que j’ai pris conscience que l’on m’avait menti — pas nécessairement à escient, cf la guignolitude auto-alimentée évoquée par Michel (et requalifiée avec mes mots).

Pour l’instant mon choix est que je suis prêt à payer une erreur de compilation pour les cas où je change radicalement d’interface et que je ne puisse pas profiter d’inférence de type (avec auto, bossant essentiellement en C++). Après j’ai droit aux typedefs aussi, de fait, les quelques cas qui auraient pu être critiques ont dû être absorbés.


Pour l’exemple du Chat. Quel est le besoin véritable derrière ce SetNickname? Si on parle le nom, il est unique et affecté à la création, il n’a plus besoin de changer. Un surnom, ne sera pas unique et on pourra en rajouter, ou en enlever. Set n’est donc plus la bonne action.

Quant aux contrôles réalisés, je vois ici de la programmation excessivement défensive qui cherche à occuper le terrain de la programmation par contrat. C’est à l’appelant de vérifier les préconditions. Au niveau de l’appelé, on peut à la limite réaliser un contrôle supplémentaire pour balancer des exceptions de type erreur de programmation dans le but d’espérer de robustifier un code dans lequel il reste des erreurs une fois livré.

J’ai vu une conf il y a peu qui aborde le sujet et passe rapidement sur le cas de langages qui officialisent l’approche exceptions dans tous les sens, même si au final on ne sait pas trop quoi en faire. Je me demande si ce n’était pas dans le talk d’Herb Sutter au dernier ACCU — et j’aime beaucoup la direction qu’il prend à considérer que notre std::logic_error, en C++, était en toute logique une erreur de conception qui ne devrait pas exister.

Combien de fois as-tu professionnellement eu besoin de ça?

Assez souvent en fait (pas en Java, que j’ai la chance de ne pas utiliser); je développe du code qui est utilisé par pas mal de gens derrière, et non une application où tout le code est au même endroit et peut être mis à jour d’un seul coup. On a en plus des contraintes de compatibilité, des gens qui utilisent des anciennes versions de nos bibliothèques et qui veulent pouvoir écrire du code compatible avec à la fois les nouvelles et les anciennes versions.

Je ne veux pas dire qu’il faut mettre des setters partout par principe, mais il n’empêche que passer par une indirection de fonction plutôt que d’écrire directement dans des champs peut être utile en cas de changement de représentation. La compatibilité se pense en amont pour une partie si possible bien définie et pas trop grosse de l’interface qu’on expose, et le fait de se restreindre à passer par des setters peut être une des techniques mises en place.

en java c’est tellement devenu une convention de faire des setter / getter, que l’éditeur Eclipse propose une génération automatique de code pour tous les attributs d’une classe.

marius007

Faut voir ça autrement

  • de temps en temps on a besoin d’en faire
  • il me semble (j’y connais rien en fait) qu’il en faut des tonnes avec les JavaBeans
  • c’est complètement trivial, pour un IDE moderne, de proposer ce genre de trucs, comme les constructeurs, toString, hash et tutti quanti
  • quand on en a besoin, ça prend deux secondes avec l’IDE

ce qui n’est absolument pas une raison pour en coller partout, et surtout en faire le paradigme pour modifier l’état d’un objet.

Malheureusement, les exemples de classes qu’on trouve dans la plupart des introductions à la POO, c’est des machins qui sont de simples agrégats de données dont la seule responsabilité identifiable est de contenir des champs. Au mieux on leur ajoute une méthode afficher(), ce qui est passablement idiot (il faut mieux que l’objet soit capable de fournir sa description textuelle, c’est pas son job de piloter la console).

Quand je dis que ça incite à faire des conneries, un exemple : une classe Personne, qui ont des surnoms. Stockés dans un attribut ArrayList<String>. Avec une methode ajouterSurnom(String surnomSupplémentaire). Jusque là tout va bien. Et là

class Personne {
    private ArrayList<String> surnoms;
    ...
    public ArrayList<String> getSurnoms() {
        return surnoms;
    }
}

What could possibily go wrong?, comme disait l’autre.

+1 -0

Je ne fais pas de Java donc je ne suis pas sûr de la sémantique, mais j’imagine que la méthode renvoie une référence sur la liste, son contenu peut donc être altéré depuis l’extérieur.

entwanne

Tout à fait : si un getter retourne la référence d’un attribut qui est un objet mutable, ça donne les clés de la boutique pour aller tripoter ce qu’il y a dedans sans passer par l’objet qui est censé s’en occuper.

    Personne p = ....

    ArrayList<String> liste = p.getSurnoms();

    liste.clear();    // bingo
+1 -0

En oubliant les données, et en se focalisant sur les services à mettre à disposition. Tant que l’on voit des objets comme des agrégats de données manipulés par un tiers on ne peut pas avancer dans la bonne direction.

A la place, tu auras des fonctions pour savoir How may I call you?, ou pour voir qui répond si on utilise tel ou tel nom Do you answer on {name}?, etc.

Comment corriger ce problème ?

La réponse courte: renvoyer des copies non mutables.

return Collections.unmodifiableList(surnoms);

ON devrait le faire systématiquement… mais dans 99% des cas on oublie, même en prod.

EDIT: quelqu’un m’explique comment le site m’a concaténé ce message avec un de mes posts précédents ???

+0 -0

La réponse courte: renvoyer des copies non mutables.

return Collections.unmodifiableList(surnoms);

On peut aussi se poser la question : est-ce que mon besoin, c’est VRAIMENT d’avoir une collection, ou plutot de pouvoirfaire quelque chose avec chacun des surnoms.

Parce que là c’est typique d’un "problème XY", le problème X c’est que je veux la possibilité de faire quelque chose avec chaque surnom "donc" je retourne une collection, et problème Y : mince si je retourne l’attribut je suis emmerdé, hop, copie ou unmodifiable

Si on rembobine un peu, ça ouvre plein de possibilités

  • et si ça retournait un itérateur sur les surnoms ? (unmodifiable)
  • et si ça retournait un itérable sur les surnoms ? (unmodifiable)
  • et ça appliquait un Consumer à chaque surnom ?
void pourChaqueSurnom(Consumer<String> action)) {
    surnoms.forEach(action);
}
  • et si ça retournait un stream ? (actions parallélisables !)
+0 -0

Pour gérer tout ça et aller au-delà, on peut introduire les notions d’objets immutables (qui ne peuvent pas être modifiés) et de builders.

Les objets immutables sont pratiques dans le sens où tu as la garantie qu’une fois créés, il ne vont pas être modifiés dans ton dos. Par contre les utiliser massivement met une pression supplémentaire sur le garbage collector, puisque que par définition tu ne peux pas réutiliser de tels objets.

Les builders permettent de garantir que ton objet sera construit correctement (et généralement via du code plus lisible qu’un constructeur, surtout si tu as beaucoup de paramètres).

La question de la compatibilité est plus subtile. De ce que j’ai vu, elle se résume en général à deux cas dans les projets tels qu’on les trouve en Java (qu’il faudra qu’on m’explique en quoi c’est une chance de ne pas utiliser) :

  1. Tu as une obligation de compatibilité parce que tu parles d’un élément d’une API : tu as donc une obligation de compatibilité sur une interface (qu’elle soit matérialisée ou non par une interface Java), et qu’un champ d’une classe fasse partie d’une interface semble de base une mauvaise idée. Mais peut-on parler de getter/setter sur un élément d’interface ? Normalement non, parce que normalement ton interface n’expose pas le fonctionnement interne du système.
  2. Tu est sur une classe à usage interne uniquement, et là le refactoring ne pose aucun problème.

Donc, se dire qu’utiliser des getters/setters pour garantir la compatibilité ressemble fortement à une bonne solution à une mauvaise question, en ce sens que si la compatibilité est importante, alors tu dois définir une interface indépendamment de la représentation interne. Il se peut que cette interface ressemble à des getters/setters, mais pas qu’elle soit définie parce que ce sont à l’origine des getters/setters.

D’une manière générale, chaque ligne de code devrait exister pour une raison bien précise et maîtrisée, et mieux comprise que « on fait toujours comme ça » ou « j’ai mis ça là au cas où ».

Alors que se passe t’il si j’essaye d’éviter les getters en faisant comme ça :

class Personnage {
    private Voiture voiture;
    private String marqueVoiture;

    public Personnage(Voiture v) {
        this.voiture = v;
        this.marqueVoiture = v.getMarque();
    }

    public void faireQqch() {
        qqch(this.marqueVoiture);
    }
}

Je procède comme ça pour respecter la loi de demeter et donc éviter à la ligne 11 :

qqch(this.voiture.getMarque());

À mon avis là tu donnes à ton personnage des responsabilités qu’il ne devrait pas avoir : la marque de la voiture ne devrait pas être attribuée au personnage. Savoir la marque d’une voiture à partir d’un objet de type voiture n’est pas choquant, qu’un personnage puisse l’utiliser directement à partir d’une voiture donnée non plus.

+0 -0

Déjà, une personne peut avoir plusieurs voitures. Ou pas.

Avant d’appliquer la loi de Demeter, il faut commencer par avoir un truc qui tient debout…

class Voiture {
   String plaque;
   String getPlaque() { return plaque; }
   ...
}

class Personne {
   ArrayList<Voiture> voitures;

   void pourChaquePlaque(Consumer<String> action) {
       voitures.forEach(v -> action.accept(v.getPlaque());
   }
}


Personne p;
...
p.pourChaquePlaque(System.out :: println);
+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte