Defensive programming

Reading time15 min

En bref

Résumé de l’article

Dans ce cours, nous introduisons la programmation défensive, qui est une manière d’écrire vos programmes en anticipant les erreurs possibles des utilisateurs. L’objectif est de réduire le nombre de plantages possibles dus à une mauvaise utilisation, et de fournir des messages d’erreur utiles à l’utilisateur.

Soyons clairs dès maintenant, vous ne pourrez jamais imaginer toutes les erreurs possibles qu’un utilisateur peut faire ! Cependant, essayer de le faire aidera l’utilisateur à comprendre les problèmes courants et à adapter son comportement en conséquence.

« Une erreur courante que les gens font en essayant de concevoir quelque chose de complètement infaillible est de sous-estimer l’ingéniosité des idiots complets. » — Douglas Adams, Mostly Harmless

Points clés

  • La programmation défensive consiste à rendre votre code plus résilient face à des entrées et des conditions inattendues.

  • Elle se compose de deux problèmes principaux : identifier les situations problématiques et identifier les actions correctives.

  • Elle implique la validation des entrées, l’évitement des suppositions, l’utilisation de valeurs par défaut sûres, et la gestion élégante des erreurs.

Contenu de l’article

1 — Principes de la programmation défensive

1.1 — Qu’est-ce que c’est ?

La programmation défensive permet de développer des programmes capables de détecter des anomalies et de répondre de manière prédéterminée en supposant que des erreurs peuvent survenir et en se protégeant contre elles. Elle garantit qu’un logiciel continuera de fonctionner dans des circonstances imprévues.

Les pratiques de programmation défensive sont souvent utilisées là où une haute disponibilité, la sécurité ou la sûreté sont requises. Elles peuvent aussi être très utiles à des fins pédagogiques. Pensez par exemple à PyRat, qui est rempli d’assertions de programmation défensive. Vous êtes probablement plus satisfait d’un message disant « Invalid direction received in the turn function » que « The game has crashed ».

La programmation défensive est une approche visant à améliorer le logiciel et le code source en termes de :

  • Qualité globale – Réduire le nombre de bugs et de problèmes dans le logiciel.
  • Rendre le code source compréhensible – Pour qu’il puisse être accepté lors d’un audit de code.
  • Faire en sorte que le logiciel se comporte de manière prévisible – Malgré des entrées ou des actions utilisateur inattendues.

Cependant, une programmation trop défensive peut protéger contre des bugs qui ne surviendront jamais, ce qui entraîne des coûts d’exécution et de maintenance. Il est important de trouver un équilibre entre trop et pas assez.

Exemple

Faisons un parallèle avec la conduite prudente. Les conducteurs défensifs supposent que les autres conducteurs feront des erreurs et prennent des mesures pour se protéger. Les conducteurs défensifs anticipent les situations dangereuses et adaptent leurs habitudes de conduite pour éviter les accidents.

Lorsque vous écrivez votre code, vous faites de nombreuses suppositions sur l’état du programme et les données qu’il traite. Par exemple :

  • La valeur d’une variable est toujours dans une certaine plage.
  • Un fichier existe et peut être lu.
  • Une connexion réseau est toujours disponible.

La justesse de votre code dépend de la véracité de ces suppositions. Cependant, dans le monde réel, les choses peuvent mal tourner, par exemple :

  • Un utilisateur saisit des données invalides.
  • Un fichier est manquant.
  • Une connexion réseau est perdue.

Des suppositions erronées peuvent entraîner des erreurs d’exécution, qui peuvent faire planter votre programme ou produire des résultats incorrects.

1.2 — Programmation défensive vs. offensive

La programmation défensive est une manière d’écrire vos programmes en anticipant les erreurs possibles des utilisateurs. Ainsi, elle est l’opposée de la programmation offensive. La programmation offensive repose sur l’idée que c’est la responsabilité de ceux qui utilisent un service de vérifier les conditions d’utilisation de ce service.

La programmation défensive consiste en deux problèmes :

  • Identifier les situations problématiques (division par zéro, racine d’un nombre négatif, etc.) pour tout ou partie de l’application logicielle.
  • Identifier les actions correctives à mettre en œuvre en évitant systématiquement d’escalader le problème aux services appelants.

2 — Concepts clés de la programmation défensive

Elle peut être mise en œuvre par les quatre techniques suivantes :

  • Établir un contrôle sur la cohérence/consistance de l’état du système.
  • Gestion des erreurs.
  • Récupération d’erreur par continuation.
  • Établir des assertions.

La philosophie de la programmation défensive est d’être prudent à propos de :

  • Code développé par d’autres et par vous-même.
  • Entrées utilisateur (depuis une interface utilisateur, un fichier, un réseau, etc.).
  • Services externes (base de données, services web, etc.).
  • Tout ce qui est externe au code en général.

En d’autres termes, vous devez toujours être préparé au pire et vous assurer que votre code peut gérer n’importe quelle situation (ou du moins essayer de le faire). Voici quelques concepts clés pour défendre votre code :

  1. Validation des entrées – Supposer toujours que les entrées peuvent être invalides, malformées ou malveillantes. Il est donc crucial de valider toutes les entrées, en particulier celles provenant de sources externes telles que les entrées utilisateur, les données réseau et les fichiers.

  2. Éviter les suppositions – Ne supposez pas que quoi que ce soit, comme une entrée, un format de fichier ou l’état du système, sera toujours conforme aux attentes. Au contraire, supposez le pire et préparez-vous. Par exemple, plutôt que de supposer qu’un fichier existe, vérifiez-le toujours d’abord.

  3. Valeurs par défaut sûres – En cas d’échec ou de conditions inattendues, le système doit revenir à un état sûr plutôt que de continuer de manière non sécurisée ou dangereuse. Par exemple, si l’authentification utilisateur échoue, le système doit refuser l’accès par défaut.

  4. Utilisation défensive des exceptions – Utilisez les exceptions judicieusement, mais ne les ignorez pas (i.e., attraper une exception et ne rien faire). Attraper des exceptions doit s’accompagner d’une stratégie claire de gestion.

  5. Contrats de code – Un contrat de code définit les attentes concernant les entrées, sorties et changements d’état d’une fonction. Ceux-ci sont généralement décomposés en :

    • Préconditions – Conditions qui doivent être vraies avant l’exécution d’une fonction.
    • Postconditions – Conditions qui doivent être vraies après l’exécution d’une fonction.
    • Invariants – Conditions qui doivent toujours être vraies pour un objet ou système particulier. De nombreux langages modernes supportent les fonctionnalités de design-by-contract, qui rendent ces conditions explicites.
  6. Dégradation élégante et gestion des erreurs – Les programmes doivent se dégrader élégamment face à des entrées ou des échecs inattendus. Par exemple, au lieu de planter, un programme pourrait enregistrer l’erreur et fournir un mécanisme de secours.

  7. Éviter l’état global – L’utilisation excessive de variables globales ou d’état partagé mutable peut entraîner des bugs difficiles à tracer. Assurez-vous que les modifications dans une partie du programme n’affectent pas involontairement d’autres parties. Préférez l’état local ou un accès contrôlé aux ressources partagées (e.g., en utilisant des techniques de synchronisation appropriées dans les applications multithread).

  8. Assertions – Utilisez des assertions pour vérifier des conditions qui ne devraient jamais se produire dans des circonstances normales.

  9. Immutabilité – Les objets immuables sont plus faciles à raisonner car leur état ne peut pas changer une fois créés. Autant que possible, préférez les structures de données immuables pour réduire la probabilité d’effets secondaires non désirés. En Python, les tuples sont un exemple de structures immuables, tandis que les listes sont mutables.

  10. Séparation des préoccupations – Écrivez du code qui respecte le principe de responsabilité unique. Évitez qu’une fonction ou un module effectue plusieurs tâches. Cela rend le code plus facile à tester, déboguer et comprendre.

3 — Bonnes pratiques pour la programmation défensive

La programmation défensive implique plusieurs pratiques clés pour garantir un code robuste et résilient :

  • Considérez toujours les cas limites qui pourraient casser votre code, tels que des entrées vides, des valeurs maximales, et d’autres situations improbables mais possibles.

  • Utilisez des valeurs par défaut sûres en configurant vos options par défaut sur des paramètres sécurisés, comme un accès au moindre privilège en sécurité.

  • Limitez l’exposition en gardant les fonctions et variables internes aussi privées que possible, ce qui fait partie de l’encapsulation en programmation orientée objet (POO) et aide à limiter la portée des erreurs ou des mauvais usages.

  • Testez votre code minutieusement en écrivant des tests unitaires pour vérifier la fonctionnalité attendue et simuler des entrées invalides, des exceptions et des scénarios d’échec afin d’assurer des réponses appropriées.

  • Échouez tôt en détectant et en gérant les problèmes dès que possible dans le cycle de vie d’un programme, comme valider les entrées aux frontières de votre système.

  • Enfin, utilisez la vérification de type lorsque c’est applicable, en vous assurant que les types de données sont vérifiés à l’exécution pour les langages dynamiques ou en utilisant des outils de vérification statique des types pour détecter les erreurs durant le développement.

Pour aller plus loin

On dirait que cette section est vide !

Y a-t-il quelque chose que vous auriez aimé voir ici ? Faites-le nous savoir sur le serveur Discord ! Peut-être pouvons-nous l’ajouter rapidement. Sinon, cela nous aidera à améliorer le cours pour l’année prochaine !

Pour aller au-delà