Les routines

Définitions

La routine

On appel routine (ou sous-routine ou sous-programme) permet d’encapsuler une portion de code (ou série d’instructions) effectuant un travail bien précis. La routine est identifiée et peut être exécutée une multitude de fois à plusieurs endroits dans le code d’un programme.

Appel de routine

Lorsqu’une première routine exécute une autre routine, on dit que la première routine effectue un appel de routine (ou que la première routine appelle l’autre routine.

Par exemple, si j’ai la routine A et la routine B; je peux dire que A appel B si al routine A lance l’exécution de la routine B.

Également, lorsqu’une routine A appel une routine B, on dit que la routine A est la routine appelante et que la routine B est la routine appelée.

public void maRoutineA(){
    ...
    maRoutineB();
    ...
}

public void maRoutineB(){
    ...
}

Les variables locales

Les routines peuvent utiliser des variables locales afin de lui permettre d’effectuer un travail plus complexe et d’avoir un code plus propre. Il est par contre important de noter que ces variables locales ne sont accessibles qu’à l’intérieur de la routine et les valeurs placées dans ces variables locales ne restent pas sauvegardées entre les différents appels de routine. En d’autres mots, ces variables sont créées au début de l’exécution d’une routine et sont détruites à la fin de l’exécution de cette dernière. Par exemple:

public void salutation() {
    Scanner lScanner = new Scanner(System.in);
    String lPrenom, lNom;
    System.out.print("Veuillez entrer votre prenom: ");
    lPrenom = lScanner.nextLine();
    System.out.print("Veuillez entrer votre nom: ");
    lNom = lScanner.nextLine();
    System.out.println("Bonjour " + lPrenom + " " + lNom);
}

Dans cet exemple, la routine « salutation » possède 3 variables locales: « lScanner », « lPrenom » et « lNom ».

Les arguments

Il est parfois nécessaire, pour une routine, d’avoir accès à certaines valeurs afin de bien faire le travail qu’elle doit exécuter. Afin que la routine appelante puisse transmettre ces valeurs vers la routine appelée, on utilise généralement des arguments. La routine appelée doit spécifier les arguments qu’elle nécessite dans sa signature et la routine appelée doit passer ces arguments dans son appel de routine.

public void salutation(String aPrenom, String aNom){
    System.out.println("Bonjour " + aPrenom + " " + aNom);
}

public void programme(){
    Scanner lScanner = new Scanner(System.in);
    String lPrenom, lNom;
    System.out.print("Entrez votre prenom: ");
    lPrenom = lScanner.nextLine();
    System.out.print("Entrez votre nom: ");
    lNom = lScanner.nextLine();
    salutation(lPrenom, lNom);
}

Dans cet exemple, la routine « programme » appel la routine « salutation » avec deux (2) arguments: le prénom (aPrenom) et le nom (aNom).

Valeur de retour

Il est possible que nous veuillions qu’une routine appelée retourne une valeur à une routine appelante. Il y a plusieurs manières pour faire ce type de retour, mais la plus utilisée (et celle qui devrait être privilégiée) est la valeur de retour.

Pour spécifier une valeur de retour, il faut débuter par spécifier le type de cette valeur dans la signature de la routine. Par exemple:

public String maRoutine()

Dans cette routine, maRoutine retourne une valeur de type « String » aux routines appelantes.

Pour retourner une valeur en particulier, il faut utiliser l’instruction return. Afin de s’assurer de ne pas briser de structure de contrôle, il est une bonne pratique de mettre l’instruction return une seule fois dans une routine, à la dernière ligne de la routine.

public int somme(List<Integer> aListe) {
    int lResultat = 0;
    for (Integer element : aListe) {
        lResultat = lResultat + element.intValue();
    }
    return lResultat;
}

Modification sur place d’argument

Une routine appelée peut également utiliser un autre mécanisme pour retourner une valeur à la routine appelante. La routine appelée peut modifier un des arguments. On appelle ce mécanisme modification sur place d’argument. Par exemple:

public void versMajuscule(List<String> aListe) {
    int i;
    for (i = 0; i < aListe.size(); i = i + 1) {
        aListe.set(i, aListe.get(i).toUpperCase());
    }
}

On voit que la méthode « versMajuscule » modifie directement l’argument « aListe » en effectuant des « set ».

Par contre, pour effectuer ce mécanisme, nous ne pouvons pas assigner l’argument avec un nouvel objet. En d’autres mots, vous ne devez pas faire de « = ». Par exemple, l’exemple suivant ne fonctionnera pas:

public void versMajuscule(List<String> aListe) {
    ArrayList<String> lListe = new ArrayList<String>(aListe.size());
    for (String element: aListe) {
        lListe.add(element.toUpperCase());
    }
    aListe = lListe;
}

Pour comprendre la raison pour laquelle cette modification ne fonctionne pas, il faut comprendre comment un argument fonctionne. Voici un schéma représentant un passage d’argument lors de l’appel d’une routine.

On voit que chaque routine stock chacun une copie du pointeur qui pointe vers le même objet en mémoire. En effet, si une modification sur place est faite sur a liste, puisqu’il s’agit de la même liste, la modification s’effectue pour les deux (2) routines. Par contre, lorsque nous créons une autre liste et que nous faisons une assignation « = », voici ce que devient le schéma.

On voit qu’une nouvelle liste a bel et bien été créée, mais seule la routine appelée y a accès. La routine appelante n’a aucun accès à cette nouvelle liste et son pointeur pointe toujours vers la première liste.

Les effets de bord

On a un effet de bord lorsqu’une routine modifie l’état du système (par système, j’entends le programme au sens large). La meilleure façon de savoir si une routine fait un effet de bord, c’est de regarder si tout est identique dans le programme entre avant l’appel de la routine et après l’appel de la routine.

Il y a différents effets de bord:

    • Effectuer une entrée ou sortie:
      • « Print »,
      • « Scanner »,
      • Valeur dans une fenêtre,
      • Création, modification, suppression de fichiers,
      • Modification de base de données,
      • etc.
    • Modifier une valeur de:
      • Variable globale,
      • Attribut,
      • Argument de la méthode (modification sur place),
      • etc.

Voici quelques exemples:

public void salutation(String aPrenom, String aNom){
    System.out.println("Bonjour " + aPrenom + " " + aNom);
}

La méthode « salutation » de l’exemple précédent effectue un effet de bord puisqu’il inscrit un texte dans la console, ce qui change l’état du programme en cours.

public String lireChaine(){
    Scanner lScanner = new Scanner(System.in);
    return lScanner.nextLine();
}

La méthode « lireChaine » de l’exemple précédent effectue un effet de bord puisqu’il lit de l’information au clavier, ce qui change l’état du programme en cours.

public void versMajuscule(List<String> aListe) {
    int i;
    for (i = 0; i < aListe.size(); i = i + 1) {
        aListe.set(i, aListe.get(i).toUpperCase());
    }
}

La routine « versMajuscule » ci-dessus effectue un effet de bord puisqu’elle modifie le contenu de la liste reçue en argument.

Voici maintenant un exemple sans effet de bord:

public int moyenne(List<Integer> aListe) {
    int lSomme = 0;
    for (Integer element : aListe) {
        lSomme = lSomme + element.intValue();
    }
    return lSomme = lSomme / aListe.size();
}

La routine « moyenne » dans cet exemple n’a pas d’effet de bord puisqu’elle ne modifie aucune valeur autre que dans ses propres variables locales et ne fait aucune entrée/sortie.

Les différents types de routines

Le terme routine est un terme très général. Il y a deux types différents de routine qui ont des utilités assez différentes. On les appelle les procédures et les fonctions. Il est très important de bien comprendre la différence entre ces deux types de routines, car ils seront utilisés différemment dans un contexte objet.

Les procédures

Une procédure est une routine qui ne retourne aucune valeur à l’utilisateur. Ce type de routine est généralement utilisé afin d’effectuer des effets de bord.

En java, une procédure est une routine dont la valeur de retour est « void ».

Les fonctions

Une fonction est une routine qui retourne une valeur à la routine appelante par la mécanique de la valeur de retour. Ce type de routine sert généralement à calculer une valeur ou bien à retourner une valeur n’étant pas accessible par la routine appelante.

Les fonctions pures

On dit qu’une fonction est pure dans le cas ou elle ne cause pas d’effet de bord. En général, lorsque nous créons des fonctions, nous essayons au maximum de créer des fonctions pures. Lorsque ce n’est pas possible ou qu’il y a un gros avantage à faire une fonction qui n’est pas pure, il est important de bien spécifier dans la documentation de la fonction qu’il y a un effet de bord ainsi que la portée de cet effet.

Récursivité

Certaines routines peuvent s’appeler elles-mêmes afin d’effectuer leurs travaux. On appelle ces routines des routines récursives.

En général, les routines récursives sont construites de la manière suivante:

    • Base:
      • La valeur la plus minimaliste.
    • Récursion:
      • Le calcul de la prochaine valeur

Par exemple, on définit la factorielle d’un « nombre » comme étant le produit de toutes les valeurs précèdent le « nombre ».

Par exemple, la factorielle de 10 = 10 * 9 * 8 * 7 * 6 * 5 * 4 * 3 * 2 * 1 = 3628800

Voici le code Java de la méthode « factorielle » qui calcul la factorielle d’un nombre:

public int factorielle(int aValeur) {
    int lResultat;
    if (aValeur == 0) {
        lResultat = 1;
    } else {
        lResultat = aValeur * factorielle(aValeur - 1);
    }
    return lResultat;
}

On voit que, selon l’algorithme, la base est Factorielle(0) qui donne 1; Ensuite, Factorielle(1) = 1 * Factorielle(0) = 1; Ensuite, Factorielle(2) = 2 * Factorielle(1) = 2; etc.

Gestion des erreurs

Lorsqu’une routine est créée, il est important de bien spécifier le travail, la portée et les limites de cette routine. En effet, il faut éviter de créer des routines qui gèrent trop de choses et se limiter au travail que cette dernière est censée faire. Dans le cas où l’état de l’appel de routine est problématique (par exemple arguments ou attributs des objets non valides), la routine ne doit pas se terminer normalement. Une exception doit être lancée afin d’informer la routine appelante de la problématique.

Les exceptions

Si une routine lance une exception, il faut que cette exception soit déclarée dans la signature de la routine. On spécifie cette exception en ajoutant le mot clé throw, suivie du type d’Exception à la signature de la routine. Également, pour lancer une exception dans le code, il faut utiliser l’instruction throw en y spécifiant un objet de type Exception. Voici un exemple:

/**
 * Additionne les éléments de `aListe1` et de `aListe2`.
 *
 * Noter que les deux listes doivent être de la même taille.
 * @param aListe1 La première liste à additionner
 * @param aListe2 La seconde liste à additionner
 * @return Liste contenant la somme des deux listes.
 * @exception Exception Les listes ne sont pas de la même taille.
 **/
public List<Integer> additionne(List<Integer> aListe1,
        List<Integer> aListe2) throws Exception {
    ArrayList<Integer> lResultat = new ArrayList<Integer>(aListe1.size());
    int i;
    if (aListe1.size() == aListe2.size()) {
        for (i = 0; i < aListe1.size(); i = i + 1) {
            lResultat.add(Integer.valueOf(aListe1.get(i).intValue() +
                        aListe2.get(i).intValue()));
        }
    } else {
        throw new Exception("Les listes doivent être de la même taille");
    }
    return lResultat;
}

On voit que cette méthode doit recevoir deux (2) listes contenant le même nombre d’éléments. Dans le cas où ce ne serait pas le cas, la routine lance une exception de type Exception. Lorsque nous verrons l’héritage, nous verrons que nous pouvons créer des types plus précis d’exceptions. Pour l’instant, vous pouvez créer toutes vos exceptions en instanciant la classe Exception, comme dans l’exemple.

Il est également à noter que l’instruction throw termine directement l’exécution de la routine. Malgré cela, il est une bonne pratique de vous assurer de respecter une bonne logique au niveau de vos structures de contrôle. Par exemple, l’exemple précédent est correct, mais la forme ci-dessous ne devrait pas être utilisée:

/**
 * Additionne les éléments de `aListe1` et de `aListe2`.
 *
 * Noter que les deux listes doivent être de la même taille.
 * @param aListe1 La première liste à additionner
 * @param aListe2 La seconde liste à additionner
 * @return Liste contenant la somme des deux listes.
 * @exception Exception Les listes ne sont pas de la même taille.
 **/
public List<Integer> additionne(List<Integer> aListe1,
        List<Integer> aListe2) throws Exception {
    ArrayList<Integer> lResultat = new ArrayList<Integer>(aListe1.size());
    int i;
    if (aListe1.size() != aListe2.size()) {
        throw new Exception("Les listes doivent être de la même taille");
    }
    for (i = 0; i < aListe1.size(); i = i + 1) {
        lResultat.add(Integer.valueOf(aListe1.get(i).intValue() +
                    aListe2.get(i).intValue()));
    }
    return lResultat;
}

Malgré qu’elle est fonctionnelle et qu’elle peut même paraître, à première vue, plus simple, les structures de contrôle de cette version peuvent causer des incompréhensions et des ambiguïtés à la lecture. Par exemple, quelqu’un pourrait ne pas remarquer qu’il y a un « throw » dans le « if » et se demander pourquoi la boucle « for » ne s’exécute pas, malgré ce cette dernière n’est dans aucune structure de contrôle conditionnelle (« if »). Cette ambiguïté pourrait donc avoir des effets dangereux dans un contexte de l’entretien à long terme du logiciel.

Gestion des exceptions

Du côté de la méthode appelante, il est important de gérer les exceptions des méthodes appelées avec une structure try/catch. Voici un exemple d’utilisation d’un try/catch:

ArrayList<Integer> lListe1 = new ArrayList<Integer>(5);
ArrayList<Integer> lListe2 = new ArrayList<Integer>(5);
lListe1.add(Integer.valueOf(5));
lListe1.add(Integer.valueOf(10));
lListe1.add(Integer.valueOf(15));
lListe1.add(Integer.valueOf(20));
lListe1.add(Integer.valueOf(25));
lListe2.add(Integer.valueOf(5));
lListe2.add(Integer.valueOf(10));
lListe2.add(Integer.valueOf(15));
lListe2.add(Integer.valueOf(20));
lListe2.add(Integer.valueOf(25));
try {
    List<Integer> lResultat = additionne(lListe1, lListe2);
    for (Integer element : lResultat) {
        System.out.println(element);
    }
}catch (Exception exception) {
    System.out.println("Il manque une valeur à la liste.");
}

Avec l’exemple précédent, regardez ce qui arrive lorsque vous retirez une des instructions « add ». Normalement, le message d’erreur devrait apparaître.

Il est également à noter qu’il est possible de mettre plus d’une clause catch dans le cas ou le la routine appelée peut lancer plus d’un type d’exception.

Exceptions de type « RuntimeException »

Comme nous l’avons vu plus haut, lorsqu’une exception de type « Exception » est lancée avec un « throw », il est obligatoire d’indiquer l’exception dans la signature de la méthode et il est également obligatoire pour le client de gérer cette exception avec un « try ».

Par contre, il existe également un type d’exception qui a comme particularité qu’il n’exige pas d’être géré. Ce type d’exception se nomme les « RuntimeException ».

Prenons par exemple, une nouvelle méthode « duplique », qui retourne une liste contenant les éléments d’une liste multipliés par 2. Afin d’éviter la duplication de code, nous allons utiliser la méthode « additionne » que nous avons utilisé plus haut:

/**
 * Multiplie par 2 les éléments de `aListe`.
 *
 * @param aListe La liste à multiplier par 2.
 * @return Liste contenant la multiplication par 2 des éléments de `aListe`.
 **/
public List<Integer> duplique(List<Integer> aListe) {
    return additionne(aListe, aListe);
}

Si on essaie de compiler cette méthode, le compilateur nous obligera à utiliser un « try…catch » afin de gérer l’exception de la méthode « additionne » (ou bien déclarer l’exception dans la signature de la méthode « duplique », ce qui obligera le client à gérer l’exception avec un « try…catch »).

Le problème, c’est qu’ici, l’exception est complètement illogique. Puisqu’il est impossible qu’une liste n’ait pas la même taille qu’elle-même, il est impossible que l’exception survienne. Nous ne devrions donc pas retourner d’exception dans la méthode « duplique ». Instinctivement, nous pourrions être tentés de ne rien mettre dans le « catch » du « try…catch ». Comme ceci (noter qu’il s’agit d’un exemple à ne pas reproduire):

/**
 * Multiplie par 2 les éléments de `aListe`.
 *
 * @param aListe La liste à multiplier par 2.
 * @return Liste contenant la multiplication par 2 des éléments de `aListe`.
 **/
public List<Integer> duplique(List<Integer> aListe) {
    List<Integer> lResultat;
    try {
        lResultat = additionne(aListe, aListe);
    } catch(Exception laException) {
        // Que faire ici
    }
    return lResultat;
}

Par contre, il est une mauvaise pratique en programmation de ne rien mettre dans un « catch ». Après tout, il est possible que nous ayons mal pensé notre logique et qu’une exception arrive tout de même. Donc, afin d’éviter d’avoir un « catch » vide et afin d’éviter que le client soit obligé de mettre inutilement un « try..catch », on peut utiliser un « RuntimeException ». Comme ceci:

/**
 * Multiplie par 2 les éléments de `aListe`.
 *
 * @param aListe La liste à multiplier par 2.
 * @return Liste contenant la multiplication par 2 des éléments de `aListe`.
 **/
public List<Integer> duplique(List<Integer> aListe) {
    List<Integer> lResultat;
    try {
        lResultat = additionne(aListe, aListe);
    } catch(Exception laException) {
        throw new RuntimeException("Un erreur interne s'est produit.");
    }
    return lResultat;
}

Noter qu’il est recommandé d’utiliser le moins possible les « RuntimeException ». En d’autres mots, utilisez seulement le « RuntimeException » lorsque cette exception ne devrait jamais être lancée (et que si elle est lancée, il s’agit du bogue de la méthode en tant que tel, et non celui du client).

Les suites de tests

Il est important de bien tester vos routines lorsque vous en créez. Il n’y a, bien entendu aucune surprise ici. Mais que veut dire « bien tester une routine »?

Pour bien tester une routine, il faut valider les éléments suivants:

    • Que la routine fait le travail qu’elle doit faire;
    • Que la routine fait TOUT le travail qu’elle doit faire;
    • Quel la routine ne fait pas le travail qu’elle n’est pas censée faire.

Ce qui nous amène aux différents types de cas de tests:

    • Les cas normaux:
      • Valide des valeurs qui devraient valider le travail standard de la routine,
      • Souvent les seuls tests effectués par les programmeurs;
    • Les cas erronés:
      • Valide que des valeurs non valides lancent des exceptions,
      • Les routines n’ont pas toutes des cas erronés;
    • Les cas limites:
      • Valide que les cas aux limites normaux et erronés sont valides,
      • Il est possible qu’il n’y ait pas de cas limite.

Il est à noter qu’une série de tests doit généralement être « arrangée avec le gars des vues ». C’est-à-dire que les valeurs sont préalablement choisies et inscrites dans le code. Il n’est pas nécessaire de faire des tests avec des valeurs aléatoires.

Voici une classe contenant une série de tests pour la routine « additionne » présentée plus tôt:

import java.util.ArrayList;
import java.util.List;

/**
 * Programme de pour montrer la création de tests.
 * 
 * @author Louis Marchand
 * @version %I%, %G%
 * 
 * Distribué sous licence MIT.
 */

public class Programme
{

    /**
     * Additionne les éléments de `aListe1` et de `aListe2`.
     *
     * Noter que les deux listes doivent être de la même taille.
     * @param aListe1 La première liste à additionner
     * @param aListe2 La seconde liste à additionner
     * @return Liste contenant la somme des deux listes.
     * @exception Exception Les listes ne sont pas de la même taille.
     **/
    public List<Integer> additionne(List<Integer> aListe1,
            List<Integer> aListe2) throws Exception {
        ArrayList<Integer> lResultat = new ArrayList<Integer>(aListe1.size());
        int i;
        if (aListe1.size() == aListe2.size()) {
            for (i = 0; i < aListe1.size(); i = i + 1) {
                lResultat.add(Integer.valueOf(aListe1.get(i).intValue() +
                            aListe2.get(i).intValue()));
            }
        } else {
            throw new Exception("Les listes doivent être de la même taille");
        }
        return lResultat;
    }

    /**
     * Suite de tests de la routine `additionne`.
     **/
    public void testAdditionne() {
        testAdditionneNormal();
        testAdditionneErrone();
        testAdditionneLimite();
    }

    /**
     * Cas normal de la suite de tests de la routine `additionne`.
     **/
    public void testAdditionneNormal() {
        ArrayList<Integer> lListe1 = new ArrayList<Integer>(5);
        ArrayList<Integer> lListe2 = new ArrayList<Integer>(5);
        List<Integer> lResultat;
        lListe1.add(Integer.valueOf(5));
        lListe1.add(Integer.valueOf(10));
        lListe1.add(Integer.valueOf(15));
        lListe2.add(Integer.valueOf(10));
        lListe2.add(Integer.valueOf(20));
        lListe2.add(Integer.valueOf(30));
        try {
            lResultat = additionne(lListe1, lListe2);
            if (lResultat.size() != 3 ||
                    lResultat.get(1).intValue() != 15 ||
                    lResultat.get(2).intValue() != 30 || 
                    lResultat.get(3).intValue() != 45) {
                System.out.println("Cas normal non valide.");
            }
        }catch (Exception exception) {
            System.out.println("Cas normal non valide.");
        }
    }

    /**
     * Cas erroné de la suite de tests de la routine `additionne`.
     **/
    public void testAdditionneErrone() {
        ArrayList<Integer> lListe1 = new ArrayList<Integer>(5);
        ArrayList<Integer> lListe2 = new ArrayList<Integer>(5);
        List<Integer> lResultat;
        lListe1.add(Integer.valueOf(5));
        lListe1.add(Integer.valueOf(10));
        lListe1.add(Integer.valueOf(15));
        lListe2.add(Integer.valueOf(10));
        lListe2.add(Integer.valueOf(20));
        try {
            lResultat = additionne(lListe1, lListe2);
            System.out.println("Cas erroné non valide.");
        }catch (Exception exception) {}
    }

    /**
     * Cas limite de la suite de tests de la routine `additionne`.
     **/
    public void testAdditionneLimite() {
        ArrayList<Integer> lListe1 = new ArrayList<Integer>(5);
        ArrayList<Integer> lListe2 = new ArrayList<Integer>(5);
        try {
            List<Integer> lResultat = additionne(lListe1, lListe2);
            if (lResultat.size() != 0) {
                System.out.println("Cas limite non valide.");
            }
        }catch (Exception exception) {
            System.out.println("Cas limite non valide.");
        }
    }

    /**
     * Exécution du programme
     **/
    public static void main(String[] arguments) {
        Programme programme = new Programme();
        programme.testAdditionne();
    }

}

Retour

 


Auteur: Louis Marchand
Creative Commons License
Sauf pour les sections spécifiées autrement, ce travail est sous licence Creative Commons Attribution 4.0 International.