P
'
t
i
t
e
C
h
a
t
t
e
 
spacer~ THERE'S NO PLACE LIKE 127.0.0.1 Articles | Connexion
 
~GRAIN DE SEL

 Composition et héritage
12/04/2006 - 13:01

L'héritage est une des grandes qualités de la programmation orientée objet, ainsi qu'une de ses caractéristiques majeures. Puisque le titre de ce billet ne devrait normalement pas attirer des énergumènes quelconque, je vais partir du principe que vous connaissez les bases de la programmation orientée objet et je ne vous ferai donc pas l'insulte de faire un rappel sur l'héritage. À propos, bien que ce billet propose des exemples en Java, ses analyses et conclusions sont valables avec n'importe quel langage orienté objet.

L'héritage de classes est bien souvent mal employé par les développeurs qui lui donnent une signification plus large que nécessaire. L'héritage définit normalement une relation sémantique entre deux classes. Par exemple, un bouton est un composant graphique. Malheureusement, à cette relation sémantique est souvent substituée une relation fonctionnelle. On n'hérite plus alors d'une autre classe pour marquer des qualités intrinsèques pour mais pour hériter de fonctionnalités qui évitent au programmeur de devoir les implémenter à nouveau. En résumé, sous prétexte de favoriser la réutilisation du code, la développeur se rend coupable d'une grave erreur de conception qui peut avoir des conséquences assez importantes dans des API publiques.

L'API de la plate-forme Java SE contient plusieurs exemples flagrants d'héritage fonctionnel qui n'auraient jamais dû exister. Comme tous les mauvais exemples de l'API Java que j'utilise dans ces articles, celui-ci date de Java 1.x, lorsque de nombreux concepts n'étaient pas encore maîtrisés. Intéressons-nous donc a la classe java.util.Stack censée représenter une pile LIFO. Avant l'apparition de J2SE 1.2, il n'existait pas d'API uniformisée pour les collections; les interfaces List, Map et Collection ne sont apparues que plus tard. Ainsi, pour réutiliser le code existant, la classe Stack a été implémentée en dérivant java.util.Vector. Cette erreur de conception pourrait être pardonnée, bien qu'un tas ne soit pas un vecteur on peut facilement se convaincre du contraire, si elle n'avait pas introduit d'importants problèmes fonctionnels. L'API de la classe Stack ne présente en soi aucun problème. Elle expose en revanche toute l'API de Vector. Il est donc possible de manipuler le tas comme un vecteur et de réaliser des opérations inattendues qui peuvent altérer le fonctionnement de votre programme :

Stack pile = new Stack();
pile.push("Bas de la pile");
pile.push("Haut de la pile");
pile.insertElementAt("Perdu", 0);
while (!pile.empty()) {
  System.out.println(pile.pop());
}

Cet exemple viole allègrement le contrat établi par la documentation de Stack qui stipule que the Stack class represents a last-in-first-out (LIFO) stack of objects. Prenons maintenant un autre exemple, une classe intitulée LoggedList qui enregistre dans un journal toutes les ajouts réalisés sur une liste :

public class LoggedList extends ArrayList {
  private static final Logger LOG = Logger.getLogger("logged lists");

  public void add(E element) {
    LOG.info("Added element: " + e.toString());
    super.add(e);
  }

  public void addAll(Collection c) {
    for (E e : c) {
      LOG.info("Added element: " + e.toString());
    }
    super.addAll(c);
  }
}

Cette implémentation naïve semble parfaitement légitime à première vue. Trois graves problèmes existent néanmoins, une erreur de sémantique, une limitation fonctionnelle et un bug. Cela fait beaucoup pour quelques lignes de code. Le premier problème est relativement simple : une liste journalisée n'est pas une ArrayList. Le lien sémantique introduit par le programmeur est complètement artificiel mais surtout, faux. De cette première erreur découle naturellement la deuxième, notre liste est limitée à l'implémentation fournie par la classe ArrayList. Que faire si une liste chaînée est mieux appropriée mais que je désire la journaliser ? Enfin, cette classe enregistre dans le journal tous les ajouts.. en double lorsque la méthode addAll() est invoquée. Ce comportement est dû à un détail de l'implémentation d'ArrayListqui appelle add() dans addAll(). Vous ne pouvez pas sérieusement prendre en compte ce fait pour votre propre implémentation. Si vous corrigez le bug en retirant la journalisation d'addAll(), que se passera-t'il si la prochaine version de l'API change l'implémentation ? Vous perdrez alors la journalisation d'addAll().

Il est donc très important de prendre le temps de réfléchir à la signification de la relation que vous rédigez chaque fois que vous héritez une classe. La composition est une solution parfaite dans la plupart des situations. En utilisant la composition vous définissez une relation possède un. Cette technique, qui est aussi une des notions de base de la programmation orientée objet, est souvent associée aux interfaces. La composition se retrouve en outre dans plusieurs design patterns comme le composite, le delegate ou le decorator. L'exemple de composition que je vais utiliser est tiré du projet Fuse. Il s'agit de la classe FallbackMap qui délègue ses appels à une Map par défaut s'ils échouent dans la Map courante. La solution évidente pour de nombreux programmeurs pour réaliser une telle classe serait d'hériter d'HashMap pour bénéficier de toute son implémentation. En se contenant d'implémenter java.util.Map et en favorisant la composition le résultat est beaucoup plus robuste :

class FallbackMap<K, V> implements Map<K, V> {
	private final Map<K, V> fallback;
	private final Map<K, V> peer;
	
	FallbackMap(Map<K, V> fallback) {
		this.fallback = fallback;
		
		peer = new HashMap<K, V>();
	}
	
	public int size() {
		return peer.size() + fallback.size();
	}

	public boolean isEmpty() {
		return !(peer.isEmpty() && fallback.isEmpty());
	}

	public boolean containsKey(Object key) {
		return (peer.containsKey(key) ||
                  fallback.containsKey(key));
	}

	public boolean containsValue(Object value) {
		return (peer.containsValue(value) ||
                  fallback.containsValue(value));
	}

	public V get(Object key) {
		if (peer.containsKey(key)) return peer.get(key);
		
		return fallback.get(key);
	}

	public V put(K key, V value) {
		return peer.put(key, value);
	}

	public V remove(Object key) {
		return null;
	}

	public void putAll(Map<? extends K, ? extends V> t) {
		peer.putAll(t);
	}

	public void clear() {
		peer.clear();
	}

	public Set<K> keySet() {
		Set<K> keySet = new HashSet<K>();
		
		keySet.addAll(peer.keySet());
		keySet.addAll(fallback.keySet());
		
		return keySet;
	}

	public Collection<V> values() {
		Collection<V> values = new ArrayList<V>();
		
		values.addAll(peer.values());
		values.addAll(fallback.values());
		
		return values;
	}

	public Set<Entry<K, V>> entrySet() {
		Set<Entry<K, V>> entrySet = new HashSet<Entry<K, V>>();
		
		entrySet.addAll(peer.entrySet());
		entrySet.addAll(fallback.entrySet());
		
		return entrySet;
	}
}

Malgré sa longueur cette classe ne demande que très peu de travail. La grande majorité de son code a en outre été généré par un IDE (cherchez dans le menu Code, Source ou Refactoring une fonction appelée Generate Delegate ou équivalent). La même classe implémentée par héritage pourrait laisser la possibilité aux clients de ne pas accéder la map par défaut en invoquant des méthodes de la classe parente, non redéfinie dans FallbackMap.

Il existe de nombreuses situations dans lesquelles vous pourrez écrire une classe par héritage qui ne présente aucun problème fonctionnel (même si vous ne pourrez corriger la sémantique). J'insiste beaucoup sur le fait que vous devez néanmoins vous concentrer sur la composition car une API peut évoluer. Il suffit que l'auteur de la classe que vous dérivez rajoute une méthode et que vous ne mettiez pas à jour votre version pour introduire un bug potentiel dans votre code source. L'utilisation de l'héritage favorise en outre beaucoup trop une relation à une implémentation spécifique, qui présente les mêmes dangers en cas d'évolution de la classe parente.

J'espère que vous regarderez dorénavant vos définitions de classes d'un oeil soupçonneux :)



 Romain GUY (Gfx)

 mercredi 12 avril 2006 @ 16:43
  
:love:

Tu viens en un coup de cuillière à pot de me montrer l'interet qu'il y a à se servir *réellement* des interfaces. J'avoue, jusqu'ici par fainéantise, je faisais un bon gros héritage de grand-mère et basta...
Wizmaster 
Gravatar Image
 mercredi 12 avril 2006 @ 22:03
  
C'est le retour de Login: tous ces articles... j'aime.

@+
thev 
Gravatar Image
 vendredi 14 avril 2006 @ 00:03
  
Stoi qu'on regarde d'un oeil soupçonneux !

Ceci dit, j'm ossi les articles :)
F8Full 
Gravatar Image
 mardi 25 avril 2006 @ 13:21
  
Excellent, très bon.
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