GLO-4002 - Site du cours 2025

Temps estimé: 30 minutes

Recap 4 : Anciens examens

Voici des questions sur le TDA et le SRP qui proviennent d'anciens examens.

Question 1

En considérant la classe suivante

public class SiegeController{
    public ReponseSiegeDto trouverMeilleursSiege(DemandeSiegeDto demande) {
        // 1−Créer la bonne classe dans le vol
        ClasseVol classeVol;
        if (demande.classeVol == "E") {
            classeVol = new ClasseEconomie(volId);
            // Ignorez comment la classe obtient les sièges
            // pas important pour l’examen
        } else if (demande.classeVol == "A") {
            classeVol = new ClasseAffaires(volId);
        } else {
            throw new RuntimeException("Classenonsupportée");
        }

        // 2−Trouver les siège
        List<Siege> siegesTrouves = classeVol.trouverMeilleursSieges(demande);

        // 3−Préparation de la réponse
        if (siegesTrouves.isEmpty()) {
            throw new PasDeSiegeTrouve();
        }
        return ReponseSiegeDto.from(siegesTrouves);
    }
}

Expliquez où et comment le SRP n'est pas respecté

ATTENTION, nous ne cherchons pas une réponse théorique en répétant l’énoncé du principe. Nous voulons une explication concrète qui explique ce qui vous amène à penser que le principe n’est pas respecté dans CE CAS.

Le SRP n'est pas respecté dans cette classe puisque la méthode trouverMeilleursSiege fait plusieurs choses. Tout d'abord, elle détermine quelle est la classe de vol en fonction de demande.classVol. Cette responsabilité devrait être transmise à une autre classe (une factory de la couche Domaine par exemple). Elle se charge aussi de trouver les meilleurs sièges pour la classe de vol et de construire la réponse retournée par le controlleur.

Idéalement, toute la logique qui précède le if (siegesTrouves.isEmpty()) devrait se trouver dans la couche Application et non dans la couche Web puisque la couche Web devrait se préoccuper de recevoir les requêtes, les désérialiser, appeler la couche Application, et formatter les réponses HTTP retournées par le controlleur.

Il y a aussi un problème d'OCP ici puisque le controlleur (couche Web) devra changer chaque fois qu'il y a un nouveau type de classe de vol à supporter. Puisque ça serait alors un nouveau besoin d'affaires, il ferait plus de sens que la couche Domaine soit affectée par ce changement. C'est pourquoi le déplacement de cette logique dans une factory de la couche Domaine serait pertinent.

Question 2

Considérant le code suivant

public class GroupeClasse {
    private List<Travail> travaux = new ArrayList<>();
    private List<Etudiant> etudiantsInscrits = new ArrayList<>();

    public void ajouterTravail(Travail travail) {
        this.travaux.add(travail);
    }

    public void inscrireEtudiant(Etudiant etudiant) {
        etudiantsInscrits.add(etudiant);
    }

    /*
    * 1. Pour tous les travaux > Pour tous les etudiants ayant remis
    *   > Inscrire son resultat a son dossier
    *   Le resultat est pondere ( % que vaut le travail)
    *   > Rien inscrire si pas remis
    * 2. Une fois tous resultats inscrits aux dossiers
    * > On fixe la cote en fonction de la note
    *   totale de tous les travaux (sommes des notes ponderees)
    */
    public void publierNotes() {
        if (travaux.size() == 0) {
            return;
        }

        Map<Etudiant, Double> notesPondereeParEtudiant = new HashMap<>();

        for (Travail travail : travaux) {
            for (Remise remise : travail.getRemises()) {
                Etudiant etudiantRemise = remise.getEtudiant();

                double note = remise.getNote();
                double ponderation = travail.getPonderation();
                double notePonderee = note * ponderation;
                etudiantRemise.getDossier().inscrireResultatTravail(travail.getCode(),
                    notePonderee);

                // Pour l’etudiant ayant soumis cette remise, on cummule ses notes
                // totales de tous les travaux dans une grande
                // Map d "Etudiant => Note_commulee"
                notesPondereeParEtudiant.merge(etudiantRemise, notePonderee, (x, y) -> x + y);
            }
        }

        // Pour chaque etudiant, il faut fixer sa cote a partir
        // de la somme des notes ponderees de tous ses travaux remis
        for (Etudiant etudiant : etudiantsInscrits) {
            double total = notesPondereeParEtudiant.getOrDefault(etudiant, 0.0);
            etudiant.getDossier().fixerCote(total);
        }
    }
}

class Travail {

    private String code;
    private Map<Etudiant, Remise> remises = new HashMap<>();
    private double ponderation;

    Travail(String code, double ponderation) {
        this.code = code;
        this.ponderation = ponderation;
    }

    public List<Remise> getRemises() {
        return new ArrayList<>(remises.values());
    }

    public void addRemise(Remise remise) {
        remises.put(remise.getEtudiant(), remise);
    }

    public String getCode() {
        return code;
    }

    public double getPonderation() {
        return ponderation;
    }
}

class Remise {

    private double note;
    private Etudiant etudiant;

    Remise(double note, Etudiant etudiant) {
        this.note = note;
        this.etudiant = etudiant;
    }

    public double getNote() {
        return note;
    }

    public Etudiant getEtudiant() {
        return etudiant;
    }
}

class Etudiant {

    private String ni;
    private DossierEtudiantElectronique dossier;

    public Etudiant(String ni, DossierEtudiantElectronique dossier) {
        this.ni = ni;
        this.dossier = dossier;
    }

    public String getNI() {
        return ni;
    }

    public DossierEtudiantElectronique getDossier() {
        return dossier;
    }
}

interface DossierEtudiantElectronique {
    public void inscrireResultatTravail(String code, double notePonderee);
    public void fixerCote(double noteFinale);
    public void publier();
}

class Registraire {
    private List<DossierEtudiantElectronique> etudiants = new ArrayList<>();

    public void publierCotes() {
        for (DossierEtudiantElectronique dossierEtudiant : etudiants) {
            dossierEtudiant.publier();
        }
    }
}

Ce code fait preuve d’obsession des primitives (Primitive Obsession).

Donnez 2 cas d’obsession :

  1. l’un que vous pensez judicieux de régler et indiquez comment le régler

Note, Pondération et NI mériteraient d'être remplacés par des values objects.

Note

  • Afin d'y ajouter de la validation indiquant si le format de la note est 15/20, 75 ou 0.75.
  • Le fait d'ajouter un value object Note nous permet d'empêcher qu'une valeur comme 125 soit entrée.

Ponderation

  • Un peu le même principe. Est-ce que c'est 20% ou 0.20 qui est attendu ?
  • Le value object Note nous permet d'empêcher qu'une valeur comme 20 soit entrée si le format attendu est 0.20.

NumeroIdentification

  • Permet de valider que le format du NI est valide (5 lettres suivies par maximum 3 caractères)
  1. un 2e que vous laisseriez tel quel (ne pas corriger) et pourquoi

Le code du travail peut demeurer une chaîne de caractères, car il ne semble pas avoir de format précis requis.

Question 3

En vous référant au code de la question 2, la méthode publierNotes de la classe GroupeClasse ne respecte pas à plusieurs endroits le Tell don’t Ask (TDA).

Proposer un réusinage afin de respecter le TDA.

Instructions :

  • Vous devez préserver exactement le même comportement final.
  • Vous pouvez cependant créer de nouvelles méthodes ou modifier des méthodes dans les classes existantes.
  • Vous de devez PAS toucher à l’interface DossierEtudiantElectronique
  • Vous ne devez PAS changer la signature de la méthode d’entrée GroupeClasse::publierNotes()
  • Indice : il n’est pas nécessaire d’introduire de nouvelles classes.

public class GroupeClasse {
    private List<Travail> travaux = new ArrayList<>();
    private List<Etudiant> etudiantsInscrits = new ArrayList<>();

    public void ajouterTravail(Travail travail) {
        this.travaux.add(travail);
    }

    public void inscrireEtudiant(Etudiant etudiant) {
        etudiantsInscrits.add(etudiant);
    }

    /*
     * 1. Pour tous les travaux > Pour tous les etudiants ayant remis
     *   > Inscrire son resultat a son dossier
     *   Le resultat est pondere ( % que vaut le travail)
     *   > Rien inscrire si pas remis
     * 2. Une fois tous resultats inscrits aux dossiers
     * > On fixe la cote en fonction de la note
     *   totale de tous les travaux (sommes des notes ponderees)
     */
    public void publierNotes() {
        if (travaux.size() == 0) {
            return;
        }

        Map<Etudiant, Note> notesPondereeParEtudiant = new HashMap<>();
        for (Travail travail : travaux) {
            travail.comptabiliserNotesPonderees().forEach((etudiant, notePondereeTravail) ->
                    notesPondereeParEtudiant.merge(etudiant, notePondereeTravail, Note::ajouter)
            );
        }

        for (Etudiant etudiant : etudiantsInscrits) {
            etudiant.fixerCote(notesPondereeParEtudiant.getOrDefault(etudiant, new Note(0.0)));
        }
    }
}

public class Travail {

    private String code;
    private Map<Etudiant, Remise> remises = new HashMap<>();
    private Ponderation ponderation;

    Travail(String code, Ponderation ponderation) {
        this.code = code;
        this.ponderation = ponderation;
    }

    public List<Remise> getRemises() {
        return new ArrayList<>(remises.values());
    }

    public void addRemise(Remise remise) {
        remises.put(remise.getEtudiant(), remise);
    }

    public String getCode() {
        return code;
    }

    public Ponderation getPonderation() {
        return ponderation;
    }

    public Map<Etudiant, Note> comptabiliserNotesPonderees() {
        Map<Etudiant, Note> notes = new HashMap<>();
        this.remises.forEach((etudiant, remise) -> {
            var notePonderee = remise.getNotePondereeDuTravail(ponderation);
            etudiant.inscrireResultatDuTravail(this, notePonderee);
            notes.put(etudiant, notePonderee);
        });
        return notes;
    }
}

public class Remise {

    private Note note;
    private Etudiant etudiant;

    Remise(Note note, Etudiant etudiant) {
        this.note = note;
        this.etudiant = etudiant;
    }

    public Note getNote() {
        return note;
    }

    public Etudiant getEtudiant() {
        return etudiant;
    }

    public Note getNotePondereeDuTravail(Ponderation ponderation) {
        return note.ponderee(ponderation);
    }
}

public class Etudiant {

    private NumeroIdentification ni;
    private DossierEtudiantElectronique dossier;

    public Etudiant(NumeroIdentification ni, DossierEtudiantElectronique dossier) {
        this.ni = ni;
        this.dossier = dossier;
    }

    public NumeroIdentification getNI() {
        return ni;
    }

    public DossierEtudiantElectronique getDossier() {
        return dossier;
    }

    public void inscrireResultatDuTravail(Travail travail, Note notePonderee) {
        dossier.inscrireResultatTravail(travail.getCode(), notePonderee);
    }

    public void fixerCote(Note total) {
        dossier.fixerCote(total.note());
    }
}

// DossierEtudiantElectronique est resté identique
// Registraire est resté identique

public record Note(double note) {
    public Note {
        if (note < 0 || note > 100) {
            throw new IllegalArgumentException("La pondération doit être entre 0 et 100");
        }
    }

    public Note ponderee(Ponderation p) {
        return new Note(this.note * p.ponderation());
    }

    public Note ajouter(Note autre) {
        return new Note(this.note + autre.note());
    }
}

public record Ponderation(double ponderation) {
    public Ponderation {
        if (ponderation < 0 || ponderation > 1) {
            throw new IllegalArgumentException("La pondération doit être entre 0 et 1");
        }
    }
}

public record NumeroIdentification(String numeroIdentification) {
    public NumeroIdentification {
        if (numeroIdentification == null || numeroIdentification.isEmpty()) {
            throw new IllegalArgumentException("Laval University ID cannot be empty");
        }
        if (!matches("^[a-zA-Z]{5}($|([1-9]\\d{0,2})$)", numeroIdentification)) {
            throw new IllegalArgumentException(
                    "%s is not a valid Laval University ID (expected 5 letters followed by at most 3 digits)"
                            .formatted(numeroIdentification)
            );
        }
    }
}

Question 4

  1. Expliquez dans vos mots ce qu’est un domaine anémique.

Un domaine anémique est un domaine dans lequel les classes servent uniquement de structure de données c'est-à-dire que les classes servent uniquement à déterminer les propriétés d'un objet et ne contiennent aucune logique du domaine d'affaires. Les règles métier peut alors se retrouver dans les services de la couche Application ou dans les controlleurs de la couche Web qui sont des couches beaucoup moins stables que la couche Domaine. Les règles métier sont donc beaucoup moins protégées des changements que si elles étaient dans la couche Domaine.

  1. Pourquoi est-ce un problème dans un domaine « riche » où la logique d’affaires est complexe ? Quelle serait la conséquence de créer un tel domaine anémique dans ce contexte ?

Dans un domaine anémique, la logique du domaine d'affaires peut être dupliquée et éparpillée à plusieurs endroits ce qui ne permet pas aux classes de la couche Domaine d'exprimer véritablement et facilement le domaine métier. Le domaine d'affaires devient donc difficile à comprendre ce qui rend aussi la maintenance et l'ajout de nouvelles fonctionnalités plus difficiles puisqu'il y a souvent plusieurs classes de plusieurs couches différentes à modifier pour effectuer le changement.

L'éparpillement de la logique augmente aussi le risque d'introduire des bugs.

  1. Dans quel contexte devrait-on utiliser un domaine anémique ?

Un domaine anémique serait pertinent dans un système où le domaine n'est pas riche en règles d'affaires et d ont le but principal est les opérations CRUD (Create/Read/Update/Delete) dans la base de données.