advanced.java
← Tous les concepts
Conception des classes · ch. 3, p. 25

Héritage vs composition

L'héritage couple une sous-classe aux détails d'implémentation de son parent, si bien qu'un changement du parent peut silencieusement la casser. La composition délègue à une instance détenue et ne dépend que du contrat publié.

La démonstration classique :

class CountingSet<E> extends HashSet<E> {
    private int added = 0;

    @Override public boolean add(E e) { added++; return super.add(e); }

    @Override public boolean addAll(Collection<? extends E> c) {
        added += c.size();
        return super.addAll(c);          // qui appelle elle-même add()
    }
}

HashSet.addAll se trouve implémentée en termes de add, donc chaque élément est compté deux fois. Rien dans le contrat documenté ne le disait, et une future version pourrait changer cela dans un sens ou dans l’autre.

La version composée ne peut pas casser :

class CountingSet<E> {
    private final Set<E> delegate = new HashSet<>();
    private int added = 0;

    public boolean add(E e) { added++; return delegate.add(e); }
}

Quand l’héritage est justifié

Seulement pour une véritable relation « est-un » où le supertype a été conçu pour l’extension — ce qui signifie qu’il documente ses propres motifs d’auto-utilisation, ou qu’il est abstract avec des méthodes-crochets clairement désignées. Tout le reste devrait être final.

2 questions sur ce concept S'entraîner