GLO-4002 - Site du cours 2025

Temps estimé: 15 minutes

Pratique examen: Quiz

Polymorphisme

Considérez le code suivant.

Le code présente un ou des problèmes d'abstraction. Quelle serait alors la meilleure solution:

public class Employe {
    // ...

    public void payer(Periode periode) {
        Montant montant;

        if (this.tauxHoraire == 0) {
            montant = this.calculerSalaireAnnuel(periode);
        } else {
            montant = this.calculerSalaireHoraire(periode);
        }

        // ...
    }
}

public class Contractuel {
    // ...

    public void payer(Periode periode) {
        Montant montant = travaux.stream().reduce(new Montant(0), (s, t) -> s.plus(t.getTotal()));

        // ...
    }
}

public class PaiementCommande {
    // ...

    public void execute() {
        tousEmployes.forEach(employe -> employe.payer(periode));
        tousContractuels.forEach(contractuel -> contracuel.payer(periode));
    }
}
  1. Une classe abstraite Paie :
    public abstract class Paie { /* ... */ }
    public class Employe extends Paie { /* ... */ }
    public class Contractuel extends Paie { /* ... */ }
  2. Une interface Employe :
    public interface Employe { /* ... */ }
    public class EmployeSalaireHoraire implements Employe { /* ... */ }
    public class EmployeSalaireAnnuel implements Employe { /* ... */ }
    public class Contractuel implements Employe { /* ... */ }
  3. Une classe abstraite Employe :
    public abstract class Employe { /* ... */ }
    public class EmployeSalaireHoraire extends Employe { /* ... */ }
    public class EmployeSalaireAnnuel extends Employe { /* ... */ }
    public class Contractuel extends Employe { /* ... */ }
  4. Une classe concrète Employe dont les contractuels sont une variante (sous-classe)
    public class Employe { /* ... */ }
    public class Contractuel extends Employe { /* ... */ }
  5. Une interface Payable :
    public interface Payable { /* ... */ }
    public class EmployeSalaireHoraire implements Payable { /* ... */ }
    public class EmployeSalaireAnnuel implements Payable { /* ... */ }
    public class Contractuel implements Payable { /* ... */ }

La réponse est 5. Une interface Payable.

Le choix 1 n'est pas judicieux, car 1) Il ne règle pas le problème de paie entre horaire/annuel et 2) ne passe pas le test "IS-A"

Le choix 2 n'est pas judicieux, car il ne passe pas le test "IS-A" (contractuel est un employé?)

Le choix 3 n'est pas judicieux pour les mêmes raisons que 2.

Le choix 4 n'est pas judicieux pour toutes ces raisons mises ensemble.

Abstraction

Considérez le code suivant :

public class ValidateurEcheance {
    public void valider(Echeance echeance) {
        // ...
        if (echeance.estPassee()) {
            // ...
        }
    }
}

public class Echeance {
    private boolean estPassee;

    public boolean estPassee() {
        return this.estPassee;
    }
}

Certains pourraient se demander s'il ne serait pas mieux de rendre Echeance abstrait afin que le ValidateurEcheance n'ait pas à connaitre l'échéance. Qu'en pensez-vous (une seule réponse) :

  1. Oui, c'est une bonne idée pour faciliter les tests
  2. Oui, car un jour il existera un autre type d'échéance et il y aura du code à changer
  3. Non, car ValidateurEcheance n'a pas de données
  4. Non, car Echeance et ValidateurEcheance changent pour des raisons similaires
  5. Oui, pour améliorer la cohésion

La réponse est 4, Non, car Echeance et ValidateurEcheance changent pour des raisons similaires.

Placer une interface entre le validateur et l'échance n'apporterait pas de valeur ici. Ces 2 objets ont un cycle de vie identique.

On pourrait se poser la question à savoir si la validation devrait être dans l'échéance lui-même, mais ici ce n'était pas ce qui était demandé.

Le choix 5 est intéressant. On pourrait détecter un problème de cohésion dans ce code, mais ajouté une interface/classe abstraite n'adresserait pas le problème.

SOLID

Considérant ces tests (il faut s'imaginer le code qui les supporte) :

public class XTest {
    @Test
    public void quandPayer_devraitDistribuerMontant() {
        IDistributeur distributeur = mock(IDistributeur.class);
        X x = new X(distributeur);

        x.payer(20.0);

        verify(distributeur).distribuer(20.0);
    }
}

public class YTest {
    @Test
    public void quandMaintenance_devraitRemplir() {
        IDistributeur distributeur = mock(IDistributeur.class);
        Y y = new Y(distributeur);

        y.maintenir();

        verify(distributeur).remplir();
    }
}

Quel principe SOLID+T n'est pas respecté (un seul)?

  1. SRP
  2. OCP
  3. ISP
  4. DIP ou dépendances stables
  5. TDA

La réponse est l'ISP.

On peut détecter via ces tests que l'interface IDistributeur à 2 méthodes (remplir/0 et distribuer/1) et au moins deux clients (X et Y). Or, chacun des clients n'utilise qu'une des méthodes de l'interface.

Ce n'était pas demandé ici, mais la solution serait d'avoir plutôt 2 interfaces. N'oubliez pas que l'ISP est un principe qui, contrairement au LSP, s'intéresse aux clients de l'interface (e.g. l'interface est dictée par ses clients).

On peut potentiellement aussi détecter que le SRP serait facile à briser ici, mais sans voir l'implémentation de IDistributeur on ne peut pas tirer de conclusion.

Interfaces

Vous avez une interface nommée IBoutique dont l'unique implémentation est Boutique. Une classe Acheteur dépend de IBoutique afin de pouvoir effectuer une partie de ses achats se trouvant dans une boutique partenaire.

En vous basant uniquement sur ces informations, quels problèmes pouvez-vous anticiper? Indiquez tous les énoncés qui sont vrais.

  1. Considérant que Boutique est le nom de l'implémentation, il semble difficile d'imaginer ce que pourraient être les autres implémentations futures et distinctives.
  2. Le TDA ne semble pas être respecté
  3. L'interface ne semble pas définir une capacité
  4. L'abstraction IBoutique a clairement été créé en fonction de son client.
  5. Le SRP risque de ne pas être respecté pour IBoutique
  6. Cela semble être une violation de la Primitive Obsession
  7. Le fait d'avoir une interface qui commence par I n'est jamais une bonne pratique
  1. C'est vrai. On peut voir le problème de deux angles: l'implémentation devrait dire ce qui la distingue OU l'interface n'a pas un nom avec une capacité, ce qui rend le nommage difficile.
  2. C'est faux. Rien ne permet de déduire cela.
  3. C'est vrai, voir la réponse #1
  4. C'est faux. Si c'était le cas, le nom reflèterait une capacité désirée par le client.
  5. C'est faux. Rien ne permet de déduire cela.
  6. C'est faux. On ne parle d'aucun type primitif dans l'énoncé...
  7. C'est faux. C'est découragé puisque ça encourage un mauvais nommage, mais si c'est un code style et qu'on fait attention au nommage, ce n'est pas un problème en soi.

TDD

Voici un bout de code :

public class Passager {
    private int age;
    private double prix;

    public Passager(int age, double prix) { /* ... */ }

    public double calculerPrix() {
        if (age < 12) { return prix * 0.8; }
        return prix;
    }

    // ...
}

Et voici les tests unitaires correspondants :

public class PassagerTest {
    @Test
    public void testConstructeur() {
        Passager p = new Passager(1, 2.0);

        assertNotNull(p);
    }

    @Test
    public void testAssignation() {
        Passager p = new Passager(50, 2.0);

        assertEquals(50, p.getAge());
        assertEquals(2.0, p.getPrix());
    }

    @Test
    public void enfant_quandCalculerPrix_devraitAppliquerRabais() {
        Passager p = new Passager(1, 2.0);

        double prixCalcule = p.calculerPrix();

        assertEquals(2.0 * 0.8, prixCalcule);
    }

    @Test
    public void nePasAppliquerDeRabaisSurUnAdulte() {
        Passager p = new Passager(13, 2.0);

        double prixCalcule = p.calculerPrix();

        assertEquals(2.0, prixCalcule);
    }
}

Est-ce que ce test a été écrit en TDD? Choisir la meilleure réponse.

  1. Oui, car en TDD on doit commencer par tester le constructeur
  2. Oui, car la couverture est de 100%
  3. Non, car certains tests n'auraient jamais pu échouer
  4. Non, car le nommage des tests n'est pas pareil pour tous les tests
  5. Oui, car tous les tests testent un comportement et non des méthodes

La réponse est 3, non car certains tests n'auraient jamais pu échouer

Tester un constructeur, entre autres, ne peut jamais échouer. C'est un comportement de java, pas besoin de le tester...

La couverture de test ne veut rien dire. Au contraire, écrire des tests pour réussir à avoir 100% est généralement un smell.

Le nommage des tests n'est pas pareil en effet, et une phase bleue (refactoring) dans le TDD pourrait attraper cela (ainsi que plusieurs autres erreurs de code style ici). Par contre, avoir mal fait son refactoring ou avoir fait un oubli n'est pas forcément signe que ce n'est pas écrit en TDD.

Tester les comportements au lieu des méthodes est bien, mais n'est pas forcément signe que la personne a fait ou non du TDD. Par contre, à l'inverse, faire du TDD aide grandement à focuser sur les comportements dans ses tests! La réponse 5 est également une fausse prémise, ce n'est pas vrai que tous les tests testent des comportements.

TDA

Cochez le(s) énoncé(s) qui est/sont vrai(s)

  1. Un objet n'ayant que des "getters" ne respecte pas le Tell Don't Ask
  2. Une violation du TDA se matérialise souvent par ce que l'on nomme un "train wreck" au niveau du code
  3. Le non-respect du TDA créer un problème de couplage
  4. Pour respecter le Tell Don't Ask à la lettre, toutes les méthodes doivent être void
  1. C'est faux. C'est impossible de conclure que ça respecte le TDA ou non, ça dépend de ce que le client fait avec ces donneés.
  2. C'est vrai. Ce n'est pas toujours le cas, mais c'est toujours un smell. Un train wreck c'est, par exemple : this.getUserAccount().getCreditAccount().setBalance(...)
  3. C'est vrai. Il y a plusieurs façons dont ça peut arriver, mais une façon rapide de le voir est qu'on est couplé avec les dépendances de sa dépendance. Par exemple dans l'exemple pour #2, on sait maintenant que user account n'a qu'un et un seul compte crédit.
  4. C'est faux. Le mot clé est doivent. On peut respecter le TDA même sans avoir des méthodes void partout.