Constructeurs, paramètres nommés et fabriques

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

Choisir un langage de programmation est toujours une tâche difficile qui nécessite bien souvent de bien connaître les différentes options à notre disposition. Certaines particularités d'un langage peuvent parfois influencer votre décision en sa faveur, malgré l'absence de certaines fonctionnalités d'un autre que vous regretterez peut-être plus tard. Les paramètres optionnels et nommés sont une des fonctionnalités du langage Python que j'affectionne particulièrement. Présents dans beaucoup d'autres langages, parfois sous une forme un peu particulière comme nous le verrons ensuite, ces paramètres peuvent recevoir une valeur par défaut et limitent le nombre de constructeurs à écrire pour chaque classe. Imaginons une classe DropShadowImage :

 
Sélectionnez

class DropShadowImage {
  private final Color color;
  private final int angle;
  private final float opacity;

  DropShadowImage() {
    this(Color.BLACK, 60, 0.5f);
  }

  DropShadowImage(Color color) {
    this(color, 60, 0.5f);
  }

  DropShadowImage(Color color, int angle) {
    this(color, angle, 0.5f);
  }

  DropShadowImage(Color color, int angle, float opacity) {
    this.color = color;
    this.angle = angle;
    this.opacity = opacity;
  }
}

Écrire toutes les variations possibles des paramètres peut être très rapidement fastidieux et, dans bien des cas, tout simplement impossible sans obtenir une API et une documentation qui rendent fou le premier lecteur. Pour pallier ce problème, Python utilise les paramètres nommés :

 
Sélectionnez

class DropShadow Image:
  def __init__(self, color=Color.BLACK, angle=60, opacity=0.5):
    self.color = color
    self.angle = angle
    self.opacity = opacity

Ces quelques lignes sont non seulement plus rapides à écrire que la version Java mais offrent en outre toutes les combinaisons possibles des paramètres :

 
Sélectionnez

// version Java d'une ombre noire, avec angle de 60 degrés
// et une opacité de 70%
DropShadowImage shadow = new DropShadowImage(Color.BLACK, 60, 0.7f);

# version Python
shadow = DropShadowImage(opacity=0.7)

Diable ! Je préfère nettement la solution Python qui est en outre plus lisible car les paramètres nommés donnent des informations supplémentaires sur les valeurs. Nous savons par exemple ici que le chiffre indiqué correspond à l'opacité et non à l'angle. Les paramètres nommés nous permettent également de changer leur ordre.

Objective-C ne propose pas de paramètres optionnels ni nommés mais offre une approche intéressante dont nous allons nous inspirer pour contourner le problème posé par Java. L'exemple suivant présente une méthode pour créer une ellipse en Objective-C :

 
Sélectionnez

+ (id)ellipseByLocation:(Location*)location
  withWidth:(int)width withHeight:(int)height

Voici comment nous invoquerions cette méthode :

 
Sélectionnez

MyEllipse* ellipse = [MyEllipse ellipseByLocation:
  [Location locationByX: 40 withY: 40] withWidth: 20 withHeight: 20]

En oubliant un instant le babillage d'Objective-C par rapport au taciturne Python, je vous demande de considérer cette ligne et de la comparer avec son équivalent Java :

 
Sélectionnez

MyEllipse ellipse = new MyEllipse(new Location(40, 40), 20, 20);

La version Java semble plus "propre" mais se révèle bien plus difficile à lire si on ne connaît pas, ou peu, la classe MyEllipse. Avoir une documentation bien fournie à portée de main est indispensable. Fort heureusement il existe une solution pour parvenir à un résultat semblable en Java. Celle-ci repose sur l'utilisation de fabriques, ou méthodes statiques et publiques retournant une instance de leur classe, et sur le chaînage des méthodes. Certaines classes proposent déjà cela dans le JDK. Vous êtes peut-être familier de StringBuffer qui vous permet de chaîner les appels à append() :

 
Sélectionnez

StringBuffer buffer = new StringBuffer();
buffer.append("Once").append(' ').append("upon").append(" a time.");

Je vous propose donc de retranscrire l'exemple Objective-C suivant en Java :

 
Sélectionnez

+ (id)stringWithContentsOfFile:(NSString *)path
  encoding:(NSStringEncoding)enc error:(NSError **)error

L'API finale doit permettre d'exécuter le code suivant :

 
Sélectionnez

NSString s = NSString.stringWithContentsOfFile("blast.txt").
  encoding("ISO-8859-1").error(errorContainer);

La solution est relativement simple à définir :

 
Sélectionnez

class NSString {
  public static NSString stringWithContentsOfFile(String path) {
    return new NSSString().loadFromFile(path);
  }

  public NSString encoding(String encoding) {
    setEncoding(encoding);
    return this;
  }

  public NSString error(NSError errorContainer) {
    setErrorContainer(errorContainer);
    return this;
  }
}

En permettant de chaîner les appels nous obtenons un "constructeur" bien plus loquace. Cette solution permet en outre de n'appeler que les paramètres que vous désirez, nous obtenons donc les paramètres nommés et optionnels de Python, et d'en changer l'ordre. Bien qu'apparemment parfaite, cette solution recèle quelques problèmes. Il est par exemple difficile de savoir quelles méthodes font partie de la chaîne de construction. Vous devez donc rigoureusement documenter votre classe. Cette technique pêche également par son efficacité : vous devez impérativement initialiser tous les champs dans la fabrique, ici stringWithContentsOfFile(), sous peine de laisser à l'utilisateur une instance partiellement initialisée. Cela signifie donc que vous affecterez probablement la plupart des paramètres deux fois. Les performances ne devraient pas en souffrir mais vous devez connaître ce problème.

Malgré ces contrariétés, les fabriques dévoilent ici un de leurs avantages par rapport aux constructeurs : elles fournissent bien plus d'informations. Lisez par exemple les fragments de code suivants et décidez lesquels vous préférez :

 
Sélectionnez

// Fragment de constructeurs
new XmlDocument(in);
new XmlDocument("dom.xml");
new XmlDocument("");

// Fragments de fabriques
XmlDocument.loadFromInputStream(in);
XmlDocument.loadFromFile("dom.xml");
XmlDocument.loadFromXmlString("");

Les fabriques permettent en outre de modifier très aisément l'implémentation sous-jacente sans modifier l'API publique. Dans ce petit exemple, nous pourrions par exemple renvoyer des instances d'AsynchronousXmlDocument, qui effectueraient le travail de parsing dans un thread, sans que l'utilisateur ne le sache. Avec les constructeurs, nous devons impérativement retourner une instance de la classe courante. Figeant ainsi l'API.

Le seul défaut des fabriques est dû à la javadoc. Cette dernière propose une section spéciale pour les constructeurs mais mélange les fabriques avec les autres méthodes. Certaines documentations, comme celle d'Apple, permettent de classer les méthodes suivant des catégories. Ce système rend encore plus efficace les fabriques. En Java, point de salut si ce n'est une bonne documentation et un respect des standards de fait. Essayez par exemple de préfixer vos fabriques par valueOf, load ou new. Sachez tout de même que cette limitation de la javadoc, et bien d'autres, sont en cours de correction grâce à la JSR 260.

En conclusion, les fabriques permettent non seulement d'améliorer la lisibilité de vos programmes, mais également de remplacer une implémentation sans rompre le contrat établi dans vos API exportées. L'ajout de méthodes chaînées permet en outre de simuler les paramètres optionnels et nommés en Java. Cette technique doit néanmoins être employée à bon escient car tout abus peut se révéler dangereux pour la stabilité et la cohésion de vos programmes.

Articles et tutoriels Java
L'essentiel de Java en une heure
L'API java.nio du JDK 1.4
Inversion de contrôle en Java
L'introspection
Le Java Community Process
Conception de tests unitaires avec JUnit
Les Strings se déchaînent
Présentation de SWT
La programmation réseau en Java avec les sockets
Du bon usage de l'héritage et de la composition
Les références et la gestion de la mémoire en Java
Constructeurs et méthodes exportées en Java
Les membres statiques, finaux et non immuables en Java
Les classes et objets immuables en Java
Comprendre et optimiser le Garbage Collector
Les principes de la programmation d'une interface graphique
Les opérateurs binaires en Java
Prenez le contrôle du bureau avec JDIC
Les Java Data Objects (JDO version 1.0.1)
La persistance des données avec Hibernate 2.1.8
Journalisation avec l'API Log4j
Java 5.0 et les types paramétrés
Les annotations de Java 5
Java 1.5 et les types paramétrés
Créer un moteur de recherche avec Lucene
Articles et tutoriels Swing
Threads et performance avec Swing
Rechercher avec style en utilisant Swing
Splash Screen avec Swing et Java3D
Drag & Drop avec style en utilisant Swing
Attendre avec style en utilisant Swing
Mixer Java3D et Swing
Articles et tutoriels Java Web
Redécouvrez le web avec Wicket
Cette création est mise à disposition sous un contrat Creative Commons (Paternité - Partage des Conditions Initiales à l'Identique).