Meilleures pratiques de programmation défensive (intelligentes)

Si vous deviez choisir vos techniques favorites pour le codage défensif, quelles seraient-elles? Bien que mes langages actuels soient Java et Objective-C (avec un arrière-plan en C ++), n’hésitez pas à répondre dans n’importe quelle langue. L’accent serait mis ici sur des techniques de défense intelligentes autres que celles que 70% d’entre nous connaissent déjà. Alors maintenant, il est temps de creuser profondément dans votre sac de trucs.

En d’autres termes, essayez de penser à autre chose que cet exemple inintéressant :

  • if(5 == x) au lieu de if(x == 5) : pour éviter une affectation involontaire

Voici quelques exemples de pratiques insortinggantes de programmation défensive (des exemples spécifiques au langage sont en Java):

– Verrouillez vos variables jusqu’à ce que vous sachiez que vous devez les modifier

C’est-à-dire que vous pouvez déclarer toutes les variables final jusqu’à ce que vous sachiez que vous devrez le changer, à quel point vous pouvez supprimer la final . Un fait généralement inconnu est que ceci est également valable pour les parameters de méthode:

 public void foo(final int arg) { /* Stuff Here */ } 

– Quand quelque chose de mauvais arrive, laissez une trace de preuves derrière

Il y a un certain nombre de choses que vous pouvez faire lorsque vous avez une exception: il est évident que vous devez vous connecter et effectuer certains nettoyages. Mais vous pouvez également laisser des traces (par exemple, définir des variables sur des valeurs sentinelles telles que “UNABLE TO LOAD FILE” ou 99999 serait utile dans le débogueur, au cas où vous rencontreriez un bloc catch exception).

– En matière de cohérence: le diable est dans les détails

Soyez aussi cohérent avec les autres bibliothèques que vous utilisez. Par exemple, en Java, si vous créez une méthode qui extrait une plage de valeurs, la limite inférieure est incluse et la limite supérieure est exclusive . Cela le rend cohérent avec les méthodes comme Ssortingng.subssortingng(start, end) qui fonctionne de la même manière. Vous trouverez tous ces types de méthodes dans le JDK de Sun pour se comporter de cette manière, car il effectue diverses opérations, notamment l’itération d’éléments cohérents avec les tableaux, où les index vont de zéro ( inclus ) à la longueur du tableau ( exclusif ).

Alors, quelles sont vos pratiques défensives préférées?

Mise à jour: Si vous ne l’avez pas déjà fait, n’hésitez pas à intervenir. Je donne une chance à d’autres réponses avant de choisir la réponse officielle .

Dans c ++, j’ai aimé une fois redéfinir le nouveau afin de fournir une mémoire supplémentaire pour attraper les erreurs des poteaux de clôture.

Actuellement, je préfère éviter les programmes défensifs en faveur du développement piloté par les tests . Si vous détectez des erreurs rapidement et en externe, vous n’avez pas besoin de brouiller votre code avec des manœuvres défensives, votre code est DRY -er et vous vous retrouvez avec moins d’erreurs que vous devez défendre.

Comme WikiKnowledge A écrit :

Évitez la programmation défensive, plutôt rapide.

Par programmation défensive, j’entends l’habitude d’écrire du code qui tente de compenser certains échecs des données, d’écrire du code qui suppose que les appelants peuvent fournir des données non conformes au contrat entre l’appelant et la sous-routine. avec ça.

SQL

Quand je dois supprimer des données, j’écris

 select * --delete From mytable Where ... 

Quand je le lance, je saurai si j’ai oublié ou bâclé la clause where. J’ai une sécurité Si tout va bien, je mets tout en évidence après les jetons de commentaire ‘-‘ et lance-le.

Edit: si je supprime beaucoup de données, j’utiliserai count (*) au lieu de simplement *

Allouez un morceau de mémoire raisonnable au démarrage de l’application – je pense que Steve McConnell a parlé de parachute de mémoire dans Code Complete.

Cela peut être utilisé dans le cas où quelque chose de grave se passe mal et que vous êtes obligé de résilier.

En allouant cette mémoire à l’avance, vous disposez d’un filet de sécurité, car vous pouvez le libérer, puis utiliser la mémoire disponible pour effectuer les opérations suivantes:

  • Enregistrer toutes les données persistantes
  • Fermez tous les fichiers appropriés
  • Écrire des messages d’erreur dans un fichier journal
  • Présenter une erreur significative à l’utilisateur

Dans chaque instruction de commutateur qui n’a pas de cas par défaut, j’ajoute un cas qui annule le programme avec un message d’erreur.

 #define INVALID_SWITCH_VALUE 0 switch (x) { case 1: // ... break; case 2: // ... break; case 3: // ... break; default: assert(INVALID_SWITCH_VALUE); } 

Lorsque vous manipulez les différents états d’une énumération (C #):

 enum AccountType { Savings, Checking, MoneyMarket } 

Puis, à l’intérieur d’une routine …

 switch (accountType) { case AccountType.Checking: // do something case AccountType.Savings: // do something else case AccountType.MoneyMarket: // do some other thing default: --> Debug.Fail("Invalid account type."); } 

À un moment donné, j’appendai un autre type de compte à cette énumération. Et quand je le ferai, j’oublierai de corriger cette déclaration de changement. Donc, le Debug.Fail bloque horriblement (en mode Debug) pour attirer mon attention sur ce fait. Lorsque j’ajoute le case AccountType.MyNewAccountType: le crash horrible s’arrête … jusqu’à ce que j’ajoute encore un autre type de compte et oublie de mettre à jour les cas ici.

(Oui, le polymorphism est probablement meilleur ici, mais ce n’est qu’un exemple de ma tête).

Lors de l’impression de messages d’erreur avec une chaîne (en particulier celle qui dépend de la saisie de l’utilisateur), j’utilise toujours des guillemets simples '' . Par exemple:

 FILE *fp = fopen(filename, "r"); if(fp == NULL) { fprintf(stderr, "ERROR: Could not open file %s\n", filename); return false; } 

Ce manque de guillemets autour de %s est vraiment mauvais, car dire que nomfichier est une chaîne vide ou simplement un espace ou quelque chose. Le message imprimé serait bien sûr:

 ERROR: Could not open file 

Donc, toujours mieux de faire:

 fprintf(stderr, "ERROR: Could not open file '%s'\n", filename); 

Au moins, l’utilisateur voit cela:

 ERROR: Could not open file '' 

Je trouve que cela fait une énorme différence en termes de qualité des rapports de bogues soumis par les utilisateurs finaux. S’il y a un message d’erreur amusant comme celui-ci au lieu de quelque chose de générique, alors il est beaucoup plus probable de le copier / coller au lieu de simplement écrire “cela n’ouvrirait pas mes fichiers”.

Sécurité SQL

Avant d’écrire un code SQL qui modifiera les données, j’emballe tout cela dans une transaction annulée:

 BEGIN TRANSACTION -- LOTS OF SCARY SQL HERE LIKE -- DELETE FROM ORDER INNER JOIN SUBSCRIBER ON ORDER.SUBSCRIBER_ID = SUBSCRIBER.ID ROLLBACK TRANSACTION 

Cela vous empêche d’exécuter une mauvaise suppression / mise à jour de manière permanente. Et, vous pouvez exécuter le tout et vérifier le nombre d’enregistrements raisonnables ou append des SELECT entre votre SQL et la ROLLBACK TRANSACTION pour vous assurer que tout est correct.

Lorsque vous êtes tout à fait sûr que vous faites ce que vous attendiez, changez le ROLLBACK en COMMIT et exécutez-le pour de vrai.

Pour toutes les langues:

Réduisez la scope des variables au minimum requirejs. Évitez les variables qui sont fournies pour les transporter dans la prochaine instruction. Les variables qui n’existent pas sont des variables que vous n’avez pas besoin de comprendre et dont vous ne pouvez pas être tenu responsable. Utilisez Lambdas autant que possible pour la même raison.

En Java, en particulier avec les collections, utilisez l’API. Par conséquent, si votre méthode retourne le type List (par exemple), procédez comme suit:

 public List getList() { return Collections.unmodifiableList(list); } 

Ne laissez rien échapper à votre classe dont vous n’avez pas besoin!

En cas de doute, bombardez l’application!

Vérifiez chaque paramètre au début de chaque méthode (que ce soit explicitement en le codant vous-même ou en utilisant une programmation basée sur un contrat n’a aucune importance ici) et bombardez avec la bonne exception et / ou un message d’erreur significatif si pas rencontré.

Nous connaissons tous ces conditions préalables implicites lorsque nous écrivons le code , mais si elles ne sont pas vérifiées explicitement, nous créons des labyrinthes lorsque quelque chose ne va pas et que des stacks d’appels de méthodes séparent l’occurrence du symptôme et l’emplacement réel. où une condition préalable n’est pas remplie (= où le problème / bug est en réalité).

C #:

 ssortingng mySsortingng = null; if (mySsortingng.Equals("someValue")) // NullReferenceException... { } if ("someValue".Equals(mySsortingng)) // Just false... { } 

en Perl, tout le monde le fait

 use warnings; 

J’aime

 use warnings FATAL => 'all'; 

Cela provoque la mort du code pour tout avertissement du compilateur / runtime. Ceci est surtout utile pour capturer des chaînes non initialisées.

 use warnings FATAL => 'all'; ... my $ssortingng = getSsortingngVal(); # something bad happens; returns 'undef' print $ssortingng . "\n"; # code dies here 

Dans c # vérification de ssortingng.IsNullOrEmpty avant de faire des opérations sur la chaîne comme length, indexOf, mid etc

 public void SomeMethod(ssortingng mySsortingng) { if(!ssortingng.IsNullOrEmpty(mySsortingng)) // same as mySsortingng != null && mySsortingng != ssortingng.Empty { // Also implies that mySsortingng.Length == 0 //Do something with ssortingng } } 

[Modifier]
Maintenant, je peux aussi faire ce qui suit dans .NET 4.0, qui vérifie en outre si la valeur est juste blanche

 ssortingng.IsNullOrWhiteSpace(mySsortingng) 

En Java et en C #, atsortingbuez un nom significatif à chaque thread. Cela inclut les threads du pool de threads. Cela rend les vidages de stack beaucoup plus significatifs. Il faut un peu plus d’effort pour donner un nom significatif aux threads de pool même, mais si un pool de threads a un problème dans une application longue, je peux provoquer un vidage de la stack (vous connaissez SendSignal.exe , non? ), attraper les journaux, et sans avoir à interrompre un système en cours d’exécution, je peux dire quels sont les threads … peu importe. Impasse, fuyant, grandissant, quel que soit le problème.

Avec VB.NET, Option Explicit et Option Ssortingct sont activés par défaut pour Visual Studio.

C ++

 #define SAFE_DELETE(pPtr) { delete pPtr; pPtr = NULL; } #define SAFE_DELETE_ARRAY(pPtr) { delete [] pPtr; pPtr = NULL } 

puis remplacez tous vos appelsdelete pPtr ‘ et ‘ delete [] pPtr ‘ par SAFE_DELETE (pPtr) et SAFE_DELETE_ARRAY (pPtr)

Maintenant, par erreur, si vous utilisez le pointeur “pPtr” après l’avoir supprimé, vous obtiendrez une erreur “violation d’access”. Il est beaucoup plus facile de réparer que les corruptions de mémoire aléatoires.

Avec Java, il peut être utile d’utiliser le mot-clé assert, même si vous exécutez du code de production avec des assertions désactivées:

 private Object someHelperFunction(Object param) { assert param != null : "Param must be set by the client"; return blahBlah(param); } 

Même avec des assertions désactivées, au moins le code documente le fait que le paramètre devrait être défini quelque part. Notez qu’il s’agit d’une fonction d’assistance privée et non d’un membre d’une API publique. Cette méthode ne peut être appelée que par vous, il est donc possible de faire certaines hypothèses sur la manière dont elle sera utilisée. Pour les méthodes publiques, il est probablement préférable de lancer une véritable exception pour les entrées non valides.

Je n’ai pas trouvé le mot-clé readonly avant d’avoir trouvé ReSharper, mais je l’utilise maintenant instinctivement, en particulier pour les classes de service.

 readonly var prodSVC = new ProductService(); 

En Java, lorsque quelque chose se passe et que je ne sais pas pourquoi, je vais parfois utiliser Log4J comme ceci:

 if (some bad condition) { log.error("a bad thing happened", new Exception("Let's see how we got here")); } 

De cette façon, une trace de stack me montre comment je me suis retrouvé dans une situation inattendue, par exemple un verrou qui ne se déverrouille jamais, quelque chose de nul qui ne peut pas être nul, etc. Évidemment, si une véritable exception est lancée, je n’ai pas besoin de le faire. C’est à ce moment que je dois voir ce qui se passe dans le code de production sans déranger quoi que ce soit d’autre. Je ne veux pas lancer d’exception et je n’en ai pas attrapé une. Je veux juste qu’une trace de stack soit enregistrée avec un message approprié pour me signaler ce qui se passe.

C #

  • Vérifiez les valeurs non nulles pour les parameters de type de référence dans la méthode publique.
  • J’utilise beaucoup de choses sealed pour éviter de créer des dépendances où je n’en voulais pas. Permettre l’inheritance doit être fait explicitement et non par accident.

Si vous utilisez Visual C ++, utilisez le mot clé override chaque fois que vous remplacez la méthode d’une classe de base. De cette façon, si quelqu’un change la signature de la classe de base, cela provoquera une erreur de compilation plutôt qu’une mauvaise méthode appelée en silence. Cela m’aurait sauvé quelques fois s’il avait existé plus tôt.

Exemple:

 class Foo { virtual void DoSomething(); } class Bar: public Foo { void DoSomething() override { /* do something */ } } 

J’ai appris en Java à ne jamais attendre indéfiniment le délocking d’un verrou, sauf si je m’attendais vraiment à ce que cela prenne un temps indéfiniment long. Si, de manière réaliste, le verrou devrait se déverrouiller en quelques secondes, je n’attendrai plus que quelques instants. Si le verrou ne se déverrouille pas, je me plains et je vide la stack dans les journaux, et selon ce qui est le mieux pour la stabilité du système, continuez comme si le verrou déverrouillait ou continuez comme si le verrou ne déverrouillait jamais.

Cela a permis d’isoler quelques conditions de course et des conditions de pseudo-blocage qui étaient mystérieuses avant de commencer.

Lorsque vous émettez un message d’erreur, essayez au moins de fournir les mêmes informations que le programme lorsqu’il a pris la décision de lancer une erreur.

“Autorisation refusée” vous indique qu’il ya eu un problème de permission, mais vous n’avez aucune idée de la raison ou du lieu du problème. “Impossible d’écrire le journal des transactions / mon / fichier: le système de fichiers en lecture seule” vous permet au moins de connaître la base sur laquelle la décision a été prise, même si elle est erronée – surtout si elle est erronée: nom de fichier incorrect? ouvert mal? autre erreur inattendue? – et vous permet de savoir où vous étiez lorsque vous avez eu le problème.

En C #, utilisez le mot-clé as pour convertir.

 ssortingng a = (ssortingng)obj 

jettera une exception si obj n’est pas une chaîne

 ssortingng a = obj as ssortingng 

laissera une valeur nulle si obj n’est pas une chaîne

Vous devez toujours tenir compte de la valeur null, mais cela est généralement plus simple que de rechercher des exceptions. Parfois, vous voulez un comportement de type “cast ou blow up”, auquel cas (ssortingng)obj syntaxe (ssortingng)obj est préférée.

Dans mon propre code, je trouve que j’utilise la syntaxe as environ 75% du temps et (cast) syntaxe (cast) environ 25%.

Java

Le java api n’a pas de concept d’objects immuables, ce qui est mauvais! Final peut vous aider dans ce cas. Étiquetez chaque classe qui est immuable avec Final et préparez la classe en conséquence .

Parfois, il est utile d’utiliser Final sur les variables locales pour s’assurer qu’elles ne changent jamais leur valeur. I found this useful in ugly, but necessary loop constructs. Its just to easy to accidently reuse a variable even though it is mend to be a constant.

Use defense copying in your getters. Unless you return a primitive type or a immutable object make sure you copy the object to not violate encapsulation.

Never use clone, use a copy constructor .

Learn the contract between equals and hashCode. This is violated so often. The problem is it doesn’t affect your code in 99% of the cases. People overwrite equals, but don’t care about hashCode. There are instances in wich your code can break or behaves strange, eg use mutable objects as keys in a map.

Be prepared for any input, and any input you get that is unexpected, dump to logs. (Within reason. If you’re reading passwords from the user, don’t dump that to logs! And don’t log thousands of these sorts of messages to logs per second. Reason about the content and likelihood and frequency before you log it.)

I’m not just talking about user input validation. For example, if you are reading HTTP requests that you expect to contain XML, be prepared for other data formats. I was surprised to see HTML responses where I expected only XML — until I looked and saw that my request was going through a transparent proxy I was unaware of and that the customer claimed ignorance of — and the proxy timed out trying to complete the request. Thus the proxy returned an HTML error page to my client, confusing the heck out of the client that expected only XML data.

Thus, even when you think you control both ends of the wire, you can get unexpected data formats without any villainy being involved. Be prepared, code defensively, and provide diagnostic output in the case of unexpected input.

I try to use Design by Contract approach. It can be emulated run time by any language. Every language supports “assert”, but it’s easy and covenient to write a better implementation that let you manage the error in a more useful way.

In the Top 25 Most Dangerous Programming Errors the “Improper Input Validation” is the most dangerous mistake in the “Insecure Interaction Between Components” section.

Adding precondition asserts at the beginning of the methods is a good way to be sure that parameters are consistent. At the end of methods i write postconditions , that check that output is what’s inteded to be.

In order to implement invariants , I write a method in any class that checks “class consistence”, that should be called authomatically by precondition and postcondition macro.

I’m evaluating the Code Contract Library .

I forgot to write echo in PHP one too many times:

 < ?php $foo->bar->baz(); ?>  < ?php echo $foo->bar->baz(); ?> 

It would take me forever to try and figure out why ->baz() wasn’t returning anything when in fact I just wasn’t echoing it! :-S So I made an EchoMe class which could be wrapped around any value that should be echoed:

 < ?php class EchoMe { private $str; private $printed = false; function __construct($value) { $this->str = strval($value); } function __toSsortingng() { $this->printed = true; return $this->str; } function __destruct() { if($this->printed !== true) throw new Exception("Ssortingng '$this->str' was never printed"); } } 

And then for the development environment, I used an EchoMe to wrap things which should be printed:

 function baz() { $value = [...calculations...] if(DEBUG) return EchoMe($value); return $value; } 

Using that technique, the first example missing the echo would now throw an exception …

when getting a table from a dataset

 if( ds != null && ds.tables != null && dt.tables.Count > 0 && ds.tables[0] != null && ds.tables[0].Rows > 0 ) { //use the row; } 

C ++

When I type new, I must immediately type delete. Especially for arrays.

C #

Check for null before accessing properties, especially when using the Mediator pattern. Objects get passed (and then should be cast using as, as has already been noted), and then check against null. Even if you think it will not be null, check anyway. I’ve been surprised.