1. Notions de défense et de protection
1.7 Créer et lancer ses propres exceptions
2. Traitement d’un exemple de protections
Dans un autre cours nous avons vu une méthode de programmation défensive à l’aide des plans d’action dans une interface. Nous allons montrer comment on peut concevoir la programmation défensive sous un autre angle en protégeant directement le code à l’aide de la notion d’exception (semblable à celle du C++ ou d’Ada). L’objectif principal est d’améliorer la qualité de " robustesse " (définie par B.Meyer) d’un logiciel. L’utilisation des exceptions avec leur mécanisme intégré, autorise la construction rapide et néanmoins efficace de logiciels robustes. |
Rappellons au lecteur que la sécurité d'une application est susceptible d'être mise à mal par toute une série de facteurs :
les problèmes liés au matériel : par exemple la perte subite d'une connection à un port, un disque défectueux... les actions imprévues de l'utilisateur, entrainant par exemple une division par zéro...
1. Notions de défense et de protection
A l’occasion de la traduction Algorithme à langage évolué comme pascal, nous avons répertorié en plus des actions algorithmiques, des actions de sécurité et des actions ergonomiques qui doivent elles aussi être programmées.
La partie sécurité a déjà été abordée ailleurs, nous regroupons ici ce que nous devons connaître.
Pour obtenir une certaine "
robustesse " dans nos programmes nous savons déjà
que la sécurité doit porter au moins sur :
Toutefois les faiblesses dans
un logiciel pendant son exécution, peuvent survenir : lors des entrées-sorties,
lors de calculs mathématiques interdits (comme la division par zéro),
lors de fausses manoeuvres de la part de l’utilisateur, ou encore lorsque
la connexion à un périphérique est inopinément
interrompue.
La programmation défensive est plus une attitude de pensée et de comportement qu’une nouvelle méthode. Cette attitude consiste à prévoir que le logiciel sera soumis à des défaillances dues à certains paramètres externes ou internes et donc à prévoir une réponse adaptée à chaque type de situation.
La comparaison des coûts de différents ratios de productivité établie par B.Boehm, montre que l’exigence de fiabilité est l’une des plus chères (surcoût de 87%). Cette remarque pourrait en apparence nous conduire à croire que ce facteur est donc une affaire de spécialistes et ne constitue pas une préoccupation dans un discours d’initiation. Nous allons voir qu’il n’en est rien et qu’en fait notre méthode de travail et les outils que nous utilisons concourent à une attitude de programmation défensive sans que nous contraignions fortement notre pensée en ce sens.
Nous pensons enfin qu’il est
bon d’induire dans l’esprit du développeur en programmation visuelle
débutant des idées fondées sur des pratiques méthodiques
qui lui éviteront l’écueil de " l’art indiscipliné "
du logiciel, mais qui pourraient lui donner le goût de continuer dans
cette discipline.
1.1 Outils participant à la programmation défensive
Voici cinq secteurs d’activité
parmi ceux que nous connaissons, participant à une méthode
de programmation défensive.
La modularité
Le principe du découpage en modules restreint la propagation des effets perturbateurs sur d’autres modules à partir d’une erreur survenant dans un module donné. |
L’encapsulation
Associée à la modularité, elle protège les constituants internes d’un module (champs et méthodes). |
Les TAD (types abstraits)
En fournissant un outil de spécification des données avec des préconditions sur la validité des opérateurs, un TAD assure la description des vérifications de domaines. |
L’utilisation de méthodes
systématiques
Comme l’algorithmique, la programmation par la syntaxe, les machines abstraites, les générateurs d’automates,... ces outils encouragent l’étudiant à s’essayer à une programmation sûre. |
L’utilisation d’un RAD visuel
comme Delphi
Ce genre de système de développement apporte un certains nombres d’avantages en la matière :
|
A côté de cet
éventail d’outils intégrant en général la notion
de programmation défensive, il existe un outil spécifique dédié
à ce genre de programmation : les exceptions.
1.2 Rôle et mode d’action d’une exception
Réservé jusqu'à présent aux spécialistes en Ada, en C++ ou en Java, cet outil est mis dans le RAD Delphi à la portée du débutant. Comme son nom l’indique, une exception est chargée de signaler un comportement exceptionnel (mais prévu) d’une partie spécifique d’un logiciel. Dans les langages de programmation actuels, les exceptions font partie du langage lui-même. C’est le cas de Delphi qui intègre les exceptions comme une classe particulière : la classe Exception. Cette classe contient un nombre important de classes dérivées.
Dès qu’une erreur se
produit comme un manque de mémoire , un calcul impossible,
un fichier inexistant, un transtypage non valide,..., un objet de la classe adéquate dérivée
de la classe Exception est instancié. Nous dirons que
le logiciel " déclenche une exception ".
Exemple : soit une saisie
d’un entier dans un Tedit nommé Editsaisie.
![]() |
Objets visuels :
Editsaisie: TEdit; Actions : |
procedure TForm1.Button1Clic(Sender:
TObject); var n:integer; begin Editresultat.color:=clAqua; n:=StrtoInt(Editsaisie.text); Editresultat.color:=clyellow; Editresultat.text:= Editsaisie.text; Editsaisie.clear ; end; |
Delphi.Exceptions
\Exception01
Une exécution sans incident
donne ceci :
![]() avant clic sur Button1 |
![]() après clic sur Button1 |
Lors de l’exécution, supposons que nous entrons dans Editsaisie la chaîne " asa12324 " qui n’est pas un entier.
La fonction StrtoInt déclenche une exception et nous envoie un message d’erreur général sur l’incident qui vient de se produire :
après clic sur Button1
Message d’erreur :
![]() |
![]() |
Si nous voulons que le message soit plus explicite, ou si nous voulons par exemple que malgré tout une certaine partie du code s’exécute en fournissant des valeurs par défaut malgré l’incident, nous devons gérer nous-mêmes cette exception.
Afin de pouvoir gérer
une exception déclenchée, il nous faut disposer d’un gestionnaire
d’exception qui permette de traiter tous les types d’exception.
1.3 Gestion de la protection du code
Le langage Delphi contient
un mécanisme appelé gestionnaire d’exception qui s’insère
dans les lignes de code là où nous souhaitons assurer une protection.
Syntaxe du gestionnaire : mots clefs
(try, except)
try
<lignes de code à protéger> - ...
<lignes de code réagissant à l’exception> - ... |
Principe de fonctionnement d’un tel gestionnaire :
Dès qu’une exception est déclenchée dans le bloc de lignes compris entre try....except, il y a déroutement de l’exécution (arrêt d’exécution séquentielle du code) vers la première ligne du bloc except...end et l’exécution continue séquentiellement à partir de cet endroit.
Reprenons l’exemple précédent
légèrement modifié dans le code du gestionnaire du clic
de Button1.
procedure TForm1.Button1Clic(Sender:
TObject); var n:integer; begin Editresultat.color:=clAqua; n:=StrtoInt(Editsaisie.text); Editresultat.color:=clyellow; Editresultat.text:= inttostr(n); Editsaisie.clear ; end; |
Mettons en place une protection
de l’instruction incriminée, tout en conservant l’exécution
des lignes de code suivantes. Comme la variable " n " n’aura pas de valeur
à cause de l’incident, nous lui attribuons la valeur zéro par
défaut si un incident se produit.
procedure TForm1.Button1Clic(Sender:
TObject); var n:integer; begin try Editresultat.color:=clAqua; n:=StrtoInt(Editsaisie.text); except showmessage('tapez un entier (zéro par défaut !'); n:=0 end; Editresultat.color:=clyellow; Editresultat.text:= inttostr(n); Editsaisie.clear ; end; |
Delphi.Exceptions
\Exception02
1.3.1 -
Fonctionnement sans incident :
(Légende : la
flèche
indique l’exécution séquentielle de la ligne de code devant
laquelle elle est placée).
![]() |
![]() |
Toutes les lignes de code sont
exécutées séquentiellement sauf le bloc except...end
(qui n’est exécuté qu’en cas d’incident).
1.3.2 - Fonctionnement avec incident :
Nous entrons dans Editsaisie
comme précédemment une valeur non entière.
![]() |
![]() |
![]() |
Message envoyé par
la fonction Showmessage dans le bloc except...end.
Ensuite le code continue
à s’exécuter en séquence à partir de n :=0 ;...
|
![]() |
![]() |
En fait dans cet exemple, n’importe quelle exception
déclenchée dans le bloc try…except déroute vers
le bloc except…end.
1.4 Effets dus à la position du bloc except...end
Soit, dans le même exemple,
le nouveau code dans Button1Clmick où nous avons repoussé le
bloc except...end à la fin.
procedure TForm1.Button1Clic(Sender:
TObject); var n:integer; begin try Editresultat.color:=clAqua; n:=StrtoInt(Editsaisie.text); Editresultat.color:=clyellow; Editresultat.text:= inttostr(n); Editsaisie.clear ; except showmessage('tapez un entier (zéro par défaut !'); n:=0 end; end; |
Puis exécutons le programme.
Delphi.Exceptions
\Exception03
1.4.1 - Fonctionnement sans incident :
Identique au précédent
paragraphe, le bloc except...end étant ignoré.
1.4.2 -
Fonctionnement avec incident :
(Légende : la
flèche
indique l’exécution séquentielle de la ligne de code devant
laquelle elle est placée).
![]() |
![]() |
le code continue son déroulement
dans le bloc except...end en " sautant " trois instructions.
![]() |
![]() |
Etat final des résultats
après traitement de l’exception :
![]() |
les deux Tedit Editresultat
et Editsaisie n’ont pas changé puisque les instructions:
Editresultat.color:=clyellow;
n’ont pas été exécutées, par suite du déroutement du code.
|
Nous notons que cette méthode de déroutement du code est très proche du fonctionnement d’une machine de Von Neumann. Nous venons de voir comment intercepter une exception quelconque sans savoir exactement sa catégorie. Nous pouvons en fait intercepter une exception d’une manière encore plus " fine " avec le gestionnaire try...except...end.
Il nous permet de sélectionner
la classe exacte de l’exception et de ne faire fonctionner le déroutement
du code que pour une exception définie à l’avance.
1.5 Interception d’une exception d’une classe donnée
Diagrammes de syntaxe du gestionnaire :
<instruction
try> :
<Bloc
d’exception> :
<gestionnaire
d’exception> :
Les différentes classes d’exception Delphi :
Ci-dessous la hiérarchie des classes d’exception, qui dérivent toutes de la classe Exception:
Exemple d’écriture d'un gestionnaire :
try
On EConvertError do begin
On EintError do begin
else begin
end ; |
Principe de fonctionnement d’un tel gestionnaire :
Dès qu’une exception est déclenchée dans le bloc de lignes compris entre try....except, il y a déroutement de l’exécution (arrêt d’exécution séquentielle du code) vers le bloc except...end.
Le sélecteur de gestionnaire d’exception on...do fonctionne approximativement comme un case...of, en n’exécutant que le champ sélectionné.
Supposons que l’exception levée
soit de la classe EintError ; l’instruction try n’exécute alors que
le code du " bon " gestionnaire en l’occurrence :
On EintError do
begin <lignes de code réagissant à l’exception EintError > end ; |
1.6 Ordre dans l’interception d’une exception
A la différence d’un
" case " pascal, le choix du sélecteur de gestionnaire (on...do)
s’effectue séquentiellement dans l’ordre d’écriture des lignes
de code. On choisira donc, lorsqu’il y a une hiérarchie entre les
exceptions à intercepter, de placer le code de leurs gestionnaires
dans l’ordre inverse de la hiérarchie.
Exemple :division par zéro dans un calcul en virgule flottante
EMathError est la classe
des exceptions pour les erreurs de calcul.
EZeroDivide indique la
division par zéro dans une telle opération.
Programmons un calcul à partir d’un bouton :
![]() |
var x,y,z:real; begin try x:=StrtoFloat(Edit1.text); y:=StrtoFloat(Edit2.text); z:=x /y ; Edit3.text:=FloattoStr(z); except ............ ............ end; end; |
Observons ce qui se passe lorsque
nous interceptons les exceptions EMathError et EZeroDivide.
1.6.1 - Interception dans l’ordre de la hiérarchie :
La sélection d’exception est programmée dans l’ordre de la hiérarchie des classes :
EMathError
|____ EZeroDivide
![]() |
var x,y,z:real; begin try x:=StrtoFloat(Edit1.text); y:=StrtoFloat(Edit2.text); z:=x /y ; Edit3.text:=FloattoStr(z); except on EMathError do Edit3.text:='Erreur générale'; on EZeroDivide do Edit3.text:='division par zéro'; end; end; |
1.6.2 - Interception dans l’ordre inverse :
La sélection d’exception
est programmée dans l’ordre inverse de la hiérarchie des classes.
![]() |
var x,y,z:real; begin try x:=StrtoFloat(Edit1.text); y:=StrtoFloat(Edit2.text); z:=x /y ; Edit3.text:=FloattoStr(z); except on EZeroDivide do Edit3.text:='division par zéro'; on EMathError do Edit3.text:='Erreur générale'; end; end; |
1.7 Créer et lancer ses propres exceptions
Il est possible de construire de nouvelle classe d'exceptions personnalisées en héritant d'une des classes de Delphi mentionnées plus haut, ou à minima de la classe de base Exception.(cette classe contient une propriété public property Message: string, qui contient en type string le texte à afficher dans la boîte de dialogue des exceptions quand l'exception est déclenchée).
Il est aussi possible de lancer une exception personnalisée (comme si c'était une exception propre à Delphi) à n'importe quel endroit dans le code d'une méthode d'une classe en instanciant un objet d'exception personnalisé et en le préfixant du mot clef raise.
Le mécanisme général d'interception des exceptions à travers des gestionnaires d'exceptions try…except s'applique à tous les types d'exceptions y compris les exceptions personnalisées.
Création d'une nouvelle classe
![]()
Type
MonExcept = class (Exception)
End;
MonExcept hérite par construction de la propriété public Message de sa mère.
Lancer une MonExcept
![]()
Type
MonExcept = class (Exception)
End;
Procedure classA.Truc;
begin
…..
raise MonExcept.Create ( 'salut' );
…..
end;
intercepter une MonExcept
![]()
MonExcept = class (Exception)… end;
Procedure classB.Meth;
begin
…..
try….. // code à protéger
except on MonExcept do begin
….. // code de réaction à l'exception
end;
end;
end;
2. Traitement d’un exemple de protections
Reprenons comme base le premier exemple étudié dans le chapitre sur les interfaces. Supposons que nous voulons élargir notre interface de calcul aux opérations entières : Mutiplication, Division, Addition, Soustraction, Quotient, Reste.
Nous limiterons nos entiers
au type Smallint Delphi identique au type integer du pascal (Smallint
= -32768..32767, signé sur 16 bits).
Interface retenue :
Editnbr1: TEdit;
Editnbr2: TEdit; Editmessage: TEdit; Editresult: TEdit; ListBoxOperation: TListBox; ButtonCalcul: TBitBtn; Labeltypoperat: TLabel; |
2.1 Le code de départ de l’unité
var
Toper:array[0..5]of string; |
implementation
procedure TForm1.FormActivate(Sender:
TObject); begin Toper[0]:='*'; Toper[1]:='/'; Toper[2]:='+'; Toper[3]:='-'; Toper[4]:=' div '; Toper[5]:=' mod '; end; |
procedure TForm1.ListBoxOperationClic(Sender:
TObject); begin Labeltypoperat.caption:=Toper[ListBoxOperation.ItemIndex]; end; |
procedure TForm1.Editnbr1Change(Sender:
TObject); begin ButtonCalcul.enabled:=true; end; |
procedure TForm1.Editnbr2Change(Sender:
TObject); begin ButtonCalcul.enabled:=true; end; |
procedure TForm1.ButtonCalculClic(Sender:
TObject); var op1,op2,result:smallint; begin ButtonCalcul.enabled:=false; op1:=strtoint(Editnbr1.text); op2:=strtoint(Editnbr2.text); case ListBoxOperation.ItemIndex of 0: result:=op1 * op2; 1: result:=trunc(op1 / op2); 2: result:=op1 + op2; 3: result:=op1 - op2; 4: result:=op1 div op2; 5: result:=op1 mod op2; end; Editresult.text:=inttostr(result); Editmessage.text:=inttostr(op1)+Toper[ListBoxOperation.ItemIndex] +' '+inttostr(op2)+'= '+Editresult.text; end; |
A ce stade la saisie n’est
pas encore sécurisée. Afin que le lecteur puisse retrouver
les éléments déjà traités dans le chapitre
sur les interfaces, nous reprenons comme premier niveau de sécurité
le même genre de programmation : synchronisation des 3 objets de saisie
Editnbr1, Editnbr2 et ListBoxOperation, puis programmation par plans d’action.
var
oper1,oper2,operat:boolean;
//les 3 drapeaux
Toper:array[0..5]of
string;
implementation
procedure
RAZTout; begin with Form1 do begin ButtonCalcul.enabled:=false; Editnbr1.clear; Editnbr2.clear; Editresult.clear; Editmessage.clear; oper1:=false; oper2:=false; operat:=false; end end; |
procedure
TestEntrees; begin if oper1 and oper2 and operat then Form1.ButtonCalcul.enabled:=true end; |
procedure
TForm1.ListBoxOperationClic(Sender: TObject); begin operat:=true; TestEntrees; Labeltypoperat.caption:=Toper[ListBoxOperation.ItemIndex]; end; |
procedure
TForm1.Editnbr1Change(Sender: TObject); begin if Editnbr1.text<>'' then begin oper1:=true; TestEntrees; end else begin ButtonCalcul.enabled:=false; oper1:=false; // drapeau de Editnbr1 baissé end end; |
procedure
TForm1.Editnbr12Change(Sender: TObject); begin if Editnbr2.text<>'' then begin oper2:=true; TestEntrees; end else begin ButtonCalcul.enabled:=false; oper2:=false; // drapeau de Editnbr2 baissé end end; |
Nous proposons dans
le paragraphe qui suit, un complément de sécurité apporté
par des interceptions d’exception.
2.3 Code de la version.2 (deuxième niveau de sécurité)
Delphi.Exceptions
\V.2.AvecExceptions
Tout le code de la version.1
reste identique dans la version.2, les adjonctions sont uniquement dans le
gestionnaire du ButtonCalcul. Nous lançons les levées d’exception
lorsque les incidents ont lieu.
const Maxint=32767;
function signe(n:smallint):smallint;
|
procedure TForm1.ButtonCalculClic(Sender:
TObject); var op1,op2,result:smallint; begin ButtonCalcul.enabled:=false;
Editmessage.text:=inttostr(op1)+Toper[ListBoxOperation.ItemIndex] +' '+inttostr(op2)+'= '+Editresult.text; end; |
Le lecteur modifiera les choix
de valeurs par défaut dans les gestionaires d’exception. Dans l’exemple
plus haut, ces choix ne sont qu’indicatifs et servent à montrer que
l’on peut, soit arrêter un calcul, soit le continuer avec une valeur
de remplacement après signalement de l’erreur. Il s’assurera par
lui-même que la conjonction entre les plans d’action et les exceptions
est un système de programmation défensive efficace.