• 8 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 20/08/2024

Identifiez les faiblesses de votre système actuel

Au chapitre précédent, nous avons généré un nouvel ensemble de classes d'entités, et poursuivi l’approche consistant à les connecter aux classes Java Swing et SQLite. Nous voulons maintenant prendre un peu de recul et voir ce qui doit être remplacé ou modifié dans notre système existant pour supporter une meilleure architecture (web découplée).

Analysez les problèmes au sein de votre système actuel

Avant d’ajouter de nouveaux éléments, regardons ce qui est actuellement en place, en essayant de repérer ce qui posera problème sur le long terme. Je l’ai dit, et je le répète : nous ne voulons pas rester dans une situation où nous ajoutons simplement de nouvelles fonctionnalités selon les besoins.

Une approche en fonction des besoins conduit à des systèmes faibles.
Une approche en fonction des besoins fragilise un système.

Quand on ajoute des fonctionnalités les unes par dessus les autres dans une application, ça devient rapidement une usine à gaz…

Plus on attend avant de commencer à faire un peu d'ordre, plus il devient difficile de modifier des fonctionnalités et de faire évoluer l'application : une situation que nous voulons éviter à tout prix !

Comment savoir si quelque chose est en désordre ?

Nous pouvons nous poser quelques questions sur le logiciel actuel. Voici un bon point de départ : est-il simple ? Autrement dit :

  • Est-ce que je comprends ce que le logiciel essaye de faire ? Est-ce que je comprends comment il le fait ?

  • Est-ce qu’il est facile à maintenir (trouver et réparer les bugs lorsqu’ils se produisent) ? Est-ce qu’il est facile d’apporter un changement ? Un changement qui ne provoque pas de bugs dans cette zone, ou ailleurs ?

  • Est-ce qu’il est facile à tester ?

Comme vous pouvez le voir, nous allons tout droit vers une usine à gaz :

Le diagramme montre une architecture de plus en plus complexes : Les classes JavaSwing Mécanicien de l'avion, Problème de maintenance, Avion, Réservation, Client, Voyage, Touristes et Pilote sont chacune reliées à la base de données SQLite. De plus,
Notre architecture actuelle 😱

Étape 1 : Posez les bonnes questions

Nous avons besoin d’instruments de mesure qui nous disent ce qui est mieux et ce qui ne l’est pas. Par exemple, regardons un morceau de code provenant de l’application d’origine de la compagnie aérienne :

class AircraftMechanic {
   public List<MaintenanceIssue> getAllUnfixedIssues(JavaSwing component) {
      Connection conn = DriverManager.getConnection( "jdbc:sqlite:./db/test.db")) {
      if (conn != null) {
         // get all the records
         String sql = "SELECT id, entered, details, fixed FROM maintenance”;
         Statement stmt  = conn.createStatement();
         ResultSet rs    = stmt.executeQuery(sql)) {
             
         // loop through the result set
         while (rs.next()) {
            // unfixed (i.e. no date set)
            if (rs.getString(“fixed”).equals(“”)) {
               // show the details for this item
               component.addCheckbox(rs.getString(“details”));
            }
         }
      }
   }
}

Utilisons nos questions :

  • Est-ce que je comprends ce que le code essaye de faire ? Comment il le fait ? Cette méthode fait tout. Récupérer les archives, parcourir les archives, déterminer quels éléments ne sont pas réparés, et enfin mettre à jour l’affichage.

  • Est-ce qu’il est facile à maintenir ? À modifier ? Vu que le code essaye de tout faire, il n’est pas facile à modifier.

  • Est-ce qu’il est facile à tester ? Il ne peut pas non plus être testé, sauf s’il est connecté à une IHM et une base de données.

Pourquoi est-ce que l’on écrit du code difficile à comprendre ?

Il n’était probablement pas difficile à comprendre au moment où il a été écrit (par nous ou par quelqu’un d’autre). L’idée était fraîche dans nos esprits. Je vais vous donner un autre type d’exemple. J’ai déjà vu du code ayant un émetteur et un récepteur qui fonctionnent en binôme. Les variables sont nommées tx et rx, respectivement :

public void processResponse() {
   Receiver rx = new Receiver();
   Transmitter tx = new Transmitter();
   while (rx.hasData() {
      Data data = rx.getData();
      tx.sendData(data);
   }
}

Et maintenant, commençons avec notre première question : Est-ce que je comprends ce que le code essaye de faire ? Comment il le fait ? Tant que le récepteur reçoit des données, alors il reçoit ces données et les transmet. Mais maintenant, cela fait des mois (voire des années ?) que nous (ou quelqu’un d’autre) avons écrit ce code. Transmettre quoi ? À quoi ? Recevoir quoi ? Oh, attendez, j’ai trouvé, quelque chose qu’on a nommé « données ». Super. Quel genre de données ?

Notre plus gros problème, ici, c’est que nous aurions dû mieux nommer les choses. Et si nous essayions plutôt ce qui suit ?

public void processPrescriptionResponse() {
   Receiver patientRxHistory = new Receiver();
   Transmitter pharmacySender = new Transmitter();
   while (patientRxHistory.hasData() {
      Data patientRxRefilled = rx.getData();
      pharmacySender.sendData(patientRxRefilled);
   }
}

Un petit changement rend ce code beaucoup plus facile à comprendre et à travailler.

Étape 2 : Utilisez les principes de conception SOLID

Les principes de conception SOLID sont d’excellents instruments de mesure à utiliser.

Un principe facile à enfreindre est celui de la responsabilité unique. Lorsqu’une nouvelle fonctionnalité est nécessaire, il est souvent facile d’ajouter du nouveau code à du code existant.

Comme nous pouvons le constater dans la méthode  getAllClientsWithTripThisWeeky() , le code enfreint ce principe en faisant trop. La classeClientreprésente quelqu’un dont on attend un paiement pour le voyage et qui doit être un des voyageurs. La classe ne doit pas être responsable de savoir comment interagir avec une base de données SQLite, ni avec des composants d’une IHM.

De plus, le stockage des données doit être géré par une couche de responsabilités qui lui est propre. Tout ce qui a besoin de sauvegarder ou de récupérer des informations devra le faire en accédant à cette couche de classes. De façon similaire, l’interface utilisateur doit elle-même être gérée dans sa propre couche.

Si nous voulons abandonner du code sur le critère de la responsabilité unique, notre architecture va subir une modification significative.

Étape 3 : Appliquez des design patterns

Enfin, il existe un autre groupe d’instruments de mesure : les design patterns. Les design patterns sont des solutions déjà existantes à des problèmes courants. Pourquoi réinventer les choses alors que nous pouvons tirer profit d’une idée qui a fait ses preuves ? 

Prenons un exemple associé aux langages orientés objet comme Java et C++. Nous voulons qu’un objet change de comportement en fonction de certaines circonstances. L’héritage est l’une des façons dont vous pouvez atteindre ce but. Traduisez les différentes fonctionnalités par différentes sous-classes. Puis, lorsqu’un objet doit changer, créez un nouvel objet du bon type et jetez l’ancien.

D’accord, mais que se passe-t-il si d’autres parties du système contiennent encore une référence à l’ancien objet ? Nous ne pouvons pas remplacer automatiquement l'ancienne référence par la nouvelle  sans générer une belle pagaille. La référence d’origine devra donc rester en place. Néanmoins, nous voulons obtenir ce changement de comportement.

Nous pouvons utiliser le design pattern "object-role". Il s’agit de considérer le nouveau comportement comme une partie d’un rôle joué par l’objet. De la sorte, l’objet pourra facilement changer de rôle, mais en restant le même objet.

Comment appliquer cela à notre application ?

Notre dernier cas d’usage concerne les clients qui ont des impayés, et ceux qui doivent beaucoup d’argent, plutôt que juste un peu. Nous voulons montrer ces types de clients différemment. Mais nous ne voulons pas échanger entièrement les objets. Voici un diagramme UML de la façon dont nous pouvons appliquer ce modèle dans notre application :

Le diagramme montre les différents statuts possibles concernant le client : option 1 : il a payé le montant entièrement, option 2 : il doit de l'argent et option 3 : il doit beaucoup d'argent.
Utilisation de design patterns

Nous appellerons pour commencer le  changeStatus()  d’un client avec un objet  ClientStatusPaidInFull  (StatutClientPayéEntièrement).

Puis, si nous déterminons qu’il a un solde impayé, nous le remplacerons par  ClientStatusOwes  (StatutClientDette) ou  ClientStatusOwesLots  (StatutClientDetteImportante). Lorsqu’il paye son solde, nous le remettrons à  PaidInFull . Mais nous nous en occuperons quand nous en serons là. 😉

Les design patterns peuvent vous aider à résoudre une multitude de problèmes dans votre application.

En résumé

  • Il faut examiner le code existant, et repérer ce qui est difficile à comprendre, à modifier, ou à tester.

  • On utilise les principes SOLID pour prendre des décisions de modification de l’architecture. 

  • Lorsque cela s'y prête, on peut utiliser les design patterns.

Maintenant que nous savons quels instruments de mesure nous pouvons utiliser pour nos analyses de code, nous allons prioriser nos user stories !

Exemple de certificat de réussite
Exemple de certificat de réussite