Programmation défensive

Temps de lecture15 min

En bref

Résumé de l’article

Dans ce cours, nous introduisons la programmation défensive, qui est une façon d’écrire vos programmes qui anticipe 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 fera ! Cependant, essayer de le faire aidera l’utilisateur à comprendre les problèmes courants et à adapter son comportement en conséquence.

“Une erreur courante que font les gens quand ils essaient de concevoir quelque chose de complètement infaillible est de sous-estimer l’ingéniosité des parfaits imbéciles.” — Douglas Adams, Mostly Harmless

Points clés à retenir

  • La programmation défensive consiste à rendre votre code plus résistant aux entrées et conditions inattendues.

  • Elle consiste en deux problèmes principaux : identifier les situations problématiques et identifier les actions correctives.

  • Elle implique de valider les entrées, d’éviter les suppositions, d’utiliser des valeurs par défaut sûres, et de gérer les erreurs avec élégance.

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 faire des réponses prédéterminées en supposant que des erreurs peuvent survenir et en se protégeant contre elles. Elle assure qu’un logiciel continuera à fonctionner dans des circonstances imprévues.

Les pratiques de programmation défensive sont souvent utilisées là où une haute disponibilité, sécurité ou sûreté est requise. Elle peut aussi être très utile à des fins éducatives. Pensez à PyRat par exemple, qui est plein d’assertions de programmation défensive. Vous êtes probablement plus heureux avec un message qui dit “Direction invalide reçue dans la fonction turn” qu’avec “Le jeu a planté”.

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

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

Cependant, une programmation trop défensive peut protéger contre des bugs qui ne surviendront jamais, résultant en 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 conduire prudemment. 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 ajustent leurs habitudes de conduite pour éviter les accidents.

As you write your code, you make many assumptions about the state of the program and the data it processes. For example:

  • A variable’s value is always within a certain range.
  • A file exists and can be read.
  • A network connection is always available.

The correctness of your code depends on these assumptions being true. However, in the real world, things can go wrong, e.g.:

  • A user enters invalid data.
  • A file is missing.
  • A network connection is lost.

Faulty assumptions can lead to runtime errors, which can cause your program to crash or produce incorrect results.

1.2 — Defensive vs. offensive programming

Defensive programming is a way of writing your programs that anticipates possible user errors. Thus, it is the opposite of offensive programming. Offensive programming is based on the idea that it is the responsibility of those who use a service to verify the conditions of use of this service.

Defensive programming consists of two problems:

  • Identifying problem situations (division by zero, root of a negative number, etc.) for all or part of software application.
  • Identifying corrective actions to be implemented by systematically avoiding to escalate the problem to calling services.

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 du statut du système.
  • Gestion d’erreur.
  • Récupération d’erreur par continuation.
  • Établir des assertions.

La philosophie de la programmation défensive est d’être prudent vis-à-vis 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 devriez toujours être préparé au pire et vous assurer que votre code peut gérer toute situation (ou au moins essayer de l’être). Voici quelques concepts clés pour défendre votre code :

  1. Validation des entrées – Supposez toujours que les entrées peuvent être invalides, mal formées, ou malveillantes. Par conséquent, il est crucial de valider toutes les entrées, particulièrement celles provenant de sources externes comme l’entrée utilisateur, les données réseau, et les fichiers.

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

  3. Défauts de sécurité – En cas d’échec ou de conditions inattendues, le système devrait par défaut aller vers un état sûr plutôt que de procéder de manière non sûre ou non sécurisée. Par exemple, si l’authentification utilisateur échoue, le système devrait refuser l’accès par défaut.

  4. Utilisation défensive des exceptions – Utilisez les exceptions judicieusement, mais n’avalez pas les exceptions (c’est-à-dire, attraper une exception et ne rien faire). Attraper des exceptions devrait venir avec une stratégie de gestion claire.

  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 typiquement décomposés en :

    • Préconditions – Conditions qui doivent être vraies avant qu’une fonction soit exécutée.
    • Postconditions – Conditions qui doivent être vraies après qu’une fonction s’exécute.
    • Invariants – Conditions qui doivent toujours être vraies pour un objet ou système particulier. Beaucoup de langages de programmation modernes supportent les fonctionnalités de conception par contrat, qui rendent ces conditions explicites.
  6. Dégradation gracieuse et gestion d’erreur – Les programmes devraient se dégrader gracieusement quand confrontés à des entrées inattendues ou des échecs. Par exemple, au lieu de planter, un programme pourrait enregistrer l’erreur et fournir un mécanisme de repli.

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

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

  9. Immutabilité – Les objets immutables sont plus faciles à raisonner parce que leur état ne peut pas changer une fois créé. Quand c’est possible, préférez les structures de données immutables pour réduire la probabilité d’effets de bord non intentionnels. En Python, les tuples sont un exemple de structures de données immutables, alors que les listes sont mutables.

  10. Séparation des préoccupations – Écrivez du code qui adhère au 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 — Meilleures pratiques pour la programmation défensive

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

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

  • Utilisez des défauts sûrs en définissant vos défauts sur des options sécurisées, comme l’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 mauvaise utilisation.

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

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

  • Enfin, utilisez la vérification de type quand 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 de type statique pour détecter les erreurs pendant 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à