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.
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 :
É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 classeClient
repré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 :
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 !