DEV Community

Tanguy Bernard
Tanguy Bernard

Posted on • Edited on

Empiler plusieurs maisons fait-il un immeuble ? Le pattern Composite à la rescousse !

Introduction

Qui a dit que la modélisation de nos systèmes était de tout repos ?

Dans cet article, nous allons explorer un cas concret inspiré du domaine de l’énergie :
la modélisation des sites équipés de compteurs électriques.

Nous allons ici détailler le chemin de pensée de notre équipe lors du développement d'une nouvelle fonctionnalité.

D'un modèle très simple, en passant par l'héritage et la composition nous terminerons notre voyage sur le Pattern Composite.


1. Définir le besoin métier

Dans notre système, nous avons des sites (des maisons, des écoles, des mairies...). Et nous aimerions connaître la consommation d'énergie de ces sites pour une période donnée et les comparer aux sites de même type. Par exemple pour le calcul de la consommation moyenne par foyer pour une étude INSEE.

Une maison éclairé

Pour faire simple, nous avons un site qui est une entité consommatrice d’électricité qui possède ces attributs :

  • un nom
  • un numéro de compteur (identifiant unique)
  • un type (maison, école, entrepôt…)
  • une consommation d'énergie (sur une période donnée)

Voici donc un premier modèle :

Schéma d'un Site

Cette modélisation suffit tant que nous gérons des sites individuels, mais le métier en a décidé autrement.


2. Nouveau besoin métier : regroupement de sites

En tant que gestionnaire énergétique de la ville de Rennes,
Je veux accéder à la consommation des différents quartiers,
Afin de comparer les zones et orienter les travaux urbains.

En lisant ce besoin, une idée rapide nous vient :

“Remplaçons le numéro de compteur unique par une liste de compteurs afin de les regrouper !”

Schéma d'un Site avec plusieurs compteurs

Mais ce modèle atteint rapidement ses limites :

  • Tous les compteurs d’un site seraient forcés de partager les mêmes caractéristiques (Le nom d'une maison, n'est pas le même que celui du quatier)
  • On ne distingue plus un site individuel d’un regroupement de sites.

Le vocabulaire du métier évolue aussi (ubiquitous language) : on parle désormais d'un lot (un groupement de sites), qui est une entité distincte qu’on ne représente pas du tout ici.

On manque également d'exemples pour bien comprendre l'intention métier. Prenons celui-ci déjà structuré :

Scénario: Calcul de la consommation totale du quartier
  Étant donné les sites suivants dans le quartier "Beaulieu" :
      | Nom                       | Consommation (kWh) |
      | Bibliothèque Universitaire| 6000               |
      | Restaurant Universitaire  | 15000               |
  Quand je demande la consommation du quartier "Beaulieu"
  Alors la consommation totale est 21000 kWh
Enter fullscreen mode Exit fullscreen mode

3. Tentative avec l’héritage

À ce moment-là, on se dit :

“Finalement, une maison ou un quartier, c’est un peu la même chose, non ?
Les deux consomment de l’électricité, les deux ont un nom. Que ce soit une maison qui possède une adresse ou encore le nom d'un quartier.
On pourrait peut-être modéliser ça de la même manière !”

Faisons hériter le Lot du Site.

Schéma du Lot héritant du Site

C'est pratique un Lot peut etre utilisé partout où un site est attendu.

Mais cette modélisation est trompeuse.

Plusieurs maisons les unes sur les autres

  • Un Lot n’est pas une spécialisation d'un site.
    Ce n’est pas un cas particulier de site, mais bien un agrégat de sites.

  • Une maison ne devient pas un immeuble en héritant d’une autre. Et un immeuble n’est pas une maison améliorée.

  • Et enfin plus technique, on viole le principe de substitution de Liskov.

En effet la violation se produit lorsque la classe enfant modifie le contrat de comportement implicite du parent.

Par exemple quand vous allez au marché acheter des fruits; pour calculer le prix des clémentines on va les peser. On ne pourra pas naturellement remplacer certaines clémentines de notre balance par des kiwis lors de la pesée car le prix du kiwi lui ne dépend pas du poids mais de la quantité. C'est cette différence de comportement dans la pesée qui casse la substitution de liskov car on suppose que la méthode de calcul est forcément liée au poids.

C'est pareil si le Lot hérite du Site, le comportement attendu d’un site (retourner un numéro de compteur, un type, etc.) ne peut pas s’appliquer à un Lot sans introduire des incohérences ou des contournements.


4. Revenir à la réalité pour guider la modélisation

Plutôt que de forcer la logique technique, regardons ce que dit la réalité :

  • Peut-on regrouper plusieurs maisons ? Oui, cela s’appelle un quartier.
  • Et si nous empilons les maisons (et qu'on les appelles appartements à la place 😉) ? Cela devient un immeuble.

Ce qu’on cherche à modéliser, c’est donc une structure composée de consommateurs, où chaque entité (simple ou composée) reste un consommateur d'électricité.

La composition permet déjà d’associer des objets différents (par exemple, un lot qui contient des sites).

Le pattern Composite, lui, pousse cette idée plus loin : il permet de composer des objets du même type abstrait, afin de manipuler de manière uniforme les entités simples et celles qui en regroupent plusieurs.

C’est exactement ce que permet le pattern Composite.


5. Le pattern Composite à la rescousse

Avec ce pattern, on introduit une abstraction Consommateur que peuvent implémenter :

  • les entités simples comme Site
  • les entités composites comme Lot

Schéma en utilisant le Pattern Commposite

Chaque Lot peut contenir des Site, mais aussi d’autres Lot. Le tout est considéré comme un Consommateur. Ainsi, notre système peut manipuler les entités simples et composées de façon uniforme.

Pour concrétiser le schéma, voici comment cela se traduit en Java.

Le Consommateur (le composant) :

public interface Consommateur {
    int getConsommation();
}
Enter fullscreen mode Exit fullscreen mode

Ensuite le Site (la feuille), qui implémente simplement ce contrat :

public class Site implements Consommateur {
    private String nom;
    private String numeroCompteur;
    private String type;
    private int consommation;

    public Site(String nom, String numeroCompteur, String type, int consommation) {
        this.nom = nom;
        this.numeroCompteur = numeroCompteur;
        this.type = type;
        this.consommation = consommation;
    }

    @Override
    public int getConsommation() {
        return this.consommation;
    }
}
Enter fullscreen mode Exit fullscreen mode

Enfin le Lot (le composite):

import java.util.ArrayList;
import java.util.List;

public class Lot implements Consommateur {
    private String nom;
    // La liste contient des interfaces : elle accepte donc des Sites OU des Lots
    private List<Consommateur> enfants = new ArrayList<>();

    public Lot(String nom) {
        this.nom = nom;
    }

    public void ajouter(Consommateur c) {
        enfants.add(c);
    }

    @Override
    public int getConsommation() {
        // On délègue le calcul à chaque enfant, qu'il soit un Site ou un autre Lot
        return enfants.stream()
                      .mapToInt(Consommateur::getConsommation)
                      .sum();
    }
}

Enter fullscreen mode Exit fullscreen mode

Pour reprendre notre exemple, voici son utilisation :

public class Main {
    public static void main(String[] args) {
        // Des sites simples
        Site s1 = new Site("Bibliothèque", "12345678912345", "batiment_publique", 6000);
        Site s2 = new Site("Restaurant U", "012435633785", "batiment_publique", 15000);

        // Un lot (Quartier)
        Lot quartierBeaulieu = new Lot("Beaulieu");
        quartierBeaulieu.ajouter(s1);
        quartierBeaulieu.ajouter(s2);

        // Un autre lot qui contient le premier ! (Ville)
        Lot villeRennes = new Lot("Rennes");
        villeRennes.ajouter(quartierBeaulieu); // On ajoute un Lot dans un Lot
        villeRennes.ajouter(new Site("Mairie", "12345678900045", "batiment_publique", 2000));

        // Le client n'a pas besoin de savoir la structure interne
        System.out.println("Conso totale : " + villeRennes.getConsommation()); 
        // Affiche : 23000
    }
}
Enter fullscreen mode Exit fullscreen mode

6. Ce que l’on y gagne

  • Une modélisation plus proche de la réalité
  • Une extensibilité naturelle du modèle à travers le Consommateur
  • Une logique métier plus claire
  • Un code plus simple et plus propre

Un ensemble de maisons


7. Conclusion

Finalement, notre cheminement de pensée est passé par pas mal d'état, nous avions besoin de comprendre le métier, d'y comprendre le sens, d'avoir aussi des exemples sur lesquels nous appuyer. #BDD

Nous savons qu'une modélisation du système n'est jamais figée dans le temps, elle évolue. Dans notre cas, ni l'héritage ni la simple composition pouvaient répondre au problème. Le pattern Composite était le choix le plus cohérent. Il nous a permis de représenter des structures hiérarchiques dans lesquelles les objets simples et composés pouvaient être traités de manière homogène.

Top comments (0)