Bonnes pratiques de programmation en Java
Par batmat le lundi 28 juin 2004, 20:21 - Technique - Lien permanent
Aujourd'hui, on va parler de best practices (ou bonnes pratiques en bon françois) de programmation en Java. On parlera entre autre des quelques classes de l'API fonctionnellement dépréciées, on parlera aussi d'évolutivité et d'héritages abusifs, le tout encadré de design patterns.
Les classes interdites
Je rencontre encore sur le net certains projets qui utilisent encore les 2 classes suivantes (cette liste n'est certainement pas exhaustive, mais ces deux là sont les plus courantes) :
- Hashtable
- Vector
Vous ne devez pas utiliser ces classes. Elles ne sont toujours présentes que pour assurer une compatibilité ascendante : la jvm 1.4 doit pouvoir faire tourner un programme écrit en 1.0 qui peut contenir des objets de ce type puisque leur alternative n'est apparue que pour la version 1.2.
Depuis la 1.2 donc, il ne faut plus les utiliser. Mais alors pourquoi, vous allez me dire, est-ce qu'ils n'ont pas déprécié ces classes à partir de la 1.2 ? C'est très simple : hashtable est une table de hâchage (une liste de clé-valeur) et Vector une classe qui gère une liste d'objets... C'est à dire des objets extrêmement courants et utilisés. Sun a donc décidé de ne pas les déprécier pour éviter plusieurs centaines voire milliers de lignes de warning à la compilation de projets développés à l'époque du jdk 1.2.
pourquoi ne pas les utiliser ?
Ces classes sont synchronisées, elles sont ce qu'on appelle thread-safe. Cela signifie que dans un environnement contenant plusieurs fils de processus, si vous utilisez ces objets, vous vous garantissez une cohérence de leurs données, ce qui ne serait pas le cas si elles n'étaient pas synchronisées.
Le problème c'est que cette fonctionnalité est loin d'être utilisée dans tous les projets, on n'utilise pas forcément ses objets dans un contexte concurrentiel partout (encore heureux). En terme de performances, cette synchronisation n'est pas gratuite, on peut même dire qu'elle est plutôt chère (on parle communément d'un rapport de 1 à 5). Vous vous rendez alors bien compte qu'utiliser un Vector pour stocker vos listes est le plus souvent une perte totalement inutile de performances.
Les alternatives
Mais alors donc quelles classes utiliser en remplacement ? Celles-ci, leurs équivalents non thread-safe :
ArrayListpour VectorHashMappour Hashtable
Et si je veux un conteneur thread-safe ?
Si vous avez pensé à poser cette question, bravo. Dans un environnement concurrentiel, il faut toujours synchroniser les objets d'une façon ou d'une autre... Il y a deux solutions pour ce faire :
Synchronisation aux endroits où c'est nécessaire en utilisant un conteneur non synchronisé
- Avantage : Gain possible en performances car vous maîtrisez parfaitement la synchronisation de votre code ;
- Inconvénient : Plus difficile parce que vous devez faire très attention de ne pas faire d'oubli dans cette synchro si vous ne voulez pas obtenir des résultats au mieux bizarres, au pire catastrophiques.
Utilisation de conteneur synchronisé :
- Avantage : Il suffit de se concentrer une bonne fois sur le codage du conteneur et ensuite on peut l'utiliser sans crainte de créer des inconsistences à l'exécution ;
- Inconvénient : Perte possible de performance parce qu'il va peut-être y avoir des synchronisation à des endroits où ce n'est pas nécessaire.
Heureusement, vous vous dites, il existe donc Vector pour les cas où on veut utiliser un conteneur _thread-safe_. Et bien non, vous pouvez jeter cette classe, vous-n'en-aurez-plus-besoin :-). À la place, utilisez la méthode statique Collections.synchronizedList. Vous obtiendrez ainsi une liste synchronisée à partir d'une qui ne l'est pas, en appliquant de plus l'important principe qui consiste à cacher les implémentations dont je vais maintenant parler.
Améliorer l'évolutivité du code
Masquer l'implémentation
Ce qui fait que beaucoup (trop) d'utilisations de Vector subsistent encore aujourd'hui, c'est que cette classe était utilisée partout sans être masquée par une classe ou une interface.
Depuis la version 1.2, Vector implémente l'interface List comme le font ArrayList et LinkedList. Lorsqu'on utilise une liste, il ne faut donc plus indiquer à l'extérieur qu'on utilise telle ou telle implémentation mais il faut simplement indiquer qu'on utilise une classe qui implémente l'interface List. En faisant systématiquement comme ça, vous vous laissez la possibilité de changer d'implémentation ultérieurement. S'il vous prend par exemple l'envie de ne plus utiliser une ArrayList mais une implémentation personnelle de List pour telle ou telle raison, personne ne le saura et tout le reste du code restera inchangé (et ça, c'est très important).
Les factory
Utilisez des factory pour créer vos objets. Vous pourrez ainsi facilement cacher une implémentation.
Exemple : Pouet doit pouvoir dire bonjour
public interface Pouet
{
/**
* Affiche bonjour à l'écran
*/
public void sayBonjour();
}
Très bien, la première chose à faire est donc d'écrire une implémentation de Pouet :
public class PouetImpl implements Pouet
{
public void sayBonjour()
{
System.out.println("Bonjour");
}
}
Si vous ne faites que ça, les utilisateurs de votre classe n'auront pas d'autre choix que d'écrire
PouetImpl pouet = new PouetImpl();
Et s'ils sont un petit peu plus expérimentés
Pouet pouet = new PouetImpl();
Et ils utiliseront Pouet et non PouetImpl partout dans leur code, mais ce n'est pas forcément bon parce que vous laissez d'autres que vous faire des choix de méthode d'utilisation de votre code. Autant que faire se peut, il doit toujours y avoir un minimum de possibilité pour faire quelque chose, sinon vous vous exposez à de gros problèmes de maintenance...
Et s'ils le sont encore plus, ils pourront avoir envie d'utiliser un framework de découplage, communément appelé conteneur léger en opposition avec les conteneurs d'EJB beaucoup plus intrusifs et lourds à mettre en oeuvre, comme spring, PicoContainer ou Avalon qui permettent d'utiliser de simples POJOs.
Ou alors, il écriront eux-mêmes la factory que vous auriez dû fournir :
public final class PouetFactory
{
public Pouet createPouet()
{
return new PouetImpl();
}
/**
* Singleton pour cette factory.
*/
public static PouetFactory getFactory()
{
if(pouetFactory==null)
{
pouetFactory = new PouetFactory();
}
return pouetFactory;
}
private static final pouetFactory;
private PouetFactory()
{
}
}
Ici, on aura pris soin de faire 3 choses :
- fournir une méthode statique
getFactory()ougetInstance(); - mettre un constructeur private pour interdire l'instanciation de la factory par du code extérieur ;
- mettre la classe final pour empêcher sa dérivation.
Vous voyez bien ici que si j'ai envie d'utiliser autre chose que PouetImpl, personne ne le saura si j'ai bien contraint à utiliser correctement mon code (en mettant par exemple la classe PouetImpl protected si c'est possible afin d'éviter qu'elle soit instanciable par un autre objet que ceux du package). Je pourrai librement changer d'implémentation en impactant le moins de code possible.
Héritage abusif
Dans la série du code anti-évolutif, mettons au top les dérivations excessives. Qui n'a jamais dérivé une JFrame pour afficher une pauvre fenêtre qui n'a rien de plus que la JFrame d'origine ? Et bien non, vous devez déléguer dans la plupart des cas. C'est à dire encapsuler l'objet que vous utilisez et fournir une méthode getComponent() renvoyant un Component par exemple pour le fournir à l'extérieur.
On ne décide pas de faire une dérivation à la légère, ce choix de conception doit être motivé par le fait que l'objet que vous voulez est une variante de la classe dont vous souhaitez hériter. Si ce n'est pas le cas, il y a de fortes chances pour que vous ne deviez pas hériter mais plutôt déléguer.
Ça ira pour aujourd'hui
Voilà, ce sera tout pour aujourd'hui. J'ai encore d'autres trucs dont je veux parler. Il faudra que j'en trouve le temps, ce qui n'est pas gagné. J'ai envie de vous parler de DAO, de commons-logging, de JNI ... Bref, encore de quoi dire pleins de choses :-).
Si vous n'êtes pas d'accord avec certains points ou que vous souhaitez des éclaircissements, les commentaires sont là pour ça :-).


Commentaires
Bon je me lance... Tout d'abord c'est vachement bien expliqué, sisi c'est vrai...
Cependant, j'ai quelques questions :
Par ailleurs, comme ce que tu écris est très clair, est-ce que tu pourrais faire un truc sur la reflection et le chargement dynamique ?
Merci !
Je dirais que ça dépend de l'objet que tu implémentes, personnellement je ne le fais pas systématiquement. C'est peut-être d'ailleurs une erreur, j'attends les arguments pour et contre :), un blog est aussi fait pour réfléchir à plusieurs.
Disons que je le fais plutôt avec les objets fonctionnellement complexes que je prévois de faire évoluer ou d'améliorer.
Je dirais SYSTÉMATIQUEMENT lorsqu'on est dans le cas précédent : une interface publique et les implémentations censées être non utilisées ailleurs.
En résumé, de toute façon, quand on écrit du code (d'après moi), il faut toujours penser à celui qui va l'utiliser. Il faut penser bibliothèque publique. C'est pour ça que j'utilise personnellement toutes les ressources possibles du langage pour contraindre le « développeur utilisateur » de mon code à l'utiliser commme je l'ai prévu.
Par exemple, lorsque c'est possible, il peut être intéressant de mettre protected les constructeurs des classes qui ne sont pas destinées à une utilisation "publique", etc.
Quand j'écris une classe utilitaire, comme expliqué ci-dessus. Il est aberrant qu'on puisse l'instancier ou la dériver => je la mets final avec un constructeur privé. Une autre méthode couramment utilisée est de mettre une classe utilitaire abstract : mais en fait, ce n'est pas bon puisqu'on autorise sa dérivation (c'est le concept même d'une classe abstraite).
Voilà, j'espère que j'ai été clair.
Hum! ça se sent que l'on a eu les même profs de Java et de Génie Log... Alors Batmat c'est pour quand que tu enseignes à MLV ?
J'y pense, j'y pense. Mais pas à MLV, c'est trop loin de Toulouse :-). J'me verrais bien faire des tds en IUT.
J'aimerais surtout éviter que d'autres aient à subir des cours comme ceux de PC (un prof) où il nous faisait dériver la moindre classe pour l'utiliser...
Juste une petite chose à propos de tes Pouet, PouetImpl et PouetFactory... Tu aurais très bien pu faire une seule classe Pouet qui affichait « bonjour » tout simplement.
Je pense qu'il faut bien réfléchir au choix de faire une interface et une factory, 3 classes sont bien plus couteuses qu'1 seule pour une JVM. Déjà que l'on est en Java, il ne faut pas non plus faire de son PC un tracteur :D
Bien sûr, c'est très utile pour tout ce qui est classe utilitaire dont l'implémentation peut changer. Maintenant le concept objet de méthodes est bien souvent suffisant pour cacher une implémentation et les interfaces ne servent bien souvent qu'à définir plusieurs implémentations différentes d'un même « objet ». Ce concept est très souvent utilisé pour le développement de code externe (plugin).
Pour la cas des List par exemples, je ne vois pas d'autres intérêts que le regroupement de fonctionnalités dans l'AbstractList. Et qu'on ne me dise pas que ça permet de passer facilement d'un ArrayList à une LinkedList... Ce choix est normalement à faire en amont, bien avant d'attaquer le code.
Je sais que j'arrive un peu tard ! Mais je releve cette phrase sur le site de sun :
http://java.sun.com/docs/books/tutorial/collections/implementations/list.html
Donc SI mon appli a besoin de la synchronisation (genre appli J2EE) il vaut mieux utiliser Vector. Non ?
Ce que je comprend moi ce que y a une classe pour la synchronisation et l'autre non, donc en fonction de nos besoins y a qu'a choisir !
Alors :
En français dans le texte : Vector est mal conçu, donc si on l'utilise, il faut prendre grande attention à ne pas le montrer. C'est la raison pour laquelle il ne faut pas l'utiliser dans l'absolu.
OK, il est dit que c'est légèrement plus rapide :
Pour moi, à considérer que les performances sont proches, dans la majorité des développements, il vaut donc bien mieux perdre ce millième de seconde et obtenir une plus grande qualité et cohérence du code que de vouloir gagner des temps insensibles pour le client (qui à part un informaticien vous fera chier parce qu'un programme met 0.2s, même 0.5s de plus à faire que ce qu'on lui demande ?).
Dans l'absolu, je préfère largement perdre ce temps à l'exécution mais gagner en maintenance et en réactivité des équipes de développement.
Merci pour la réponse!
Effectivement, il est préférable en terme d'évolutivité d'avoir un code un chtouille moins performant mais mieux conçu.
Cependant (au risque de passer pour un casse-pied (mais c'est le but :)) ) quels critères permettent de dirent que Vector est mal conçu ? Est-ce parcequ'il a "loads of legacy operation" ? Je doit t'avouer que mes piètres compétences en anglais ont du mal a déchiffrer cette phrase ("Vector contient des méthodes héritées" ???).
Hello, merci pour ces infos.
Donc, si j'ai bien tout compris HashMap et Vector doivent être utilisé si on a besoin d'une collection synchronized, exact?
Et le fait de les utilisé sans ce besoin de synchronisme entraîne des pertes de performances, toujours exact?
Voilà, j'espère que mes questions étaieant pas trop bêtes.
Bonne continuation pour le site.
> Donc, si j'ai bien tout compris HashMap et Vector doivent être utilisé si on a besoin d'une collection synchronized, exact?
Non. Il ne faut pas utiliser Vector.
Plus précisément, il faut toujours préférer l'utilisation de l'interface Map (via l'une de ses implémentations dont la plus connu est certainement HashMap effectivement). Et si tu as besoin de la même chose synchronisée, passe simplement ta Map à la méthode Collections.synchronizedMap() pour l'enrober dans une Map thread-safe.
> Et le fait de les utilisé sans ce besoin de synchronisme entraîne des pertes de performances, toujours exact?
Oui, c'est ça.