Fermer

mars 29, 2022

Comment simplifier et accélérer le développement de futures fonctionnalités


Un guide essentiel sur la façon de surmonter la complexité et la charge lors de l'ajout de fonctionnalités à une application existante.

Vous n'avez jamais vraiment fini d'écrire un logiciel. Vous n'arrêtez pas, par exemple, d'ajouter de nouvelles fonctionnalités à votre application après l'avoir mise en production. Au lieu de cela, vous continuez à ajouter de nouvelles fonctionnalités à cette application tout au long de sa vie et, parce que c'est la réalité dans laquelle vous vivez, vous voulez savoir comment vous pouvez maintenir les coûts de ces changements (tempsetde l'argent) sous contrôle et ajoutez ces nouvelles fonctionnalités de manière fiable (c'est-à-dire sans introduire de nouveaux bugs ni réactiver les anciens).

Il existe un monde d'outils et de techniques pour vous aider à le faire, mais ils se répartissent généralement en quatre catégories :

  • Laissez le code de travail seul
  • Ajouter de nouvelles fonctionnalités en écrivant un nouveau code
  • Architecte d'extension
  • Testez tôt et testez souvent

Laissez le code de travail seul

Les deux vérités dans la vie d'un programmeur sont que :

  1. Le changement est votre ennemi : chaque changement a la possibilité d'introduire un nouveau bogue.
  2. Votre code existant fonctionne comme prévu.

Ces deux vérités conduisent à la première règle de programmation : ne modifiez pas le code de travail.

Cela ne signifie pas pour autant que vous ne pouvez pas ajouter de nouvelles fonctionnalités à vos applications existantes. Cela signifie que vous devez créer votre application afin de pouvoir ajouter de nouvelles fonctionnalités sans avoir à modifier le code en cours de fonctionnement. Les cinq principes contenus dans leAcronyme SOLIDE vous donner des instructions sur la façon d'écrire du code afin que vous puissiez ajouter de nouvelles fonctionnalités sans perturber ce code. Ces principes fonctionnent souvent ensemble.

Par exemple, le S dans SOLID représente leprincipe de responsabilité unique (SRP)qui va de pair avec le je (pour leprincipe de ségrégation des interfaces) pour réduire le besoin de modifier le code de travail existant.

Le SRP est souvent décrit de deux manières liées : "Chaque composant doit bien faire une chose" et "Il ne doit y avoir qu'une seule raison qui motive les modifications apportées à un module". Le principe de ségrégation d'interface recommande de créer des objets avec un petit nombre de membres dans son API publique – juste assez de membres, en fait, pour prendre en charge la responsabilité unique de la classe (et le D dans SOLID – leprincipe d'inversion de dépendancea quelque chose à dire sur ce que devraient être ces membres, comme vous le verrez).

En suivant les principes de responsabilité unique et de ségrégation des interfaces, les objets restent petits et ciblés. Plus important encore, cela signifie que de nouvelles fonctionnalités sont plus susceptibles d'être ajoutées en créant de nouveaux composants plutôt qu'en modifiant le code existant. Ces deux principes fonctionnent ensemble pour réduire le risque d'introduire des bogues dans le code de travail

La meilleure façon de permettre l'ajout de nouvelles fonctionnalités sans violer le principe de responsabilité unique est de suivre lesprincipe ouvert (le O dans SOLIDE). Le principe ouvert dit qu'il devrait être facile d'étendre les objets avec de nouvelles fonctionnalités et de le faire sans changer le code existant. (Le principe d'ouverture est souvent exprimé comme gardant un objet "ouvert à l'extension mais fermé à la modification".)

L'un des moyens les plus puissants d'étendre des objets dans la programmation orientée objet est l'héritage, qui vous permet d'étendre un objet en ajoutant une sous-classe tout en laissant la classe "de base" d'origine intacte. Cependant, il ne suffit pas d'éviter d'avoir à modifier des objets existants : vous souhaitez également éviter de modifier les clients qui utilisent ces objets.

Il est possible d'éviter de modifier des clients existants lorsque les hiérarchies d'objets suivent lesPrincipe de substituabilité de Liskov (le L dans SOLIDE). Le principe de substituabilité de Liskov dit qu'un client existant pour n'importe quelle classe de base devrait pouvoir utiliser n'importe quelle nouvelle sous-classe sans avoir à être réécrit. Le respect de ce principe lors de la mise en œuvre de l'héritage garantit que les clients existants continueront de fonctionner, sans changement, même lorsqu'ils interagissent avec de nouvelles sous-classes.

Le principe d'inversion de dépendance (le D dans SOLID) prend également en charge le déploiement des modifications sans affecter les clients existants et fonctionnels. Le principe d'inversion de dépendance indique que les membres exposés par l'API publique d'un objet doivent correspondre aux demandes du client plutôt que d'exposer comment l'objet fait son travail. Suivre le principe d'inversion des dépendances signifie que, comme l'interface de la classe est contrôlée par le client, cette interface reste stable lorsque vous modifiez la façon dont votre objet répond à ses nouvelles responsabilités.

Ajouter de nouvelles fonctionnalités en écrivant un nouveau code

Si vous n'allez pas modifier votre code existant, comment, alors, ajoutez-vous de nouvelles fonctionnalités ? La réponse est que vous ajoutez de nouvelles fonctionnalités en écrivant un nouveau code (et, grâce à l'application des principes SOLID, en laissant l'ancien code seul).

C'est là qu'interviennent les modèles de conception. Les modèles de conception fournissent des moyens de résoudre les problèmes courants liés à l'ajout de nouvelles fonctionnalités, soit sans modifier le code existant, soit en localisant les modifications dans un seul module. L'adoption de la plupart des modèles de conception nécessite de tirer parti à la fois de l'héritage et des interfaces – les deux sont des techniques permettant de faire ressembler différents (c'est-à-dire des objets nouveaux et anciens) afin qu'une application puisse les utiliser selon les besoins et sans changement.

En pratique, les modèles de conception ont tendance à favoriser la création de solutions par composition, c'est-à-dire l'assemblage de plusieurs classes d'objectifs à responsabilité unique afin d'accomplir une tâche. Cela met l'accent sur l'utilisation des interfaces plutôt que sur l'héritage, mais, plus important encore, vous permet d'ajouter de nouvelles fonctionnalités en incorporant de nouveaux clients dans la composition.

Un excellent exemple de la façon dont les modèles de conception vous permettent d'ajouter de nouvelles fonctionnalités sans perturber le code existant est lemotif décorateur . Le modèle de décorateur décrit comment encapsuler de nouvelles fonctionnalités autour d'un objet existant pour fournir de nouvelles fonctionnalités à cet objet sans modifier son code existant.

lemodèle d'état est un exemple de la façon dont les changements peuvent être localisés. Il n'est pas rare qu'une application doive tenir compte de l'état actuel de ses données lorsqu'elle effectue des modifications (par exemple, un client avec des commandes client en attente et de l'argent dû ne peut pas être supprimé). Plutôt que de disperser ces tests dans l'application, le modèle d'état le centralise dans un module et isole le code de chaque état dans des modules distincts.

Avec le modèle d'état, si une nouvelle fonctionnalité nécessite un nouvel état, vous pouvez ajouter la nouvelle fonctionnalité en ajoutant un nouveau module avec le code requis par le nouvel état. Vous devez également modifier le module existant qui teste et charge le bon module mais, même si vous ne pouvez pas éviter de modifier le code existant, vous pouvez minimiser les modifications requises.

Un élément clé des modèles de conception est qu'ils fournissent un lieu où l'expérience et les connaissances des programmeurs peuvent être rassemblées. Les modèles de conception sont accompagnés de conseils d'experts sur la manière dont ils peuvent être mis en œuvre de manière fiable.

Voici une liste de 23 modèles de conception que vous pouvez utiliser :https://www.dofactory.com/net/design-patterns.

Architecte pour l'extension

Tout cela est un bon conseil à suivre au sein d'une application. Lorsque les applications interagissent, un ensemble différent de principes s'applique. Le plus important de ces principes est que les systèmes doivent être « couplés de manière lâche » : deux parties d'un système qui doivent fonctionner ensemble doivent avoir le moins de rapport possible l'une avec l'autre, comme, par exemple, dans lemodèle de fournisseur de consommateur . Cela garantit que vous pouvez ajouter en toute sécurité de nouvelles fonctionnalités à votre application sans affecter d'autres parties de votre application.

Deux méthodes populaires pour implémenter le modèle de fournisseur consommateur sont les files d'attente et les événements. Avecfiles d'attente , une application écrit un message dans une file d'attente qui est lue par d'autres applications. Avec unsystème événementielune application déclenche un événement en transmettant des informations, et d'autres applications s'abonnent à l'événement.

Les solutions basées sur les événements et les messages ne sont pas si différentes (en fait, sous le capot, les systèmes basés sur les événements sont souvent basés sur des files d'attente de messages). Les files d'attente de messages ont tendance à être un meilleur choix lorsque la résilience est importante, tandis que les architectures pilotées par les événements sont préférées lorsque des délais d'exécution plus rapides sont nécessaires. La clé des deux architectures est que le producteur qui génère les événements/messages est presque complètement indépendant du consommateur qui les traite – la seule chose sur laquelle les deux applications doivent s'accorder est le format des informations transmises dans le message ou l'événement.

La création d'applications faiblement couplées vous amènera à adopter une "cohérence éventuelle". La cohérence éventuelle assouplit l'exigence selon laquelle tous les systèmes reflètent le même état des données en même temps. Vous l'avez probablement déjà vu en action : si vous consultez votre facture de carte de crédit en ligne, vous remarquerez peut-être que la soustraction de vos factures enregistrées du montant total de votre créditn'est-ce pas reflète votre crédit disponible—votre crédit disponible inclut certains frais qui n'ont pas encore été affichés dans votre liste de factures (mais qui apparaîtront « éventuellement »). La cohérence éventuelle vous permet d'ajouter des fonctionnalités à votre application sans avoir à être en phase avec les autres composants de votre application.

Testez tôt et testez souvent

Et, bien que tous ces outils et techniques vous aideront à ajouter de nouvelles fonctionnalités de manière fiable, aucun d'entre eux ne garantit que le résultat sera exempt de bogues. Vous avez donc besoin d'un moyen de vous assurer que votre nouvelle fonctionnalité n'a pas entraîné le comportement de vos fonctionnalités existantes… différemment. C'est le but des tests de régression : ré-exécuter des tests qui prouvent que les anciennes fonctionnalités fonctionnent toujours de la même manière qu'avant le changement.

Malheureusement, à mesure que votre application évolue et que vous ajoutez plus de fonctionnalités, de plus en plus de tests sont nécessaires pour prouver que votre application continue de se comporter « comme prévu ». Finalement, le temps nécessaire pour effectuer tous ces tests manuellement dépassera votre temps et votre budget disponibles.

Autrement dit, à moins que, lorsque vous ajoutiez des fonctionnalités, vous ajoutiez également des tests automatisés qui démontrent que la nouvelle fonctionnalité fonctionne comme spécifié. Avec les tests automatisés, vous pouvez toujours exécuter tous vos tests de régression. Tout au plus, vous n'aurez qu'à ajouter un nouvel ordinateur pour vos tests en cours afin que vos tests se terminent à temps.

En réduisant le coût d'exécution de vos tests, vous autorisez des tests plus fréquents. En développant vos tests au fur et à mesure que vous écrivez votre code, vous pouvez commencer à tester plus tôt. En testant tôt et plus fréquemment, si vous constatez qu'une nouvelle fonctionnalité a créé un problème, vous serez en mesure de résoudre le problème lorsque les coûts seront les plus bas.

Les tests automatisés ont un autre avantage : ils vous permettent de rembourser votre dette technique. Dans chaque application, vous vous rendrez compte, au fil du temps, qu'une partie de votre code existant n'est pas à la hauteur de votre norme actuelle. Vous avez, en d'autres termes, une « dette technique » qui, à terme, rendra difficile l'ajout de nouvelles fonctionnalités. En vous rappelant que le changement est votre ennemi, vous devez vous demander s'il vaut la peine d'améliorer ce code.

Cependant, l'existence de tests de régression automatisés supprime ce risque : vous pouvez améliorer le code pour le mettre aux normes et réexécuter vos tests de régression automatisés pour vous assurer que votre modification est vraiment bénigne. Si vous avez suivi les principes SOLID, implémenté des modèles de conception connus et architecturé vos systèmes pour prendre en charge l'extension, vous pourrez à la fois cibler vos tests et traquer vos bogues rapidement.

La réalité est que les ateliers informatiques passent plus de temps à étendre, améliorer et modifier les applications existantes qu'à en créer de nouvelles. L'application de tous ces principes, modèles, architectures et modèles de test vous permettra d'effectuer ces changements de manière fiable, dans les délais et dans les limites du budget. Et, en plus d'ajouter de nouvelles fonctionnalités, vous pourrez réellement améliorer votre code existant.




Source link