GLO-4002 - Site du cours 2023

Temps estimé: 60 minutes

Exercices préparatoires I

Mise en situation

Voici un programme qui permet de faire l'assignation d'un siège d'avion à un passager. Malheureusement, ce code n'a pas été conçu avec de bonnes pratiques de conception OO. Il présente plusieurs signes qui le rendront difficile à évoluer avec le temps.

public class AssignateurDeSiegeSimple {

  public Siege assigner(int volId, PassagerType passagerType) {
    String req = "SELECT * FROM t_vol WHERE id=" + volId + ";";
    Vol vol = (Vol) BaseDeDonnees.getSingleton().getTransaction().selectToObject(req);

    List<Siege> sieges = vol.getSieges();

    for(Siege siege: sieges) {
      try {
        essayerAssignerCeSiege(siege, passagerType);
        return siege;
      } catch(SiegeNonDisponible e) {
      }
    }

    throw new AucunSiegeDisponibleException();
  }

  private void essayerAssignerCeSiege(Siege siege, PassagerType passagerType) {
    if(siege.getDisponible() && siege.getType() == passagerType) {
      siege.setDisponible(false);
    } else {
      throw new SiegeNonDisponible();
    }
  }

}

Question 1

Le type du volId est un nombre entier. Est-ce que cela pourrait poser un problème? Si oui, quel est-il et comment le corriger?

Cela correspond à une obsession des primitives (Primitive Obsession).

Comme l'identifiant de vol est un concept riche du domaine de réservation de sièges, le représenter par une primitive pourrait causer un cout de maintenance très élevé s'il doit changer un jour (ce qui est fortement probable). Savoir qu'il s'agit d'un entier est une fuite de connaissance et le déclarer partout amène à une forte duplication de connaissance (DRY).

Pour le corriger, il faudrait encapsuler le nombre entier dans une classe VolId pour réduire l'effet d'avalanche en cas de changement.

Question 2

Identifier toutes les violations du SRP présentes dans la classe AssignateurDeSiegeSimple. Il devrait y en avoir 6.

L'assignateur de siège devrait seulement avoir la responsabilité d'orchestrer et déléguer aux autres classes la logique dans le but d'avoir assigné un siège d'un vol à un passager d'un certain type. Dans ce cas, il devrait savoir quand faire quoi, mais pas comment le faire (quand/quoi/comment).

Sachant cela, voici les violations du SRP présentes:

  1. Lignes 4-5: Responsable du mécanisme de persistance et de la requête a effectuer pour trouver le vol
  2. Lignes 7-15: Responsable de la logique pour trouver un siège libre
  3. Ligne 17: Responsable de la logique en cas de siège non trouve
  4. Ligne 21: Responsable de la logique pour déterminer si un siège est disponible ou non
  5. Ligne 22: Responsable de la logique pour assigner un siège
  6. Ligne 24: Responsable de la logique en cas de siège non disponible

Question 3

Ce code présente une importante violation du DIP. Quelle est-elle est comment la corriger?

L'assignateur de siège, qui devrait être un module de haut niveau ne connaissant aucun détails d'implémentation, dépend directement de la base de données SQL qui est un module de bas niveau.

Pour corriger cette violation, nous pourrions introduire une abstraction de persistance (ex: VolRepository) qui offre la capacité d'aller chercher un vol selon un identifiant donné, peu importe le mécanisme de persistance en arrière.

Nous aurions donc un design qui pourrait ressembler à ceci:

╭────────────────────╮            ╭───────────────╮
│                    │            │               │
│ AssignateurDeSiege │───────────▶│ VolRepository │
│                    │            │     < I >     │
╰────────────────────╯            ╰───────────────╯
                                          △
                                          │
                                          │
                                 ╭──────────────────╮
                                 │                  │
                                 │ SqlVolRepository │
                                 │                  │
                                 ╰──────────────────╯

Question 4

Ce code n'a pas été conçu d'une manière à être testable.

Proposer une façon de le réusiner et implémenter les tests en respectant les bonnes pratiques de tests et de mocks.

Note: ceci est une proposition de réusinage et non la seule et unique solution. Il est possible de trouver d'autres façons de rendre testable le code tout en respectant les bonnes pratiques de tests et de mocks.

public class AssignateurDeSiegeSimple {

  private final VolRepository volRepository;

  public AssignateurDeSiegeSimple(VolRepository volRepository) {
    this.volRepository = volRepository;
  }

  public Siege assigner(int volId, PassagerType passagerType) {
    Vol vol = volRepository.findById(volId);
    return vol.assignerSiege(passagerType);
  }

}

public class AssignateurDeSiegeSimpleTest {

  private static final int VOL_ID = 1234;
  private static final PassagerType PASSAGER_TYPE = PassagerType.ECONOMIE;

  private Siege siege;
  private Vol vol;

  private VolRepository volRepository;

  private AssignateurDeSiegeSimple assignateur;

  @BeforeEach
  public void setUpVol() {
    siege = new Siege();
    vol = mock(Vol.class);

    volRepository = mock(VolRepository.class);
    willReturn(vol).given(volRepository).findById(VOL_ID);
  }

  @BeforeEach
  public void setUpAssignateur() {
    assignateur = new AssignateurDeSiegeSimple(volRepository);
  }

  @Test
  public void quandAssigner_alorsTrouveLeVol() {
    assignateur.assigner(VOL_ID, PASSAGER_TYPE);

    verify(volRepository).findById(VOL_ID);
  }

  @Test
  public void quandAssigner_alorsAssigneSiegeAuPassager() {
    assignateur.assigner(VOL_ID, PASSAGER_TYPE);

    verify(vol).assignerSiege(PASSAGER_TYPE);
  }

  @Test
  public void quandAssigner_alorsRetourneSiegeAssigne() {
    willReturn(siege).given(vol).assignerSiege(PASSAGER_TYPE);

    Siege siegeRetourne = assignateur.assigner(VOL_ID, PASSAGER_TYPE);

    assertSame(siege, siegeRetourne);
  }

}
public class Vol {

  private final List<Siege> sieges;

  public Vol(List<Siege> sieges) {
    this.sieges = sieges;
  }

  public Siege assignerSiege(PassagerType passagerType) {
    for(Siege siege: this.sieges) {
      return essayerAssignerSiege(siege, passagerType);
    }

    throw new AucunSiegeDisponibleException();
  }

  private Siege essayerAssignerSiege(Siege siege, PassagerType passagerType) {
    try {
      siege.assigner(passagerType);
      return siege;
    } catch(SiegeNonDisponible e) {
    }
  }

}

public class VolTest {
  // ...

  @Test
  public void plusieursSiegesDisponibles_quandAssignerSiege_alorsAssignePremierSiegeLibre() {
    // ...
  }

  @Test
  public void aucunSiege_quandAssignerSiege_alorsLanceException() {
    // ...
  }

  @Test
  public void pasDeSiegeDisponible_quandAssignerSiege_alorsLanceException() {
    // ...
  }

}
public class Siege {

  // ...

  public void assigner(PassagerType passagerType) {
    if (disponible && type.equals(passagerType)) {
      disponible = false;
    } else {
      throw new SiegeNonDisponible();
    }
  }

}

public class SiegeTest {

  @Test
  public void quandAssigner_alorsPlusDisponible() {
    // ...
  }

  @Test
  public void nonDisponible_quandAssigner_alorsLanceException() {
    // ...
  }

  @Test
  public void typeDifferent_quandAssigner_alorsLanceException() {
    // ...
  }

}
public interface VolRepository {

  Vol findById(int volId);

}

public class SqlVolRepository implements VolRepository {
  public Vol findById(int volId) {
    String req = "SELECT * FROM t_vol WHERE id=" + volId + ";";
    return (Vol) BaseDeDonnees.getSingleton().getTransaction().selectToObject(req);
  }
}

public class SqlVolRepositoryTest {

  @Test
  public void volExistant_quandFindById_alorsRetournVol() {
    // ...
  }

  @Test
  public void volInexistant_quandFindById_alorsQuelqueChose() {
    // ...
  }
}