Bonnes pratiques de programmation en Java
Mon, Jun 28, 2004Aujourd’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 :-).