Gardez vos yeux sur les objectifs
Faire fonctionner votre application, c’est bien… mais vous devez ensuite vivre avec. Voici ce qu’il faut viser lors de la création d’une application maintenable.
Comme la plupart des développeurs, je passe la plupart de mon temps à ajouter des fonctionnalités aux applications existantes, à modifier les fonctionnalités existantes pour répondre aux changements de l’environnement et (occasionnellement) à corriger les bogues. Il est inhabituel pour moi de faire du développement « greenfield ».
Par conséquent, ce qui m’importe, c’est de créer des applications qui soient facile à maintenir— des applications où il est facile d’ajouter ou de modifier des fonctionnalités et où il est facile de trouver et de corriger les bogues lorsqu’ils surviennent.
À l’époque où j’étais à la tête d’un service informatique, si vous veniez me voir et proposiez de réduire de 10 % nos coûts de développement, je serais légèrement intéressé. D’un autre côté, si vous m’offriez une réduction de 5 % sur les frais de maintenance de mes applications… eh bien, vous auriez mon attention. « Maintenable » n’est que juste derrière « fonctionnel » dans la liste des « fonctionnalités que j’aime ».
Pour moi, créer applications maintenables Il ne s’agit pas de mettre en œuvre un modèle particulier ou un principe SOLID et certainement pas d’utiliser un outil particulier. Il s’agit de garder mon œil sur un ensemble spécifique de trois objectifs. Si cela ne ressemble pas à la façon dont les principes et les modèles de programmation sont généralement discutés, laissez-moi vous donner un exemple.
Je dois cependant vous avertir : je n’ai aucune compétence en matière de planification. Lorsque j’écris du code, j’ai tendance à en inventer une grande partie au fur et à mesure (c’est en écrivant du code que j’arrive à comprendre le problème et sa solution). Je suis sûr qu’il existe des développeurs qui peuvent planifier leurs applications à l’avance. Je ne suis pas cette personne. Donc, au risque de paraître ridicule, voici une description réaliste du processus que je traverse pour créer une application maintenable. C’est aussi pourquoi les outils de test sont importants pour moi.
Quand les principes échouent
Par exemple, on peut me demander de développer une application avec une fonctionnalité « ajouter une commande client ». Cette fonctionnalité peut être implémentée en tant que méthode unique qui crée les objets/éléments de données suivants :
- En-tête de commande
- Détail de la commande client
- Facture client
- ClientCreditCheck
- InventaireRéservation
- ExpéditionRéservation
- SendCustomerEmailNotification
- …Suite…
La signature de cette méthode est simple, directe, facile à comprendre et facile à appeler :
public void CreateSalesOrder(List<Product> products, DateTime shipDate, Customer cust)
Et c’est très bien… jusqu’à ce que ce ne soit plus le cas.
Il s’avère, par exemple, que je dois ajouter une autre fonctionnalité : L’application doit avoir une fonctionnalité de « prépaiement » pour prendre en charge les clients qui utilisent un crédit existant pour payer des produits. Dans ces transactions, ni CustomerCreditCheck ni CustomerInvoice ne sont nécessaires, mais UpdateCustomerCredit l’est.
La situation s’aggrave : l’entreprise souhaite également commencer à vendre des produits numériques et a maintenant besoin d’une fonctionnalité DigitalSalesOrder. Avec les produits numériques, la fonctionnalité ShippingReservation n’est pas requise, mais une nouvelle fonction EnableDownload l’est.
Créer une solution non maintenable
Je pourrais gérer cette demande en ajoutant des paramètres booléens facultatifs à CreateSalesOrder qui peuvent être utilisés pour désactiver ou activer certains comportements dans la méthode. Le résultat est une méthode dont la signature ressemble maintenant à ceci :
public void CreateSalesOrder(List<Product> products, , DateTime shipDate, Customer cust,
bool addDownload = false,
bool addCustomerCredit = false,
bool skipCreateShipping = false,
bool skipCreditCheck= false,
bool skipCreateInvoice= false)
Bien sûr, cela nécessite l’ajout d’un nouveau code, y compris plusieurs blocs if qui incluent/excluent des fonctionnalités basées sur ces paramètres booléens. Par conséquent, ma méthode est plus difficile à comprendre, à tester et à déboguer. Et, bien sûr, comme il s’agit d’une seule grosse méthode, un bogue dans n’importe quelle partie du code rend l’ensemble du processus peu fiable : s’il y a un punaise dans ce code, j’ai beaucoup de code à parcourir pour le trouver.
Si j’avais écrit la fonction CreateSalesOrder, j’aurais vu cela venir. Vu la taille de cette méthode, j’appliquerais le principe de responsabilité unique (PRS). Suivant ce principe, je créerais chacune de ces étapes comme une méthode distincte (par exemple, CreateHeader(), CreateDetail() et ainsi de suite). Lorsqu’un traitement différent est requis, les clients peuvent mélanger et assortir ces méthodes pour créer leur solution.
Mais, remarquez : de manière réaliste (et compte tenu de mon manque de compétences en planification), j’aurais écrit du code avant d’en arriver là. Comme j’aime écrire des tests automatisés, j’aurais également tiré parti Studio d’essais et JustMock pour construire des tests qui prouvent que mon code fonctionne. Comme j’ai divisé mon code initial en cinq ou six méthodes SRP (et Visual Studio peut aider ici), j’utiliserais mes tests existants pour prouver que je n’introduisais pas de nouveaux bogues (je créerais également des tests pour chacune de mes méthodes SRP). Franchement, je pense que quiconque modifie du code sans tests unitaires pour prouver que l’application fonctionne toujours « comme prévu » est un fou.
J’ai cependant créé un nouveau problème : un client qui souhaite simplement créer une commande client standard (le cas normal qui représente 80 % de l’activité de mon entreprise) doit appeler toutes ces méthodes individuelles. Bien que j’aurais les tests qui prouvent que cela fonctionnerait, considérez les chances de tout autre développeur de réussir la première fois.
Et quand mon entreprise décide, après la sortie, d’ajouter une nouvelle fonctionnalité (PredictFutureSales, par exemple)… eh bien, oui, je n’ai qu’à écrire une nouvelle méthode et je peux laisser les autres méthodes SRP inchangées. Mais, parce que cette fonctionnalité est ajoutée après l’application a été publiée, je dois également retrouver chaque client qui crée une commande client et réécrire son code pour également appeler PredictFutureSales.
Désolé, les amis : du point de vue de la maintenance, c’est ne pas meilleur.
Créer une solution maintenable
Le problème est que je n’ai appliqué qu’un seul principe. Je suis en fait à mi-chemin d’une solution maintenable – j’ai juste besoin d’appliquer le motif de façade.
Dans le modèle de façade, j’écris toujours la méthode originale CreateSalesOrder, mais la méthode ne fait qu’appeler mes méthodes SRP. Désormais, les clients qui créent une commande client standard appellent simplement la méthode CreateSalesOrder de la façade. Et la bonne nouvelle pour moi, c’est que j’ai déjà écrit les tests pour cette méthode de façade.
Plus important encore, en combinant le SRP avec le motif de façade, les coûts d’extension de l’application baissent :
- De nouvelles fonctionnalités de traitement des commandes client peuvent être implémentées en mélangeant et en faisant correspondre les méthodes SRP sans les modifier (au pire, je pourrais avoir besoin d’écrire de nouvelles méthodes).
- Si l’application doit être étendue avec des fonctionnalités supplémentaires, j’écris une nouvelle méthode et l’ajoute à la méthode de façade. Ni le client ni les autres méthodes SRP ne doivent être modifiés.
- Si une méthode SRP doit fonctionner différemment (avant ou après la publication), je peux réécrire la méthode sans changer la façade, les méthodes SRP ou le client.
- Les bogues ont tendance à être isolés à l’intérieur de méthodes individuelles : je peux généralement isoler un bogue dans une méthode où le problème concerne soit le code de la méthode (qui est relativement court), les paramètres passés à cette méthode ou la valeur renvoyée par la méthode.
- Si la fonctionnalité n’est plus requise, je vide simplement la méthode SRP pertinente, encore une fois sans changer le client, la méthode de façade ou les autres méthodes SRP. Je ne supprime même pas l’appel de méthode de la méthode SRP car le changement est mon ennemi et moins je change de choses, moins j’ai de chance d’introduire un nouveau bogue.
Je peux même créer de nouvelles méthodes de façade : une méthode pour gérer les commandes numériques, par exemple, serait probablement utile s’il existe une variété de clients qui prennent en charge les commandes numériques.
Pour moi, c’est donc l’atteinte des objectifs de création de code maintenable qui m’importe, et non l’application d’un outil ou d’une technique spécifique. Les objectifs que je vise sont :
- Couplage desserré : remplacez ou ajoutez des composants sans, idéalement, aucun impact sur les autres composants
- Composants ciblés : les composants qui font bien une chose sont plus faciles à combiner avec d’autres composants pour offrir de nouvelles fonctionnalités
- Ajoutez de nouvelles fonctionnalités avec un nouveau code : la plupart des modifications impliquent de modifier un composant ou d’ajouter un nouveau composant. Je laisse la plupart de mon code de travail existant seul.
Je choisis le(s) modèle(s) que j’utiliserai parce qu’ils me rapprochent de ces objectifs. Le modèle de façade n’est pas inhabituel dans la réalisation de ces objectifs – la plupart des modèles de conception sont spécifiquement conçus pour prendre en charge des applications maintenables. La modèle de stratégie, par exemple, me permet de personnaliser le traitement de n’importe quelle méthode en transmettant une méthode spécialisée pour gérer les nouvelles demandes. Je peux ajouter étendre mon application simplement en écrivant une nouvelle méthode de stratégie.
Ce n’est pas un hasard si les applications qui répondent à ces objectifs sont généralement plus faciles à tester. Les modules faiblement couplés, par exemple, sont plus faciles à tester unitairement et à combiner dans des tests d’intégration ; les composants ciblés prennent en charge des tests plus simples ; si j’ajoute de nouvelles fonctionnalités avec un nouveau code, je peux créer de nouveaux tests pour ce nouveau code et laisser mes tests existants seuls.
Bien que j’aie déjà mentionné les outils de test, il existe également un certain nombre de pratiques et d’outils de codage que j’utilise pour créer une application maintenable. Par exemple, vous pouvez implémenter un couplage lâche à l’aide d’un outil d’injection de dépendances qui permet au code de sélectionner l’objet dont il a besoin dans un conteneur. Maintenant, si vous avez besoin de changer le comportement de votre code, il vous suffit de charger le référentiel avec un nouvel objet.
Mais vous devez tirer parti de plusieurs outils lors de la création d’une solution. Par exemple, les interfaces prennent en charge l’injection de dépendances en couplant de manière lâche l’API que votre code appelle à une implémentation particulière de votre fonctionnalité. Si vous envisagez d’utiliser le modèle de stratégie, vous constaterez probablement qu’il fonctionne encore mieux en conjonction avec le modèle de méthode d’usine. La plupart des modèles de conception supposent que vous utiliserez des interfaces, par exemple.
Autrement dit, il ne s’agit pas d’outils et de techniques, mais de la façon dont ils sont utilisés ensemble. Garder à l’esprit les trois objectifs des applications maintenables, ainsi qu’un outil de test qui me permet d’adapter mon code à mesure que ma solution évolue, me permet de prendre plus facilement des décisions sur la façon d’utiliser ces outils.
Nouvel ebook GRATUIT—Unit Testing Legacy Code: Effective Approaches
Source link