Héritage de classes à classes

Héritage de classes à classes #

L’héritage de classes à classes, que j’appelle également l’héritage structurel, est possible, mais fortement déconseillé. Il est parfois nécessaire de le faire lorsque des API tierces nous obligent à le faire. Evitez de faire un design qui oblige une classe à hériter d’une autre classe ou qui l’autorise involontairement.

Nous pouvons identifier deux types de dépendance entre des objets. Soit deux types A et B :

  • la composition, qui est une dépendance faible, signifie qu’un type A est composé d’un type B. Dit autrement : A possède un B.
  • l’héritage, qui est une dépendance forte, signifie qu’un type A hérite d’un type B. Dit autrement : A est un B.

Dans le premier cas, l’encapsulation peut facilement être préservée. Le type A peut utiliser un objet du type B de manière transparente à l’utilisateur. B utilise généralement un faible sous-ensemble de fonctionnalités de A. Donc B a une moindre probabilité d’être modifié si A l’est également.

Dans le deuxième cas, l’encapsulation n’est pas préservée. Nous exposons la dépendance lorsque nous écrivons A extends B. De plus, B expose toutes les fonctionnalités publiques de A. La dépendance est beaucoup plus forte et B a une probabilité plus élevée de devoir s’adapter si A change.

Exemple d’un échiquier #

Prenons comme exemple un jeu d’échecs. Nous souhaitons modéliser un échiquier avec un ensemble de pièces. Nous aimerions déplacer une pièce sur une case, connaître au préalable si le déplacement est légal et retirer une pièce de l’échiquier.

La version qui utilise l’héritage est une mauvaise idée :

1
2
3
4
5
6
7
8
9
class Chessboard extends ArrayList<Piece> {
    public void move(Piece piece, Position pos) {...}
    public boolean isLegalmove(Piece piece, Position pos) {...}
    public void remove(Piece piece) { 
        ...
        super.remove(piece);
        ...
    }
}

Toutes les fonctionnalités d’une liste sont exposées pour un échiquier. Il est possible d’ajouter une pièce, de trier un échiquier ou encore de récupérer un sous-ensemble de pièces entre deux indices.

Dans notre conception, un échiquier n’est pas un ensemble de pièces. Un échiquier possède un ensemble de pièces. Cette nuance est importante et nous amène à la composition :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Chessboard {
    private ArrayList<Piece> pieces = ...;

    public void move(Piece piece, Position pos) {...}
    public boolean isLegalmove(Piece piece, Position pos) {...}
    public void remove(Piece piece) { 
        ...
        pieces.remove(piece);
        ...
    }
}

Le couplage devient faible. Le choix de la collection est caché par l’encapsulation et devient invisible. Si de nouvelles fonctionnalités sont ajoutées à une liste ou si elles sont modifiées, elles ont peu de chances d’impacter notre échiquier.

L’héritage de classes à classes est la pire des dépendances. Ne l’utilisez pas pour éviter la duplication de code. Il décrit les fonctionnalités d’une classe à l’aide d’une autre classe. Utilisez l’héritage comportemental à la place en privilégiant l’emploi d’interfaces. Celui-ci décrit lorsqu’un objet peut être utilisé à la place d’un autre objet (d’où les sections concernant l’extensibilité et l’abstraction).

Etude de cas existant #

Regardons la classe Properties (javadoc ) qui est un bon cas d’école. Elle existe depuis Java 1 et a été très mal conçue à cause de son héritage de la classe Hashtable. Ceci s’est remarqué avec l’évolution du langage.

Cette classe a une bonne volonté : celle de manipuler des propriétés récupérées d’un fichier de configuration par exemple. Pour une clé correspond une valeur, par exemple : server_address: 192.168.2.3, server_address est la clé et 192.168.2.3 est la valeur associée. La clé est la valeur doivent être des chaînes de caractères :

The Properties class represents a persistent set of properties.
Each key and its corresponding value in the property list is a string. 

Etant donné que le comportement ressemble à celui d’un tableau associatif, le concepteur à décider d’hériter de Hashtable.

// version antérieure à Java 5
class Properties extends Hashtable {...}

Le Hashtable n’était pas générique et l’insertion d’un couple clé/valeur ou la récupération d’une valeur à l’aide d’une clé étaient de signature : Object put(Object key, Object value), respectivement Object get(Object key). Avant les génériques, les collections n’acceptaient que des Objects. Lorsque Java 5 a apporté les génériques, la version a évolué et pour rester compatible, au cas où des utilisateurs auraient mis autre chose que des chaines de caractères, la définition est devenue:

// depuis Java 5
class Properties extends Hashtable<Object, Object> {...}

Et les méthodes put et get sont toujours a même.

Pour éviter que l’utilisateur mette des valeurs autres que des chaînes de caractères, la documentation officielle nous met en garde en préconisant de ne pas les utiliser:

Because Properties inherits from Hashtable, the put and putAll methods can be applied to a Properties object. 
Their use is strongly discouraged as they allow the caller to insert entries whose keys or values are not Strings

Et propose une méthode qui accepte les chaînes de caractères uniquement.

The setProperty method should be used instead

Le mal est fait. Puisque Properties expose des fonctionnalités non adaptées, de nouvelles ont été ajoutées en préconisant de ne pas utiliser les premières !

Mais ce n’est pas tout. Si nous regardons de plus près, Properties hérite de Hashtable et Hashtable hérite de Dictionary. En lisant la documentation de cette dernière, nous remarquons qu’elle est obsolète !

NOTE: This class is obsolete. New implementations should implement the Map interface, rather than extending this class

Malheureusement, les objets de la classe Properties sont utilisés dans beaucoup de librairies et frameworks. Toute fonction qui prend en argument un Dictionary peut recevoir une Properties. Il est impossible de briser cet héritage tout en restant compatible.

Imaginons quel aurait dû être l’évolution de cette classe.

  • Avant les génériques
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Properties {
    private Hashtable props = ...;
    public add(String key, String value) {
        props.put(key, value);
    }
    public String get(String key) {
        // Nous avons le contrôle exclusif de props. Nous savons
        // qu'il contient uniquement des Strings
        return (String)props.get(key);
    }
}
  • Après les génériques
1
2
3
4
5
6
7
8
9
class Properties {
    private Hashtable<String, String> props = ...;
    public add(String key, String value) {
        props.put(key, value);
    }
    public String get(String key) {
        return props.get(key);
    }
}
  • Après l’obsolescence de Dictionary
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Properties {
    private Map<String, String> props = ...; /* Changement de structure de 
                                                données. Grâce à l'encapsulation, 
                                                les utilisateurs ne sont pas 
                                                impactés par ce changement */
    public add(String key, String value) {
        props.put(key, value);
    }
    public String get(String key) {
        return props.get(key);
    }
}

Les avantages auraient été multiples. Même si props était peuplé d'Objects, nous ne mettons à disposition de l’utilisateur que des méthodes qui respectent un état valide. De plus, le choix de notre structure de données pour props n’est pas exposée. Seule Properties aurait vu son code modifié sans impacter l’utilisateur.

Une bonne pratique #

Tout livre qui traite de bonnes pratiques ou de patrons de conception vous l’exposera :

Conseil

Préférez la composition à l’héritage

Un autre conseil d’un éminent contributeur de Java :

Conseil

Design and document for inheritance or else prohibit it (Josh Bloch, Effective Java)

Nous retiendrons cette dernière citation pour conclure et introduire une autre utilisation du mot clé final. Il permet d’interdire à une méthode ou une classe de pouvoir être redéfinie, respectivement héritée :

  • public final youCannotOverrideThisMethod() {...}
  • public final class CannotOverrideThisClass {...}

C’est le cas des classes Integer et String pour ne prendre que ces deux exemples. Il est impossible d’hériter de ces classes.












comments powered by Disqus