P
'
t
i
t
e
C
h
a
t
t
e
 
spacer~ IF YOU KNEW WHAT YOU WERE DOING YOU'D PROBABLY BE BORED Articles | Connexion
 
~GRAIN DE SEL

 Constructeurs, paramètres nommés et fabriques
18/04/2006 - 09:13

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 quelle 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("<damn />");

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

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 de 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 a 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.



 Romain GUY (Gfx)

 mardi 18 avril 2006 @ 10:25
  
paramètres nommés pour le c++ :

http://www.boost.org/libs/parameter/doc/html/index.html
BenO 
Gravatar Image
 mardi 18 avril 2006 @ 10:27
  
Première règle du programming club, on ne parle pas du C++ :)
Gfx 
Gravatar Image
 mardi 18 avril 2006 @ 10:37
  
ah merde xD
désolé :s je recommencerais plus.
BenO 
Gravatar Image
 mardi 18 avril 2006 @ 15:23
  
Le C++, tout le monde sait que ça existe, tout le monde en tate un peu en se cachant bien, mais personne ne veut en parler.

En gros, c'est du pr0n pour geek...

:>
Wizmaster 
Gravatar Image
 mardi 18 avril 2006 @ 15:56
  
et en C# ?
zloug 
Gravatar Image
 vendredi 21 avril 2006 @ 10:55
  
Va falloir que je remette en ligne le site du Comité Anti-C++ ^^
RICO 
Gravatar Image
 vendredi 21 avril 2006 @ 17:52
  
Tu veux surement dire le "omité Anti-++"

une autre news peut etre ?


...
Loone 
Gravatar Image
 vendredi 21 avril 2006 @ 20:30
  
Loone > Ah non , pas anti C ! C++ ok, pas on ne touche pas au C. Ça donne donc : "Comité Anti-" :)
Belegar 
Gravatar Image
 jeudi 27 avril 2006 @ 15:19
  
Pour en revenir au sujet initiale, voici un autre exemple. On pourrait facilement l'améliorer en ajoutant des méthodes appendRaisedBevelBorder, appendBevelBorder, appendEtchedBorder, ...


public class MyCompoundBorder implements Border {
private Border border;

public MyCompoundBorder(Border border)
{
this.border = border;
}

public Border appendBorder(Border border)
{
this.border = BorderFactory.createCompoundBorder(this.border, border);
return this;
}

public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
border.paintBorder(c, g, x, y, width, height);
}

public Insets getBorderInsets(Component c) {
return border.getBorderInsets(c);
}

public boolean isBorderOpaque() {
return border.isBorderOpaque();
}
}


Loyl 
Gravatar Image
 jeudi 27 avril 2006 @ 19:38
  
Pas forcément car je ne voudrais pas que mon ombre portée soit mélangée à une autre bordure. Le MyCompoundBorder est une bonne idée mais ajouter des append*() dans le DropShadowBorder, je ne trouve pas.
Gfx 
Gravatar Image
 jeudi 27 avril 2006 @ 21:35
  
Mon exemple est totalement indépendant, je ne pensait pas ajouter des append*() à la classe DropShadowBorder.
Je voulais juste proposer une méthode plus propre que le BorderFactory.createCompoundBorder(BorderFactory.createEmptyBorder(10,10,10,10), BorderFactory.createEtchedBorder()) en me basant sur ce que tu expliques ci-dessus.
Loyl 
Gravatar Image

 Ajoutez votre grain de sel 
 
Surnom :
E-mail :
Message :     B     I     U     CODE     QUOTE     IMAGE     CD CASE     LINK 
 
Un gâteau ?oui    non 
RisoliVillard ?oui    non 
Port de RisoliVillard :
     


 Aide
RisoliVillard est un plugin Winamp 2/5, iTunes et un outil pour XMMS qui vous permettra d'afficher la chanson que vous écoutez au moment de l'écriture de votre réponse. Le port utilisé par votre plugin doit être reproduit dans le champ ci-dessus (8462 par défaut).
Utilisation de vBCode :
- [B]gras[/B]
- [I]italique[/I]
- [U]souligné[/U]
- [QUOTE]citation[/QUOTE]
- [CODE]code[/CODE]
- [IMG]http://www.serveur.com/image.jpg[/IMG]
- [URL=http://www.serveur.com/]texte à afficher[/URL]

 
#ProgX©2005 Mathieu GINOD - Romain GUY - Erik LOUISE