La méthode des arbres programmatiques par l'exemple
La méthode de programmation structurée
C'est souvent le terme de programmation structurée qui revient
quand on décrit cette méthode. Personnellement je
préfère l'appeler la méthode des arbres
programmatiques.
Cette méthode nous viens de l'américain Jackson qui a
publié un retentissant article intitulé « L'instruction GO TO considérée comme nuisible ».
Le Cobol met à disposition des programmeurs l'instruction GO TO
qui permet de se débrancher à un autre endroit du
programme. Mais, mal utilisée, c'est cette instruction qui rend
un programme complexe et désorganisé. La méthode
de Jackson préconise donc l'interdiction du GO TO. Alors comment
faire ?
Le programme est structuré en niveaux. Chaque niveau est
constitué d'un début, d'une fin, et d'une
répétitive (ou d'une alternative) qui se trouve entre le
début est la fin. Il est possible d'en avoir plusieurs mais le début et la fin sont uniques.
Le début n'est exécuté qu'une seule fois. La fin est
elle aussi exécutée une seule fois. La
répétitive se contente d'appeler le niveau
inférieur jusqu'à ce que la condition d'arrêt soit
vérifiée. Et tout se
passe dans des sous-programmes appelés par des PERFORM.
Pour ce qui concerne l'alternative c'est exactement la même chose. Un PERFORM d'une procédure est réalisé dans le cas où la condition est vraie et un PERFORM d'une autre procédure est effectué dans le cas contraire.
Construction de l'arbre programmatique
L'infographie ci-dessus montre un exemple d'arbre programmatique simple où apparaissent plusieurs niveaux ainsi que des répétitives et une alternative.
Cet aspect est très important car quand vous retournez l'arbre de 90 degrés dans le sens inverse des aiguilles d'une montre vous obtenez les blocs « DEBUT » au dessus des blocs « FIN », ce qui est tout naturel. Mais si vous dessinez votre arbre programmatique avec un sens de lecture de gauche à droite vous allez d'abord rencontrer les fins de niveau avant de rencontrer les débuts.
Remarquez aussi, chose étrange, que quand on retourne de 90° l'arbre programmatique apparait alors parfaitement la structure d'un programme tel que l'on pourrait l'écrire en langage C ou en Pascal... Intéressant, non ?
Exemple de construction d'un programme en programmation structurée
Soit un programme destiné à traiter
un fichier avec 3 niveaux de ruptures sur les zones suivantes :
zone1,
zone2, zone3.
Voici maintenant comment va être codée la procédure division correspondant à cet arbre :
*********************** DEBUT DU PROGRAMME *
**********************
* NIVEAU 1
PERFORM NIVEAU1.
* Fin du programme
STOP RUN.
*************************
* STRUCTURE DES NIVEAUX *
*************************
* NIVEAU 1
NIVEAU1.
PERFORM DEBUT1.
PERFORM TRT1 UNTIL condition1.
PERFORM FIN1.
* NIVEAU 2
TRT1.
PERFORM DEBUT2.
PERFORM TRT2 UNTIL condition2.
PERFORM FIN2.
* NIVEAU 3
TRT2.
PERFORM DEBUT3.
IF condition3
PERFORM TRTDETAIL
ELSE PERFORM TRTDETAIL-FAUX
END-IF.
PERFORM FIN3.
* NIVEAU DETAIL si condition vraie
TRTDETAIL.
PERFORM DEBUT-TRTDETAIL.
PERFORM TRAITEMENT.
PERFORM FIN-TRTDETAIL.
* NIVEAU DETAIL si condition fausse
TRTDETAIL-FAUX.
EXIT.
**************************
* DETAIL DES TRAITEMENTS *
**************************
DEBUT1.
* Initialisations de début de programme
...
* Ouverture des fichiers
...
FIN1.
* Traitement final (affichage des compteurs, édition des totaux,...)
...
* Fermeture des fichiers
...
DEBUT2.
* Initialisations de plus au niveau
...
FIN2.
...
DEBUT3.
* Initialisations de niveau 2
...
FIN3.
...
DEBUT-TRTDETAIL.
* Initialisations de niveau détail
...
TRAITEMENT.
* cumuls
...
* traitement détail pour chaque enregistrement
...
FIN-TRTDETAIL.
...
Comme ceci, la structure de notre programme semble très claire et également très simple. Nous avons déjà la trame. Il ne reste plus qu'à renseigner quelques conditions. En effet, "condition1", "condition2" et "condition3" sont encore très vagues. Il faut les remplacer par les véritables conditions.
Il reste également une chose importante à positionner dans cette trame : c'est le lecture des fichiers. En effet, si l'endroit où les fichiers sont ouverts et refermés semble très clair, il manque le principal : la lecture. Et n'oublions pas que nous devons traiter trois niveaux de ruptures. Il faut à un moment donné pouvoir déterminer s'il y a rupture ou non, c'est à dire changement d'un des critères sur lesquels nous sommes triés.
Pour les fichiers il faut travailler avec une lecture d'avance. On va
donc devoir déclarer deux fois l'enregistrement du fichier en
entrée.
LFICIN correspond
à
l’enregistrement lu en avance.
TFICIN correspond à l’enregistrement à traiter.
Les ruptures seront déterminées par comparaison des zones.
Rupture de premier
niveau si
Lzone1 différent de Tzone1. Elle entraine également
toutes les ruptures suivantes.
Rupture de second
niveau si
Lzone1 différent de Tzone1 ou si Lzone2 différent de
Tzone2. Elle entraine la rupture de niveau inférieur.
Rupture de 3° niveau si Lzone1 différent de Tzone1 ou si Lzone2 différent de Tzone2 ou si Lzone3 différent de Tzone3
Voici donc le programme qui prend forme à commencer par la case DEBUT1 :**************************
* DETAIL DES TRAITEMENTS *
**************************
DEBUT1.
* Initialisations de début de programme
MOVE ZERO TO COMPTEUR1 COMPTEUR2 COMPTEUR3 COMPTEURDET.
MOVE LOW-VALUE TO TFICIN.
...
* Ouverture des fichiers
OPEN FIC.
* Première lecture d'avance
READ FIC AT END
MOVE HIGH-VALUE TO ENR-FIC.
MOVE ENR-FIC TO LFICIN.
Et voici comment on va coder la condition1 :
PERFORM TRT1 UNTIL LFICIN NOT = HIGH-VALUE.
Si le fichier en entrée est vide on ne va pas exécuter le perform et on passera directement au pavé FIN1.
Si le fichier contient au moins un enregistrement on passe au niveau 2 et le premier pavé d'instruction performé sera DEBUT2.
DEBUT2 correspond au traitement qui concerne une rupture première de premier niveau, sur zone1. Si nous passons ici c'est que nous sommes en rupture première et c'est effectivement le cas pour le premier enregistrement du fichier. Nous allons donc initialiser le compteur qui servira à comptabiliser le nombre de zone1 différents rencontrés. Si zone1 est un code client, ça sera le compteur de clients.
DEBUT2.
* Initialisations de plus au niveau
ADD 1 TO COMPTEUR1.
MOVE 0 TO RUPT-DER-1.
...
On aura également à faire certainement d'autres initialisations, des cumuls de montants par exemple.
Le niveau 3 sera ensuite appelé tant que nous ne sommes pas en rupture dernière. Il faut donc penser à initialiser cette rupture dernière dans le paragraphe DEBUT2. C'est ce qui a été fait ci-dessus.
Voici donc comment sera codée la condition2 :
PERFORM TRT2 UNTIL RUPT-DER-1 NOT = 1.
Le niveau 3 va beaucoup ressembler au niveau 2.
DEBUT3 correspond au traitement qui concerne une rupture première de second niveau, sur zone2. Si nous passons ici c'est que nous sommes en rupture première et c'est effectivement le cas pour le premier enregistrement du fichier. Nous allons donc initialiser le compteur qui servira à comptabiliser le nombre de zone2 différents rencontrés. Si zone2 est un code rayon (alimentation, bazar, vaisselle, vestimentaire,...), ça sera le compteur de rayons différents commandés par le client.
DEBUT3.
* Initialisations de niveau 2
ADD 1 TO COMPTEUR2.
MOVE 0 TO RUPT-DER-2.
...
Pour gérer trois niveaux de rupture il faudrait un niveau supplémentaire qui n'est pas représenté sur le schéma. Mais ça serait le même principe que pour le niveau 2 et le niveau 3. Nous allons donc faire comme s'il n'y avait que deux niveaux de rupture pour ne pas allonger la page. On oublie le niveau "rayon" pour passer directement à l'article.
Sur le schéma, le niveau 3 ne fait pas appel à une répétitive mais à une alternative. Ca pourrait être quelque chose comme "l'article est-il en stock ?".
Voici donc comment sera codée la condition3 :
IF en-stock = "YES"
PERFORM TRTDETAIL
ELSE PERFORM TRTDETAIL-FAUX
END-IF.
On suppose que le pavé DEBUT3 effectue un contrôle du stock et positionne la variable "en-stock".
Dans DEBUT-TRTDETAIL nous ferons toutes les initialisations concernant un enregistrement de niveau détail. Le traitement unitaire sera effectué dans le paragraphe TRAITEMENT
Et c'est dans le paragraphe FIN-TRTDETAIL que sera effectuée la lecture suivante. Mais attention, il ne faut pas oublier de basculer la zone LFICIN dans TFINCIN avant de lire l'enregistrement suivant.
Voici à quoi va ressembler FIN-TRTDETAIL :
* lecture suivante
MOVE LFICIN TO TFICIN.
READ FIC AT END
MOVE HIGH-VALUE TO ENR-FIC.
MOVE ENR-FIC TO LFICIN.
Il faut également calculer les ruptures
IF LFICIN-zone1 NOT = TFICIN-zone1
MOVE 1 TO RUPT-DER-1
MOVE 1 TO RUPT-DER-2
MOVE 1 TO RUPT-DER-3
END-IF
IF LFICIN-zone1 = TFICIN-zone1
AND LFICIN-ZONE2 NOT = TFICIN-zone2
MOVE 1 TO RUPT-DER-2
MOVE 1 TO RUPT-DER-3
END-IF
IF LFICIN-zone1 = TFICIN-zone1
AND LFICIN-ZONE2 = TFICIN-zone2
AND LFICIN-ZONE3 NOT = TFICIN-zone3
MOVE 1 TO RUPT-DER-3
END-IF
Quand les ruptures seront positionnées, les boucles PERFORM s'arrêteront d'elles-mêmes et on passera dans les paragraphes FIN.
Quand la lecture arrivera à la fin du fichier, LFICIN sera égal à HIGH-VALUE et la première boucle s'arrêtera pour passer au traitement de fin de programme.
A partir du moment où on a lu l'enregistrement suivant, il faut travailler avec les zones qui proviennent de TFICIN alors qu'avant on utilisait celles de LFICIN.
Mon traitement contient une erreur car si un article n'est pas en stock on ne passera jamais dans le paragraphe qui effectue la lecture suivante. Mais cet exemple est juste pour vous montrer le principe de la programmation structurée en utilisant un arbre programmatique.
Il est également possible d'utiliser une autre technique qui a l'avantage de bien montrer la structure du programme dès le début de la procédure division. En voici un exemple :
♦initialisations
♦ première lecture
♦ PERFORM UNTIL finfichier ou erreur
Si rupture de niveau 1
PERFORM traitement-debut-rupt1
Finsi
Si rupture de niveau 2 et NOT erreur
PERFORM traitement-debut-rupt2
Finsi
Si rupture de niveau 3 et NOT erreur
PERFORM traitement-debut-rupt3
Finsi
Si NOT Erreur
PERFORM traitement-detail
Finsi
Si NOT Erreur
MOVE LFICIN TO TFICIN
PERFORM lecture-fichier
Finsi
Si rupture de niveau 3 et NOT erreur
PERFORM traitement-fin-rupt3
finsi
Si rupture de niveau 2 et NOT erreur
PERFORM traitement-fin-rupt2
finsi
Si rupture de niveau 1 et NOT erreur
PERFORM traitement-fin-rupt1
finsi
END-PERFORM
♦ si erreur
PERFORM traitement-erreur
finsi
♦ Traitement final :
o Ecriture compte rendu de traitement (compteurs)
o Fermeture des fichiers
o Display de fin de traitement
o STOP RUN. (ou GOBACK)
♦
DEBUT DES MODULES DE TRAITEMENT
♦traitement-debut-rupt1
♦traitement-debut-rupt2
♦traitement-debut-rupt3
♦traitement-detail
♦lecture-fichier
♦traitement-fin-rupt3
♦traitement-fin-rupt2
♦traitement-fin-rupt1
♦traitement-erreur
Problèmes posés par cette méthode :
- La description du fichier doit être déclarée 2 fois ce qui oblige à utiliser les syntaxes OF TFICIN ou IN TFICIN (programmation lourde)
- Le programme n’est pas séquentiel. On doit sans cesse faire des aller-retour entre le chapeau et les modules ce qui ne rend pas la prise en main du programme très aisée
- Respecter l’ordre séquentiel des modules sinon tout semble mélangé
- Ne pas appeler les modules principaux depuis un autre module. Les appels de modules doivent se faire depuis la structure chapeau qui se trouve au début du programme
- Risque de compliquer la cinématique en cas d’évolution et cette cinématique se trouve entièrement englobée à l’intérieur d’un PERFORM donc pas de point possible. Ca devient vite une usine à gaz avec une accumulation de IF ... END-IF
- La détermination des ruptures est compliquée et à faire 2 fois (pour les ruptures premières et les ruptures dernières). Gros risque d’erreur quand on commence à avoir plus de 5 niveaux de ruptures
Le plus gros inconvénient de la méthode des arbres programmatiques réside dans le fait d'avoir toujours à tenir à jour le dessin de l'arbre. Quand j'ai commencé mes études d'informatique on m'a toujours dit que l'outil principal de l'informaticien est la gomme et le crayon. Avec cette méthode on comprend pourquoi.
Avantages :
Programme modulaire avec une cinématique détaillée en début de programme
Pour résoudre le problème de la double déclaration, pour LFICIN on peut définir uniquement des FILLER et les seules zones Lzone1, Lzone2, Lzone3 au bon endroit de l’enregistrement.
Quand utiliser la méthode des arbres programmatiques ?
Comme vous l'avez peut-être compris, cette méthode est très pratique à partir du moment où vous avez dessiné l'arbre programmatique. Sans le dessin de l'arbre je peux vous assurer que votre programme sera aussi complexe qu'un programme écrit from scratch. Le gros inconvénient est de toujours avoir à se promener de haut en bas et en large et en travers quand vous lisez le programme.
Pour toute modification du programme il faut commencer par modifier l'arbre programmatique. C'est un repère visuel très pratique pour savoir à quel endroit positionner un traitement particulier. Si par exemple vous avez un nouveau total à faire il suffit de se poser la question concernant le niveau de cette nouvelle variable. Est-ce une totalisation de niveau client, de niveau rayon, de niveau article ? La réponse vous indiquera d'elle même à quel endroit positionner l'initialisation de la variable de cumul.
Une fois j'ai du utiliser cette méthode pour écrire un programme très complexe de statistiques qui manipulait des tableaux à 5 dimensions. Et oui, j'ai bien écrit 5. En cobol, les tableaux ne pouvaient faire que 3 dimensions à cette époque. J'ai donc travaillé par « feuilles » avec un premier tableau à 3 dimensions. Quand j'arrivais à la quatrième dimension je prenais la « feuille » (cellule du tableau) pour la déplacer dans un second tableau intermédiaire de 2 dimensions. Pour les deux derniers niveaux de calculs je travaillais dans ce tableau et lors du traitement final de niveau 4 il fallait remettre la « feuille » à sa place dans le premier tableau à 3 dimensions. Ce traitement s'adaptait parfaitement à la programmation structurée en niveaux.