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

Arbre programmatique
Exemple d'un arbre programmatique pour schématiser un algorithme

L'infographie ci-dessus montre un exemple d'arbre programmatique simple où apparaissent plusieurs niveaux ainsi que des répétitives et une alternative.

Sens de lecture d'un Arbre programmatique
Sens de lecture d'un arbre programmatique
Attention ! Un arbre programmatique se lit toujours du haut vers le bas mais surtout de la droite vers la gauche.

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.