Sans (hors) crash de l'application, s'il vous plaît !
Publié: 2020-02-12Les programmeurs essaient d'éviter les plantages dans leur code. Si quelqu'un utilise son application, elle ne doit pas se casser ou se fermer de manière inattendue. C'est l'une des mesures de qualité les plus simples - si une application plante souvent, elle n'est probablement pas bien faite.
Les pannes d'application se produisent lorsqu'un programme est sur le point de faire quelque chose d'indéfini ou de mauvais, comme diviser une valeur par zéro ou accéder à des ressources restreintes sur une machine. Cela peut aussi être fait explicitement par le programmeur qui a écrit l'application. "Cela n'arrivera jamais, alors je vais le sauter" - c'est une pensée assez courante et pas complètement déraisonnable. Il y a des cas qui ne peuvent tout simplement pas se produire, jamais, jusqu'à ce que… cela se produise.
Promesses non tenues
L'un des cas les plus courants où nous savons que quelque chose ne peut pas arriver est celui des API. Nous avons convenu entre le backend et le frontend - c'est la seule réponse du serveur que vous pouvez obtenir pour cette requête. Les mainteneurs de cette bibliothèque ont documenté le comportement de cette fonction. La fonction ne peut rien faire d'autre. Les deux façons de penser sont correctes, mais les deux peuvent causer des problèmes.
Lorsque vous utilisez une bibliothèque, vous pouvez compter sur des outils linguistiques pour vous aider à gérer tous les cas possibles. Si le langage que vous utilisez manque de toute forme de vérification de type ou d'analyse statique, vous devez vous en occuper vous-même. Néanmoins, vous pouvez vérifier cela avant de l'expédier vers l'environnement de production, ce n'est donc pas un gros problème. Cela peut être difficile, mais vous lisez les journaux des modifications avant de mettre à jour vos dépendances et d'écrire des tests unitaires, n'est-ce pas ? Soit vous utilisez, soit vous créez une bibliothèque avec le typage le plus strict possible, le mieux pour votre code et les autres programmeurs.
La communication backend-frontend est un peu plus difficile. Il est souvent couplé de manière lâche, de sorte que le changement d'un côté peut être effectué facilement sans se rendre compte de la façon dont il affectera l'autre côté. Les changements sur le backend peuvent souvent briser vos hypothèses sur le frontend et les deux sont souvent distribués séparément. Ça doit mal finir. Nous ne sommes qu'humains et il arrive parfois que nous n'ayons pas compris l'autre côté ou que nous ayons oublié de lui parler de ce petit changement. Encore une fois, ce n'est pas un gros problème avec une bonne gestion du réseau - la réponse de décodage échouera et nous savons comment la gérer. Même le meilleur code de décodage peut cependant être affecté par une mauvaise conception…
Fonctions partielles. Mauvaise conception.
"Nous aurons ici deux variables booléennes : 'isActive' et 'canTransfer', bien sûr vous ne pouvez pas transférer lorsqu'il n'est pas actif, mais ce n'est qu'un détail." Là ça commence, notre mauvaise conception qui peut frapper fort. Maintenant, quelqu'un va créer une fonction avec ces deux arguments et traiter certaines données en fonction de cela. La solution la plus simple est… juste planter sur un état invalide, cela ne devrait jamais arriver donc nous ne devrions pas nous en soucier. Nous nous en soucions même parfois et laissons un commentaire pour le réparer plus tard ou pour demander ce qui devrait arriver, mais il peut éventuellement être expédié sans terminer cette tâche.
// pseudo-code function doTransfer(Bool isActive, Bool canTransfer) { Si ( isActive et canTransfer ) { // faire quelque chose pour le transfert disponible } else if ( not isActive et not canTransfer ) { // faire quelque chose pour le transfert non disponible } else if ( isActive et non canTransfer ) { // faire quelque chose pour le transfert non disponible } else { // alias (pas isActive et canTransfer ) // il y a quatre états possibles // cela ne devrait pas arriver, le transfert ne devrait pas être disponible lorsqu'il n'est pas actif crash() } }
Cet exemple peut sembler idiot, mais parfois vous pourriez vous prendre dans ce genre de piège qui est un peu plus difficile à repérer et à résoudre que celui-ci. Vous vous retrouverez avec quelque chose appelé une fonction partielle. Il s'agit d'une fonction qui n'est définie que pour certaines de ses entrées possibles en ignorant ou en plantant avec d'autres. Vous devez toujours éviter les fonctions partielles (veuillez noter que dans les langages à typage dynamique, la plupart des fonctions peuvent être traitées comme partielles). Si votre langage ne peut pas garantir un comportement correct avec la vérification de type et l'analyse statique, il peut se bloquer après un certain temps de manière inattendue. Le code évolue constamment et les hypothèses d'hier pourraient ne pas être valables aujourd'hui.
Échec rapide. Échoue souvent.
Comment pouvez-vous vous protéger? La meilleure défense est l'attaque ! Il y a ce joli dicton : « Échoue vite. Échoue souvent. Mais n'avons-nous pas simplement convenu que nous devrions éviter les plantages d'applications, les fonctions partielles et la mauvaise conception ? Erlang OTP donne aux programmeurs un avantage mythique qu'il se guérira après des états inattendus et se mettra à jour pendant l'exécution. Ils peuvent se le permettre, mais tout le monde n'a pas ce genre de luxe. Alors pourquoi devrions-nous échouer rapidement et souvent ?
Tout d'abord, pour trouver ces états et comportements inattendus . Si vous ne vérifiez pas si l'état de votre application est correct, cela pourrait conduire à des résultats encore pires qu'un plantage !
Deuxièmement, pour aider d'autres programmeurs à collaborer sur la même base de code . Si vous êtes seul dans un projet en ce moment, il se peut qu'il y ait quelqu'un d'autre après vous. Vous pourriez oublier certaines hypothèses et exigences. Il est assez courant de ne pas lire la documentation fournie jusqu'à ce que tout fonctionne ou de ne pas documenter du tout les méthodes et types internes. Dans cet état, quelqu'un appelle l'une des fonctions disponibles avec une valeur inattendue mais valide. Par exemple, disons que nous avons une fonction 'wait' qui prend n'importe quelle valeur entière et attend ce nombre de secondes. Et si quelqu'un lui passe '-17' ? S'il ne plante pas immédiatement après cela, cela peut entraîner des erreurs graves et des états invalides. Attend-il une éternité ou pas du tout ?

La partie la plus importante d'un crash intentionnel est de le faire avec grâce . Si vous plantez votre application, vous devez fournir des informations pour permettre un diagnostic. C'est assez facile lorsque vous utilisez un débogueur, mais vous devriez avoir un moyen de signaler les plantages d'application sans lui. Vous pouvez utiliser des systèmes de journalisation pour conserver ces informations entre les lancements d'applications ou les consulter en externe.
La deuxième partie la plus importante du plantage intentionnel est d'éviter cela dans l'environnement de production…
N'échouez pas. Déjà.
Vous enverrez éventuellement votre code. Vous ne pouvez pas le rendre parfait, il est souvent trop coûteux de ne serait-ce que penser à faire des garanties d'exactitude. Cependant, vous devez vous assurer qu'il ne se comporte pas mal ou ne plante pas. Comment pouvez-vous y parvenir puisque nous avons déjà décidé de planter rapidement et souvent ?
Une part importante des plantages intentionnels se produit uniquement dans des environnements de non -production. Vous devez utiliser des assertions supprimées dans les versions de production de votre application. Cela aidera pendant le développement et permettra de repérer les problèmes sans affecter les utilisateurs finaux. Cependant, il est toujours préférable de planter parfois pour éviter les états d'application invalides. Comment pouvons-nous y parvenir si nous avons déjà créé des fonctions partielles ?
Rendre les états indéfinis et invalides impossibles à représenter et revenir à des états valides sinon. Cela peut sembler facile, mais cela demande beaucoup de réflexion et de travail. Peu importe combien c'est, c'est toujours moins que de rechercher des bogues, de faire des correctifs temporaires et… d'ennuyer les utilisateurs. Cela rendra automatiquement certaines des fonctions partielles moins susceptibles de se produire.
// pseudo-code function doTransfer(State state) { interrupteur (état) { cas State.canTransfer { // faire quelque chose pour le transfert disponible } cas State.cannotTransfer { // faire quelque chose pour le transfert non disponible } cas State.notActive { // faire quelque chose pour le transfert non disponible } // Il est impossible de représenter le transfert disponible sans être actif // il n'y a que trois états possibles } }
Comment pouvez-vous rendre les états invalides impossibles ? Prenons deux des exemples précédents. Dans le cas de nos deux variables booléennes 'isActive' et 'canTransfer', nous pouvons les transformer en une seule énumération. Il représentera de manière exhaustive tous les états possibles et valides. Même dans ce cas, quelqu'un peut envoyer des variables indéfinies, mais c'est beaucoup plus facile à gérer. Ce sera une valeur invalide qui ne sera pas importée dans notre programme au lieu d'un état invalide passé à l'intérieur, ce qui rendra tout plus difficile.
Notre fonction d'attente peut également être améliorée dans les langages fortement typés. Nous pouvons lui faire utiliser uniquement des entiers non signés en entrée. Cela seul résoudra tous nos problèmes puisque les arguments invalides seront supprimés par le compilateur. Mais que se passe-t-il si votre langue n'a pas de types ? Nous avons quelques solutions possibles. Tout d'abord - juste crash, cette fonction est indéfinie pour les nombres négatifs et nous ne ferons pas de choses invalides ou indéfinies. Nous devrons en trouver une utilisation invalide lors des tests. Les tests unitaires (que nous devrions faire de toute façon) seront vraiment importants ici. Deuxièmement, cela peut être risqué, mais selon le contexte, cela peut être utile. Nous pouvons revenir à des valeurs valides en conservant l'assertion dans les versions hors production pour corriger les états non valides lorsque cela est possible. Ce n'est peut-être pas une bonne solution pour des fonctions comme celle-ci, mais si nous créons une valeur absolue d'entier à la place, nous éviterons les plantages de l'application. Selon le langage concret, il peut également être judicieux de lever/lever une erreur/exception à la place. Cela vaut peut-être la peine de revenir en arrière si possible, même lorsque l'utilisateur voit une erreur, c'est une bien meilleure expérience que de planter.
Prenons un autre exemple ici. Si l'état des données utilisateur dans votre application frontale est sur le point d'être invalide dans certains cas, il peut être préférable de forcer une déconnexion et d'obtenir à nouveau des données valides du serveur au lieu de planter. L'utilisateur peut être obligé de le faire de toute façon ou peut être pris dans une boucle de crash sans fin. Encore une fois, nous devrions affirmer et planter dans de telles situations dans des environnements de non-production, mais ne laissez pas vos utilisateurs être des testeurs externes.
Sommaire
Personne n'aime les applications en panne et instables. Nous n'aimons pas les fabriquer ou les utiliser. Échouer rapidement avec des assertions qui fournissent un diagnostic utile pendant le développement et les tests détectera beaucoup de problèmes tôt. Le repli sur les états valides en production rendra votre application beaucoup plus stable. Rendre les états invalides non représentables éliminera toute une classe de problèmes. Donnez-vous un peu plus de temps pour réfléchir avant le développement à la façon de supprimer et de se replier sur les états invalides et un peu plus pendant l'écriture pour inclure certaines assertions. Vous pouvez commencer à améliorer vos applications dès aujourd'hui !
Lire la suite:
- Conception par contrat
- Type de données algébrique