Héritage et composition (relation Is-a vs Has-a) en Java

Héritage et composition (relation Is-a vs Has-a) en Java

1. Vue d'ensemble

Inheritance et la composition - ainsi que l'abstraction, l'encapsulation et le polymorphisme - sont les pierres angulaires deobject-oriented programming (POO).

Dans ce didacticiel, nous aborderons les bases de l'héritage et de la composition, et nous nous concentrerons fortement sur l'identification des différences entre les deux types de relations.

2. Principes de base de l'héritage

L'héritage est un mécanisme puissant mais surutilisé et mal utilisé.

En termes simples, avec héritage, une classe de base (a.k.a. type de base) définit l’état et le comportement commun à un type donné et permet aux sous-classes (a.k.a. sous-types) fournissent des versions spécialisées de cet état et de ce comportement.

Pour avoir une idée claire sur la façon de travailler avec l'héritage, créons un exemple naïf: une classe de basePerson qui définit les champs et méthodes communs pour une personne, tandis que les sous-classesWaitress etActress fournissent des implémentations de méthodes supplémentaires et détaillées.

Voici la classePerson:

public class Person {
    private final String name;

    // other fields, standard constructors, getters
}

Et ce sont les sous-classes:

public class Waitress extends Person {

    public String serveStarter(String starter) {
        return "Serving a " + starter;
    }

    // additional methods/constructors
}
public class Actress extends Person {

    public String readScript(String movie) {
        return "Reading the script of " + movie;
    }

    // additional methods/constructors
}

De plus, créons un test unitaire pour vérifier que les instances des classesWaitress etActress sont également des instances dePerson, montrant ainsi que la condition «is-a» est remplie à la niveau de type:

@Test
public void givenWaitressInstance_whenCheckedType_thenIsInstanceOfPerson() {
    assertThat(new Waitress("Mary", "[email protected]", 22))
      .isInstanceOf(Person.class);
}

@Test
public void givenActressInstance_whenCheckedType_thenIsInstanceOfPerson() {
    assertThat(new Actress("Susan", "[email protected]", 30))
      .isInstanceOf(Person.class);
}

It’s important to stress here the semantic facet of inheritance. Outre la réutilisation de l'implémentation desPerson class,we’ve created a well-defined “is-a” relationship entre le type de basePerson et les sous-typesWaitress etActress. Les serveuses et les actrices sont, en réalité, des personnes.

Cela peut nous amener à demander:in which use cases is inheritance the right approach to take?

Si les sous-types remplissent la condition «est-un» et fournissent principalement des fonctionnalités supplémentaires plus bas dans la hiérarchie des classes, alors l'héritage est la voie à suivre.

Bien sûr, le remplacement de méthode est autorisé tant que les méthodes remplacées conservent la substituabilité type / sous-type de base promue par lesLiskov Substitution Principle.

De plus, nous devons garder à l'esprit quethe subtypes inherit the base type’s API, ce qui est dans certains cas, peut être excessif ou simplement indésirable.

Sinon, nous devrions utiliser la composition à la place.

3. Héritage dans les modèles de conception

Bien que le consensus soit que nous devrions privilégier la composition plutôt que l'héritage, il existe quelques cas d'utilisation typiques où l'héritage a sa place.

3.1. Le modèle de supertype de calque

Dans ce cas, noususe inheritance to move common code to a base class (the supertype), on a per-layer basis.

Voici une implémentation de base de ce modèle dans la couche de domaine:

public class Entity {

    protected long id;

    // setters
}
public class User extends Entity {

    // additional fields and methods
}

Nous pouvons appliquer la même approche aux autres couches du système, telles que les couches de service et de persistance.

3.2. Le modèle de méthode de modèle

Dans le modèle de méthode de modèle, nous pouvonsuse a base class to define the invariant parts of an algorithm, and then implement the variant parts in the subclasses:

public abstract class ComputerBuilder {

    public final Computer buildComputer() {
        addProcessor();
        addMemory();
    }

    public abstract void addProcessor();

    public abstract void addMemory();
}
public class StandardComputerBuilder extends ComputerBuilder {

    @Override
    public void addProcessor() {
        // method implementation
    }

    @Override
    public void addMemory() {
        // method implementation
    }
}

4. Principes de base de la composition

La composition est un autre mécanisme fourni par la programmation orientée objet pour la réutilisation de la mise en œuvre.

En un mot,composition allows us to model objects that are made up of other objects, définissant ainsi une relation «has-a» entre eux.

De plus,the composition is the strongest form of association, ce qui signifie quethe object(s) that compose or are contained by one object are destroyed too when that object is destroyed.

Pour mieux comprendre le fonctionnement de la composition, supposons que nous devions travailler avec des objets qui représentent des ordinateurs.

Un ordinateur est composé de différentes parties, dont le microprocesseur, la mémoire, une carte son, etc. Nous pouvons donc modéliser l’ordinateur et chacune de ses parties en tant que classes individuelles.

Voici à quoi pourrait ressembler une implémentation simple de la classeComputer:

public class Computer {

    private Processor processor;
    private Memory memory;
    private SoundCard soundCard;

    // standard getters/setters/constructors

    public Optional getSoundCard() {
        return Optional.ofNullable(soundCard);
    }
}

Les classes suivantes modélisent un microprocesseur, la mémoire et une carte son (les interfaces sont omises par souci de concision):

public class StandardProcessor implements Processor {

    private String model;

    // standard getters/setters
}
public class StandardMemory implements Memory {

    private String brand;
    private String size;

    // standard constructors, getters, toString
}
public class StandardSoundCard implements SoundCard {

    private String brand;

    // standard constructors, getters, toString
}

Il est facile de comprendre les motivations qui poussent la composition au détriment de l’héritage. In every scenario where it’s possible to establish a semantically correct “has-a” relationship between a given class and others, the composition is the right choice to make.

Dans l'exemple ci-dessus,Computer remplit la condition «has-a» avec les classes qui modélisent ses parties.

Il convient également de noter que dans ce cas,the containing Computer object has ownership of the contained objects if and only if the objects can’t be reused within another Computer object. s’ils le peuvent, nous utiliserons l’agrégation plutôt que la composition, où la propriété n’est pas implicite.

5. Composition sans abstraction

Alternativement, nous pourrions avoir défini la relation de composition en codant en dur les dépendances de la classeComputer, au lieu de les déclarer dans le constructeur:

public class Computer {

    private StandardProcessor processor
      = new StandardProcessor("Intel I3");
    private StandardMemory memory
      = new StandardMemory("Kingston", "1TB");

    // additional fields / methods
}

Bien sûr, ce serait une conception rigide et étroitement couplée, car nous rendrionsComputer fortement dépendants des implémentations spécifiques deProcessor etMemory.

Nous ne profiterions pas du niveau d’abstraction fourni par les interfaces et lesdependency injection.

Avec la conception initiale basée sur les interfaces, nous obtenons une conception à couplage lâche, qui est également plus facile à tester.

6. Conclusion

Dans cet article, nous avons appris les bases de l'héritage et de la composition en Java, et nous avons exploré en profondeur les différences entre les deux types de relations ("is-a" vs. "a un").

Comme toujours, tous les exemples de code présentés dans ce didacticiel sont disponiblesover on GitHub.