C) Conseils pour une programation stylée en java
- D'une manière générale, suivre les conventions de codage de
Sun. Celles-ci sont disponibles à java.sun.com/docs/codeconv/index.html (le code
fourni dans ce livre respecte ces conventions du mieux que j'ai pu). Elles sont utilisées dans la
majeure partie du code à laquelle la majorité des programmeurs Java seront exposés. Si vous décidez
de vous en tenir obstinément à vos propres conventions de codage, vous rendrez la tâche plus ardue
au lecteur. Quelles que soient les conventions de codage retenues, s'assurer qu'elles sont
respectées dans tout le projet. Il existe un outil gratuit de reformatage de code Java disponible à
home.wtal.de/software-solutions/jindent/.
- Quelles que soient les conventions de codage utilisées, cela
change tout de les standardiser au sein de l'équipe (ou même mieux, au niveau de
l'entreprise). Cela devrait même aller jusqu'au point où tout le monde devrait accepter de
voir son style de codage modifié s'il ne se conforme pas aux règles de codage en vigueur. La
standardisation permet de passer moins de temps à analyser la forme du code afin de se concentrer
sur son sens.
- Suivre les règles standard de capitalisation. Capitaliser
la première lettre des noms de classe. La première lettre des variables, méthodes et des objets
(références) doit être une minuscule. Les identifiants doivent être formés de mots collés ensemble,
et la première lettre des mots intermédiaires doit être capitalisée.
Ainsi : CeciEstUnNomDeClasse,
ceciEstUnNomDeMethodeOuDeVariable.
Capitaliser toutes les lettres des identifiants déclarés comme
static final et initialisés par une valeur constante lors de leur déclaration.
Cela indique que ce sont des constantes dès la phase de compilation.
Les packages constituent un cas à part - ils doivent être
écrits en minuscules, même pour les mots intermédiaires. Les extensions de domaine (com, org, net,
edu, etc.) doivent aussi être écrits en minuscules (ceci a changé entre Java 1.1 et Java
2).
- Ne pas créer des noms « décorés » de données membres
privées. On voit souvent utiliser des préfixes constitués d'underscores et de caractères.
La notation hongroise en est le pire exemple, où on préfixe le nom de variable par son type, son
utilisation, sa localisation, etc., comme si on écrivait en assembleur ; et le compilateur ne
fournit aucune assistance supplémentaire pour cela. Ces notations sont confuses, difficiles à lire,
à mettre en oeuvre et à maintenir. Laisser les classes et les packages s'occuper de la portée des
noms.
- Suivre une « forme canonique » quand on crée une classe
pour un usage générique. Inclure les définitions pour equals(),
hashCode(), toString(), clone() (implémentation
de Cloneable), et implémenter Comparable et
Serializable.
- Utiliser les conventions de nommage « get », « set » et « is » de
JavaBeans pour les méthodes qui lisent et changent les variables private,
même si on ne pense pas réaliser un JavaBean au moment présent. Non seulement cela facilitera
l'utilisation de la classe comme un Bean, mais ce sont des noms standards pour ce genre de méthode
qui seront donc plus facilement comprises par le lecteur.
- Pour chaque classe créée, inclure une méthode static public
test() qui contienne du code testant cette classe. On n'a
pas besoin d'enlever le code de test pour utiliser la classe dans un projet, et on peut facilement
relancer les tests après chaque changement effectué dans la classe. Ce code fournit aussi des
exemples sur l'utilisation de la classe.
- Il arrive qu'on ait besoin de dériver une classe afin d'accéder à
ses données protected.Ceci peut conduire à percevoir
un besoin pour de nombreux types de base. Si on n'a pas besoin de surtyper, il suffit de dériver
une nouvelle classe pour accéder aux accès protégés, puis de faire de cette classe un objet membre
à l'intérieur des classes qui en ont besoin, plutôt que dériver à nouveau la classe de
base.
- Eviter l'utilisation de méthodes final juste pour des
questions d'efficacité.Utiliser final
seulement si le programme marche, mais pas assez rapidement, et qu'un profilage a montré que
l'invocation d'une méthode est le goulot d'étranglement.
- Si deux classes sont associées d'une certaine manière (telles que
les conteneurs et les itérateurs), essayer de faire de l'une une classe interne à l'autre.
Non seulement cela fait ressortir l'association entre les classes, mais cela permet de réutiliser
le nom de la classe à l'intérieur du même package en l'incorporant dans une autre classe. La
bibliothèque des conteneurs Java réalise cela en définissant une classe interne
Iterator à l'intérieur de chaque classe conteneur, fournissant ainsi une interface
commune aux conteneurs. Utiliser une classe interne permet aussi une implémentation
private. Le bénéfice de la classe interne est donc de cacher l'implémentation en
plus de renforcer l'association de classes et de prévenir de la pollution de l'espace de
noms.
- Quand on remarque que certaines classes sont liées entre elles,
réfléchir aux gains de codage et de maintenance réalisés si on en faisait des classes internes
l'une à l'autre. L'utilisation de classes internes ne va pas casser l'association entre
les classes, mais au contraire rendre cette liaison plus explicite et plus pratique.
- C'est pure folie que de vouloir optimiser trop
prématurément. En particulier, ne pas s'embêter à écrire (ou éviter) des méthodes natives,
rendre des méthodes final, ou stresser du code pour le rendre efficace dans les
premières phases de construction du système. Le but premier est de valider la conception, sauf si
la conception spécifie une certaine efficacité.
- Restreindre autant que faire se peut les portées afin que la
visibilité et la durée de vie des objets soient la plus faible possible. Cela réduit les
chances d'utiliser un objet dans un mauvais contexte et la possibilité d'ignorer un bug difficile à
détecter. Par exemple, supposons qu'on dispose d'un conteneur et d'une portion de code qui itère en
son sein. Si on copie ce code pour l'utiliser avec un nouveau conteneur, il se peut qu'on en arrive
à utiliser la taille de l'ancien conteneur comme borne supérieure du nouveau. Cependant, si
l'ancien conteneur est hors de portée, l'erreur sera reportée lors de la compilation.
- Utiliser les conteneurs de la bibliothèque Java standard.
Devenir compétent dans leur utilisation garantit un gain spectaculaire dans la productivité.
Préférer les ArrayList pour les séquences, les HashSet pour les
sets, les HashMap pour les tableaux associatifs, et les
LinkedList pour les piles (plutôt que les Stack) et les
queues.
- Pour qu'un programme soit robuste, chaque composant doit
l'être. Utiliser tout l'arsenal d'outils fournis par Java : contrôle d'accès, exceptions,
vérification de types, etc. dans chaque classe créée. Ainsi on peut passer sans crainte au niveau
d'abstraction suivant lorsqu'on construit le système.
- Préférer les erreurs de compilation aux erreurs
d'exécution. Essayer de gérer une erreur aussi près de son point d'origine que possible.
Mieux vaut traiter une erreur quand elle arrive que générer une exception. Capturer les exceptions
dans le gestionnaire d'exceptions le plus proche qui possède assez d'informations pour les traiter.
Faire ce qu'on peut avec l'exception au niveau courant ; si cela ne suffit pas, relancer
l'exception.
- Eviter les longues définitions de méthodes. Les méthodes
doivent être des unités brèves et fonctionnelles qui décrivent et implémentent une petite part de
l'interface d'une classe. Une méthode longue et compliquée est difficile et chère à maintenir, et
essaye probablement d'en faire trop par elle-même. Une telle méthode doit, au minimum, être
découpée en plusieurs méthodes. Cela peut aussi être le signe qu'il faudrait créer une nouvelle
classe. De plus, les petites méthodes encouragent leur réutilisation à l'intérieur de la classe
(quelquefois les méthodes sont grosses, mais elles doivent quand même ne réaliser qu'une seule
opération).
- Rester « aussi private que possible ». Une fois rendu
public un aspect de la bibliothèque (une méthode, une classe, une variable), on ne peut plus
l'enlever. Si on le fait, on prend le risque de ruiner le code existant de quelqu'un, le forçant à
le réécrire ou même à revoir sa conception. Si on ne publie que ce qu'on doit, on peut changer tout
le reste en toute impunité ; et comme la modélisation est sujette à changements, ceci est une
facilité de développement à ne pas négliger. De cette façon, les changements dans l'implémentation
auront un impact minimal sur les classes dérivées. La privatisation est spécialement importante
lorsqu'on traite du multithreading - seuls les champs private peuvent être
protégés contre un accès non synchronized.
- Utiliser les commentaires sans restrictions, et utiliser la
syntaxe de documentation de javadoc pour produire la documentation du
programme. Cependant, les commentaires doivent ajouter du sens
au code ; les commentaires qui ne font que reprendre ce que le code exprime clairement sont
ennuyeux. Notez que le niveau de détails typique des noms des classes et des méthodes de Java
réduit le besoin de commentaires.
- Eviter l'utilisation des « nombres magiques » - qui sont
des nombres codés en dur dans le code. C'est un cauchemar si on a besoin de les changer, on ne sait
jamais si « 100 » représente « la taille du tableau » ou « quelque chose dans son intégralité ». A
la place, créer une constante avec un nom explicite et utiliser cette constante dans le programme.
Cela rend le programme plus facile à comprendre et bien plus facile à maintenir.
- Quand on crée des constructeurs, réfléchir aux
exceptions. Dans le meilleur des cas, le constructeur ne fera rien qui générera une
exception. Dans le meilleur des cas suivant, la classe sera uniquement composée et dérivée de
classes robustes, et aucun nettoyage ne sera nécessaire si une exception est générée. Sinon, il
faut nettoyer les classes composées à l'intérieur d'une clause finally. Si un
constructeur échoue dans la création, l'action appropriée est de générer un exception afin que
l'appelant ne continue pas aveuglément en pensant que l'objet a été créé correctement.
- Si la classe nécessite un nettoyage lorsque le programmeur client
en a fini avec l'objet, placer la portion de code de nettoyage dans une seule méthode bien
définie - avec un nom explicite tel que cleanup() qui suggère clairement
sa fonction. De plus, utiliser un flag boolean dans la classe pour indiquer si
l'objet a été nettoyé afin que finalize() puisse vérifier la « condition de
destruction » (cf Chapitre 4).
- La seule responsabilité qui incombe à finalize() est de
vérifier la « condition de destruction » d'un objet pour le débuggage(cf Chapitre 4). Certains cas spéciaux nécessitent de libérer de la mémoire qui autrement
ne serait pas restituée par le ramasse-miettes. Comme il existe une possibilité pour que le
ramasse-miettes ne soit pas appelé pour un objet, on ne peut utiliser finalize()
pour effectuer le nettoyage nécessaire. Pour cela il faut créer sa propre méthode de nettoyage.
Dans la méthode finalize() de la classe, vérifier que l'objet a été nettoyé, et
génèrer une exception dérivée de RuntimeException si cela n'est pas le cas, afin
d'indiquer une erreur de programmation. Avant de s'appuyer sur un tel dispositif, s'assurer que
finalize() fonctionne sur le système considéré (un appel à
System.gc() peut être nécessaire pour s'assurer de ce fonctionnement).
- Si un objet doit être nettoyé (autrement que par le
ramasse-miettes) à l'intérieur d'une portée particulière, utiliser l'approche suivante :
initialiser l'objet et, en cas de succès, entrer immédiatement dans un block try
avec une clause finally qui s'occupe du nettoyage.
- Lors de la redéfinition de finalize() dans un héritage,
ne pas oublier d'appeler super.finalize()(ceci n'est
pas nécessaire si Object est la classe parente immédiate). Un appel à
super.finalize() doit être la dernière instruction de la méthode
finalize() redéfinie plutôt que la première, afin de s'assurer que les composants
de la classe de base soient toujours valides si on en a besoin.
- Lors de la création d'un conteneur d'objets de taille fixe, les
transférer dans un tableau - surtout si on retourne le conteneur depuis une méthode. De
cette manière on bénéficie de la vérification de types du tableau lors de la compilation, et le
récipiendiaire du tableau n'a pas besoin de transtyper les objets du tableau pour les utiliser.
Notez que la classe de base de la bibliothèque de conteneurs,
java.util.Collection, possède deux méthodes toArray() pour
accomplir ceci.
- Préférer les interfaces aux classes
abstract.Si on sait que quelque chose va être une
classe de base, il faut en faire une interface, et ne la changer en classe
abstract que si on est obligé d'y inclure des définitions de méthodes et des
variables membres. Une interface parle de ce que le client veut faire, tandis
qu'une classe a tendance à se focaliser sur (ou autorise) les détails de
l'implémentation.
- A l'intérieur des constructeurs, ne faire que ce qui est
nécessaire pour mettre l'objet dans un état stable. Eviter autant que faire se peut
l'appel à d'autres méthodes (les méthodes final exceptées), car ces méthodes
peuvent être redéfinies par quelqu'un d'autre et produire des résultats inattendus durant la
construction (se référer au chapitre 7 pour plus de détails). Des constructeurs plus petits et plus
simples ont moins de chances de générer des exceptions ou de causer des problèmes.
- Afin d'éviter une expérience hautement frustrante, s'assurer qu'il
n'existe qu'une classe non packagée de chaque nom dans tout le classpath. Autrement, le
compilateur peut trouver l'autre classe de même nom d'abord, et renvoyer des messages d'erreur qui
n'ont aucun sens. Si un problème de classpath est suspecté, rechercher les fichiers
.class avec le même nom à partir de chacun des points de départ spécifiés dans le
classpath, l'idéal étant de mettre toutes les classes dans des packages.
- Surveiller les surcharges accidentelles. Si on essaye de
redéfinir une méthode de la classe de base et qu'on se trompe dans l'orthographe de la méthode, on
se retrouve avec une nouvelle méthode au lieu d'une méthode existante redéfinie. Cependant, ceci
est parfaitement légal, et donc ni le compilateur ni le système d'exécution ne signaleront d'erreur
- le code ne fonctionnera pas correctement, c'est tout.
- Ne pas optimiser le code trop prématurément. D'abord le
faire marcher, ensuite l'optimiser - mais seulement si on le doit, et seulement s'il est prouvé
qu'il existe un goulot d'étranglement dans cette portion précise du code. A moins d'avoir utilisé
un profileur pour découvrir de tels goulots d'étranglement, vous allez probablement perdre votre
temps pour rien. De plus, à force de triturer le code pour le rendre plus rapide, il devient moins
compréhensible et maintenable, ce qui constitue le coût caché de la recherche de la performance à
tout prix.
- Se rappeler que le code est lu bien plus souvent qu'il n'est
écrit. Une modélisation claire permet de créer des programmes faciles à comprendre, mais
les commentaires, des explications détaillées et des exemples sont inestimables. Ils vous seront
utiles autant qu'aux personnes qui viendront après vous. Et si cela ne vous suffit pas, la
frustration ressentie lorsqu'on tente de dénicher une information utile depuis la documentation
online de Java devrait vous convaincre.
[85]Qui m'a été expliquée
par Andrew Koenig.