Programmation défensive

applications internet utilitaires

Ce site ne sera plus alimenté de contenu après août 2014. Tous les nouveaux articles seront redigés pour www.waitingforcode.com
Le contrôle sur le code ressemble souvent à la promenade avec son chien. Il suffit de lui relâcher un peu sa laisse afin qu'il soit capable de faire des dégâts (nous renverser, faire peur ou mordre quelqu'un). Dans le développement c'est pareil. Plus de liberté on laisse aux traitements, plus de possibilités qu'on rencontre des bugs. Une approche qui vise à limiter les dégâts causés par l'application s'appelle la programmation défensive.

Cette approche sera sujet de l'article. Au tout début on verra quelle est la définition de ce concept. Ensuite, dans la 2e et la 3e partie, on abordera deux traitements possibles pour la programmation défensive. A la fin on verra un bout code écrit selon le mode fail-fast et la programmation défensive.

Qu'est-ce que c'est defensive programming
Defensive programming (programmation défensive) s'explique par la volonté du développeur à tout contrôler dans l'application. En commençant par les données provenant de l'extérieur et en terminant sur les types d'erreur renvoyés. Le but de cette gestion consiste à limiter au maximum les erreurs du système liés principalement à l'usage imprévisible de ce dernier. Une autre destination de ce style de programmation repose sur la garantie du bon fonctionnement de l'application malgré ses évolutions futures. Autrement dit, grâce à la programmation défensive, le système continuera de fonctionner même si une nouvelle fonctionnalité viendra s'y greffer.

Dans le livre "The Pragmatic Programmer", la programmation défensive est comparée à une "programmation paranoïaque". Mais grâce à elle, l'application est protégée contre plusieurs erreurs humaines possibles, aussi bien du côté de l'auteur du côté que du côté des développeurs qui repasseront derrière.

Un autre point bénéfique de la programmation défensive est la facilité de faire du débuggage. Un bon système des logs sera toujours plus réactif qu'un utilisateur qui voudra remonter le problème. En plus, si l'on prévoit l'imprévu dans le code, on peut plus facilement le corriger, par exemple en sauvegardant dans les logs les paramètres incorrects.

Bien sûr, le contrôle du code a son prix qui s'explique avant tout par la dégradation de la lisibilité du code. Ce dernier devient alors de plus en plus verbeux car il faut soit vérifier les types des données, soit leurs valeurs. Il faut également faire attention à ne pas abuser de la programmation défensive. La vérification si un objet n'est pas null tout de suite après l'appel de son constructeur (du genre Person p = new Person(); // ... if (p != null) { p.setName("x"); }) peut faire plus du mal que du bien.

Look Before You Leap (LBYL)
Cette approche peut être traduit en "vérifie avant de sauter". Autrement dit, il s'agit de s'assurer que la variable travaillée possède une valeur correcte pour pouvoir être traitée. En Java l'exemple du LBYL pourrait être la vérification si un objet n'est pas null. En PHP il peut s'agir, par exemple, de vérifier si la valeur d'un champ <select />, transmise via la requête POST, est connue par le système. Dans les deux cas, la vérification devrait être suivi par 2 scénarios :
- erreur de validation : informer le développeur que la valeur est incorrecte pour que ce premier puisse le corriger ou rendre le code de l'application plus intelligent
- validation correcte : effectuer le scénario prévu pour la donnée

L'exemple du look before you leap pourrait se présenter de cette manière :

public void parsePrice(String price) {
Pattern pricePattern = "\\d+";
Matcher priceMatcher = pricePattern.match(price);
if (priceMatcher.find()) {
Integer price = new Integer(price);
}
}


It's Easier to Ask Forgiveness than Permission (EAFP)
L'approche "il est plus simple de demander le pardon que de demander la permission" permet d'omettre les vérifications sur les valeurs des variables. Autrement dit, on peut laisser le code tourner sans se préoccuper des valeurs qui sont traitées. Cette préoccupation est inutile car dans tous les cas, même en cas d'erreur, le système continuera de fonctionner. Seulement une partie de traitement ne sera pas réalisée à cause de sa captation par le block try{} catch{}.

L'exemple précédent, écrit selon la règle EAFP, pourrait se présenter de cette manière :

public void parsePrice(String price) {
try {
Integer price = new Integer(price);
} catch (NumberFormatException nfe) {
LOGGER.error("An error occured on converting String ("+price+") into Integer", nfe);
}
}


Par contre, le lancement des exceptions ne peut pas s'appliquer tout le temps. Par exemple, la gestion des NullPointerException avec le try {} catch{} peut être à l'origine de plusieurs bugs imprévus de l'application. En fait, la non-nullabilité d'un objet est souvent déterminant pour effectuer une action. Comme l'indique une des référence des développeurs Java, le livre "Effective Java 2nd Ed", les exceptions devraient être présentes uniquement pour gérer les événements exceptionnels. Le contrôle du flux ne devrait pas s'effectuer avec leur aide. Voici pourquoi :

Integer age;
try {
age = new Integer("s");
} catch (Exception e) {
LOGGER.error("An error occured on parsing Integer", e);
}
person.setAge(age);
person.save();

Dans ce cas, l'âge est un critère déterminant pour gérer toute sorte des réductions et de niveau d'accès à des parties différentes de l'application. S'il est null, comme dans l'exemple ci-dessus, on risque de mettre en danger toutes les parties sécurisées de l'application. La mise en danger est encore plus grande quand l'application est gérée par plusieurs développeurs dont tous n'ont pas la même maîtrise du code. Pour cet exemple soit viable, il faudrait alors faire une vérification sur l'âge avant la sauvegarde :

Integer age;
try {
age = new Integer("s");
} catch (Exception e) {
LOGGER.error("An error occured on parsing Integer", e);
}
if (age != null) {
person.setAge(age);
person.save();
}


Exemple de la programmation défensive en Java
Une tactique qu'on peut opposer à la programmation défensive est fail-fast approach (approche d'échouer vite). Son principe consiste à ne pas trop se préoccuper de contrôle sur le code et de laisser l'utilisateur à nous remonter des bugs. Une approche très problématique, surtout dans les cas de l'e-commerce, comme plus on peut le voir plus bas :

class Order{
public void passOrder(final CreditCard card, final String name){
card.pay(this);
}
}

class CreditCard{
private int nr;

public void setNr(final int nr){
this.nr=nr;
}

public int getNr(){
return this.nr;
}

public void pay(final Order order){
System.out.println(this.nr+" passes the order");
}
}

La classe Order représente la commande. La classe CreditCard symbolise la carte de crédit utilisé pour payer la commande. Voici ce qui se passe en mode fail-fast approach :

public class FailFastApproach{
public static void main(final String[] args){
final Order order=new Order();
int nr=0;
try{
nr=new Integer("SSS").intValue();
}
catch(final Exception e){
e.printStackTrace();
}
final CreditCard card=new CreditCard();
card.setNr(nr);
order.passOrder(card, "Jack");
}
}

Le résultat sera le suivant :

java.lang.NumberFormatException: For input string: "SSS"
at java.lang.NumberFormatException.forInputString(Unknown Source)
at java.lang.Integer.parseInt(Unknown Source)
at java.lang.Integer.(Unknown Source)
at FailFastApproach.main(FailFastApproach.java:6)
0 passes the order

Dans ce cas, ce sera probablement l'utilisateur qui va rapporter le dysfonctionnement d'un système. Mais il suffisait de vérifier la valeur de la carte pour pouvoir afficher le message d'erreur directement au client qui a introduit le mauvais numéro de carte. Voici comment se présente le même code programmé défensivement :

public class DefensiveProgramming{
public static void main(final String[] args){
final Order order=new Order();
int nr=0;
try{
nr=new Integer("SSS").intValue();
}
catch(final Exception e){
e.printStackTrace();
}
if(nr==0){
System.out.println("Credit card number can't be equal to 0. Please rectify this number.");
// send message to e-mail too : "Credit card number ("+nr+") is invalid."
}
else{
final CreditCard card=new CreditCard();
card.setNr(nr);
order.passOrder(card, "Jack");
}
}
}

Le message qui apparaîtra alors sera :

java.lang.NumberFormatException: For input string: "SSS"
at java.lang.NumberFormatException.forInputString(Unknown Source)
at java.lang.Integer.parseInt(Unknown Source)
at java.lang.Integer.(Unknown Source)
at DefensiveProgramming.main(DefensiveProgramming.java:6)
Credit card number can't be equal to 0. Please rectify this number.


On a bien vu les avantages de la programmation défensive. Le code est sécurisé contre les changements imprévus par le compilateur ou par le développeur. La programmation défensive assure également une meilleure pérennité du projet. Les nouveaux développeurs ont beaucoup plus de chances de faire moins d'erreurs que dans l'approche fail-fast. Le contrôle du code garantit également une meilleure réactivité à des bugs. Les développeurs n'attendent plus que les autres leur rapportent des bugs, mais peuvent maîtriser cet aspects tous seuls à 100% grâce à des systèmes des logs élaborés. Cependant, la mauvaise implémentation de la programmation défensive, peut faire vite oublier les avantages. Il suffit de cacher quelques exceptions en trop, sans contrôler la valeur, pour voir son application bugguer.
Bartosz KONIECZNY 16-06-2014 06:37 applications web
Moi

Développeur d'applications Internet et journaliste passionné par l'adjectif français. Un aigle polonais orienté vers la progression, volant très haut et écoutant du zouk après les matches du foot français.

Vous appréciez mon travail ?

Pour contribuer au développement de ce site, ou pour remercier pour des articles rédigés, vous pouvez faire un don.

Un conseil Zend Framework

Comment faciliter le travail entre notre application et l'Ajax ?

L'un des moyens est l'utilisation du ContextSwitch. Il permet de définir le type de réponse renvoyé.