Constructeurs, paramètres nommés et fabriques

Romain Guy (Gfx)

- Titre : Constructeurs, paramètres nommés et fabriques

- Auteur : Romain Guy (Gfx)

- Parution : 10 août 2006

- Licence : Cette création est mise à disposition sous un contrat Creative Commons (Paternité - Partage des Conditions Initiales à l'Identique).

Sommaire

♪ 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 :

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 :

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 :

// 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 :

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

Voici comment nous invoquerions cette méthode :

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 :

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() :

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 :

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

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

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

La solution est relativement simple à définir :

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 :

// 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 efficaces 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.