Looking for Computer Science  & Information Technology online courses ?
Check my new web site: https://www.yesik.it !

Cet article est une reprise de l'article Du besoin au programme Java que j'avais précédemment publié sur http://wiki.esicom-st-malo.fr

Une fois la phase d'apprentissage de la syntaxe passée, un des problèmes fréquent que rencontrent les débutants en programmation est le passage d'un besoin au programme qui correspond.

Selon les auteurs cette phase peut porter le nom de conception ou d'architecture. Dans tous les cas, c'est à ce moment que le développeur doit être capable de trouver une solution pour résoudre un problème et pouvoir traduire cette solution sous forme d'un programme informatique.

C'est un problème ardu, et il existe une multitude de méthodes pour formaliser ce processus. Dans cet article d'initiation, nous n'allons pas du tout rentrer dans ces considérations et nous contenter d'une approche simple, adaptée pour une découverte de la programmation.

L'environnement de développement

Avant toute chose, nous allons nous appuyer dans cet article sur le langage Java. On suppose ici la syntaxe à peu près acquise. De toute manière, sur ce point, on peut facilement trouver des exemples ou de la documentation sur Internet. On suppose également que vous savez utiliser les outils du JDK en ligne de commande (javac et java suffiront).

Développement

Le besoin

Dans cette initiation, nous allons essayer d'écrire un programme Java pour répondre au besoin suivant:

On souhaite réaliser "un programme pour gérer un compte bancaire."

Les objets

Vous le savez peut-être, Java est un langage de programmation orienté-objets. C'est à dire que ce langage contient tous les outils nécessaires pour supporter un modèle de programmation (on dit un paradigme de programmation) appelé la programmation orientée objets. Qu'est-ce que ça veut dire? Simplement que pendant son exécution, un programme Java va constamment créer et manipuler des objets.

D'accord, certains se diront. Mais c'est quoi un objet? Et bien un objet, ça peut être n'importe quoi! Même si ça a l'air d'une non-réponse, c'est loin d'être une boutade.

L'équivalent en français d'un objet en informatique, c'est un nom commun (ou assimilé, comme une locution nominal ou un groupe nominal). Reprenons donc la description de notre projet pour identifier les "noms communs":

On souhaite réaliser "un programme pour gérer un compte bancaire."

2 noms communs - 2 objets:

La première chose à faire est d'identifier les objets du système. Les objets correspondent aux noms communs dans la description de l'objectif à atteindre.


Les classes

A priori, notre programme va utiliser 2 objets. Or en Java on est obligé d'organiser les objets en familles appelées classes. Une classe est en quelque sorte un groupe qui représente ce qu'ont en commun tous les objets de ce groupe.

Cependant, ici, il n'y a pas besoin de réfléchir beaucoup pour se dire que l'objet "programme" et l'objet "compte bancaire" n'ont rien en commun: selon toute vraisemblance, ils appartiennent à deux familles différentes - donc à 2 classes différentes.

Note:

Vous vous dites peut-être que l'objet "programme" et l'objet "compte bancaire" ont quelque-chose en commun, puisque le "programme" va utiliser le "compte bancaire". Mais que l'un utilise l'autre ne signifie pas qu'ils ont quelque-chose de commun entre eux!

Prenons un exemple trivial: pour manger, vous utilisez une fourchette. Pourtant, il ne viendrait à l'esprit de personne de dire que vous avez quelque-chose de commun avec une fourchette!

Arrivé à ce point, nous avons déjà fait un grand pas: en effet, nous savons maintenant que notre programme aura 2 classes. Or en Java, une classe correspond à un fichier. Nous pouvons donc déjà commencer à coder les fichiers Program.java (qui contiendra la classe Program - pour "programme") et Account.java (qui contiendra la classe Account - pour "compte bancaire").

Remarque:

Vous vous demandez peut-être pourquoi j'utilise des termes en anglais pour mes noms de classe. Rassurez-vous, ce n'est pas (juste) pour me donner un style. Il y a plusieurs raisons pragmatiques à ce choix:

  • L'anglais est souvent plus concis (comparez "Account" et "CompteBancaire");
  • Vous verrez que Java normalise le nom des méthodes dans certains contextes. Ainsi, les "JavaBeans" imposent que certaines méthodes commencent par "get" ou "set". Et du coup, le mélange anglais-français est du plus mauvais effet ("getSoldeDuCompte");
  • Dans un contexte d'internationalisation des équipes de développement, il est plus simple que tout le monde utilise des identifiants en anglais. Imaginez si le développeur français donne des noms en français à ses classes et que son collègue suédois donne des noms en suédois... Plus personne ne va y comprendre quoi que ce soit.

Account.java

public class Account {
}

Program.java

public class Program {
}

Après avoir identifié les objets du système, il faut les grouper dans des classes. On peut commencer à coder chaque classe dans son fichier.


Compiler, exécuter

C'est court, mais suffisant pour compiler:

 sh$ javac -classpath . Account.java Program.java

Par contre, si l'on essaye de lancer le programme:

 sh$ java Program
 Exception in thread "main" java.lang.NoSuchMethodError: main

Pour pouvoir être exécuté, un programme Java doit posséder une méthode spéciale appelée main. Nous allons modifier notre programme Java en conséquence:

public class Program {
    public static void main(String args[]) {
    }
}

On peut maintenant compiler et tester:

 sh$ javac -classpath . Account.java Program.java
 sh$ java Program

Pas d'erreur, mais il ne se passe rien. Normal, notre programme principal (main) ne fait rien.

Après chaque modification du code (on parle parfois d'itération, de refonte ou de refactoring) il faut s'assurer qu'il est toujours possible de compiler et d'exécuter le programme.


Utiliser les méthodes

Quelque part, si on voulait exprimer en français ce qu'il y a pour l'instant dans notre programme, ça ferait:

On a réalisé "un programme un compte bancaire."

Si l'on compare avec le besoin exprimé:

On souhaite réaliser "un programme pour gérer un compte bancaire."

On se rend compte qu'il manque un truc important: le verbe! Les verbes en Java, ce sont les méthodes.

Le seul verbe de notre description, c'est "gérer". C'était très bien jusqu'à présent, mais maintenant, il va nous falloir essayer d'être plus précis. Pour moi, gérer un compte, ça pourrait vouloir dire:

Remarquez que tous ces points sont des actions/des verbes (créer, créditer, débiter, récupérer, afficher). En Java, ces actions sont mises en œuvre par des méthodes. En 2 mots, mon "main" va appeler une méthode pour chacune de ces actions. Ce qui donne:

public class Program {
    public static void main(String args[]){
        Account account = new Account(); /* je crée le compte */
        account.deposit(300); /* je dépose 300 sur mon compte */
        account.withdraw(150); /* je retire 150 sur mon compte */
        int balance = account.getBalance(); /* je récupère mon solde */
 
        System.out.println(balance); /* J'affiche le solde */
    }
}

Le comportement dynamique d'un programme est exprimé par les verbes dans la description de ce que fait le programme. Ce comportement dynamique est mis en oeuvre dans les méthodes. Pour identifier les méthodes, il peut être nécessaire de reformuler la description du programme pour utiliser des verbes plus précis.

Remarque:

Il y avait 4 actions à mettre en oeuvre et 5 lignes de Java ont été ajoutées! Observez bien la dernière de ces lignes:

 System.out.println(balance);

Elle ne fait pas du tout référence au compte! Elle ne correspond donc pas à une méthode de la classe Account.

Définir les méthodes

Maintenant, si nous tentons de compiler, nous obtenons un certain nombre d'erreurs:

 sh$ javac -classpath . Account.java Program.java
 Program.java:5: cannot resolve symbol
 symbol  : method deposit (int)
 location: class Account
                 account.deposit(100); 
                        ^
 Program.java:6: cannot resolve symbol
 symbol  : method withdraw (int)
 location: class Account
                 account.withdraw(150);
                        ^
 Program.java:7: cannot resolve symbol
 symbol  : method getBalance ()
 location: class Account
                 int balance = account.getBalance(); 
                                      ^
 3 errors

C'est parfaitement normal, puisque l'on cherche à utiliser des méthodes qui n'existent pas dans la classe Account. Il faut donc modifier cette classe en conséquence. Ici encore, nous n'allons faire que le minimum pour satisfaire le compilateur.

public class Account {
     public void deposit(int s){ }
     public void withdraw(int s){ }
     public int getBalance(){ return 0; }
}

Testons à nouveau:

 sh$ javac -classpath . Account.java Program.java
 sh$ java Program
 0

Le programme compile sans problème. On peut le lancer. Par contre, le résultat est faux. Evidemment, les opérations de crédit et de débit ne sont pas prises en compte puisque les méthodes correspondantes ne font rien. De plus, la méthode getBalance sensée renvoyer le solde du compte renvoie toujours 0.

Les propriétés

Ce qui manque, c'est simplement que l'objet mémorise le solde du compte. Or le solde est une donnée:

C'est donc le candidat idéal pour en faire une propriété (on dit aussi "un attribut", une "donnée membre" ou une "variable membre"):

public class Account {
     int balance;
     public void deposit(int s){ }
     public void withdraw(int s){ }
     public int getBalance(){ return 0; }
}

Désormais, chaque compte possédera son propre solde (balance). Reste à prendre en compte ce solde dans les différentes opérations (méthodes) supportées par les objets "comptes bancaires":

public class Account {
     int balance;
     public void deposit(int s){ balance += s; }
     public void withdraw(int s){ balance -= s; }
     public int getBalance(){ return balance; }
}
 sh$ javac -classpath . Account.java Program.java
 sh$ java Program
 150

Les caractéristiques qui sont propres à chaque objet sont représentées par une propriété de l'objet.


Quelques bonnes pratiques

Gérer les cas limites

Formidable. Dans le cas idéal (Happy Path) tout a bien l'air de fonctionner!!! Mais modifions un peu le programme principal pour examiner un cas particulier:

public class Program {
        public static void main(String args[]){
                Account account = new Account();
                account.deposit(100);
                account.withdraw(150);
                int balance = account.getBalance();
                System.out.println(balance);
        }
}
 sh$ javac -classpath . Account.java Program.java
 sh$ java Program
 -50

D'accord, le calcul est bon. Mais normalement, il devrait être impossible de débiter 150 d'un compte dont le solde n'est que de 100. Il faut donc modifier la méthode withdraw pour gérer ce cas de figure. L'idée qui vient souvent en tête aux débutants est la suivante:

public void withdraw(int s) {
    if (balance < s)
        System.out.println("Débit impossible");
    else 
        balance = balance - s; 
}

Cloisonner

D'accord, mettre un println dans la méthode withdraw, ça a l'air d'être une bonne idée. Mais que se passerait-t-il dans la réalité? L'objet account serait vraisemblablement sur le serveur d'application centralisé de la banque, et le programme qui utilise cet objet sur le poste d'un conseiller financier - ou dans mon navigateur quand je gère mes comptes par Internet. Donc quand l'objet account fait un println, au mieux, ça s'afficherait sur la console du serveur. Le conseiller financier (ou l'utilisateur Internet) n'aurait aucun moyen de savoir que son retrait n'a pas été honoré! Pas bon, ça...

En outre, sur le principe, ce n'est généralement pas une bonne idée de mélanger dans une même méthode du code qui fait quelque chose qui a du sens (créditer/débiter ça a du sens pour un "compte bancaire") et du code qui n'en a pas ("afficher", ça ne veut rien dire pour un "compte bancaire" - par contre pour un "programme"...). Si l'on veut suivre ce principe, il faut donc que la méthode withdraw essaye de débiter le compte, et qu'en cas d'échec, elle soit capable de signaler que cela n'a pas été possible. Ni plus (pas d'affichage par exemple), ni moins (pas d'échec silencieux)...

Un moyen simple de faire est de renvoyer un indicateur pouvant avoir deux valeurs:

Deux valeurs? Immédiatement, cela doit vous faire penser à un type booléen (boolean en Java).

public boolean withdraw(int s) {
  if (balance < s)
    return false;
 
  balance -= s;
  return true;
}

Maintenant, la valeur de retour de la méthode withdraw sert à informer la méthode qui appelle withdraw si le retrait s'est bien passé ou pas.

Remarquez également comment j'ai codé cette logique: je me débarrasse tout de suite du cas "à problème". Après je peux tranquillement m'occuper du cas "normal" sans avoir à traîner de else ou d'accolades...

D'accord, maintenant, withdraw sait indiquer si le retrait est possible (et fait) ou pas. Par contre, si l'on veut faire quelque-chose d'utile, il faut modifier le main pour prendre en compte cette information:

public class Program {
        public static void main(String args[]){
                Account account = new Account();
                account.deposit(100);
                if (account.withdraw(150) == false)
                   System.err.println("Débit impossible");
                int balance = account.getBalance();
                System.out.println(balance);
        }
}

Vu de l'extérieur, ça fait à peu près la même chose que l'idée de départ. Mais remarquez comme mon code est cloisonné: d'un côté le compte // de l'autre le programme. Chacun ne fait que ce qui a du sens pour lui. Ce cloisonnement est un des principes de base d'une bonne conception orientée objets.

Chaque objet ne doit supporter que des méthodes qui ont du sens pour lui.


Utiliser les exceptions

La solution de renvoyer une valeur (ici un booléen) pour signaler le succès ou l'échec est une solution classique dans de nombreux langages. Néanmoins, cela impose à l'appelant (la méthode qui appelle withdraw) de tester cette valeur de retour après chaque appel:

public class Program {
    public void main(String args[]) {
        Account account = new Account();
        account.deposit(100);
        if (account.withdraw(50) == false)
            System.err.println("Débit impossible");
        account.deposit(100);
        if (account.withdraw(50) == false)
            System.err.println("Débit impossible");
        if (account.withdraw(50) == false)
            System.err.println("Débit impossible");
        int balance = account.getBalance(); /* je récupère mon solde */
 
        System.out.println(balance); /* J'affiche le solde */
    }
}

Non seulement cela alourdit le code, et le rend redondant, mais surtout il y a risque d'introduire des erreurs dans le programme en oubliant un test à un moment ou un autre.

Java (comme de nombreux langages modernes comme le C++) propose un mécanisme plus robuste pour gérer ce genre de situation: les exceptions.

Le mécanisme de gestion des exceptions est un dispositif qui permet d'interrompre le déroulement normal du programme et d'envoyer un message pour signaler qu'une situation exceptionnelle a été détectée.

Dans ce mécanisme, le programmeur est responsable de plusieurs choses:

Signaler une exception

En Java, pour signaler une exception on utilise le mot clé throw suivi d'un objet de la classe Exception (ou plus généralement, d'une de ses classes dérivées) qui représente l'exception.

Si l'on ré-écrit notre méthode withdraw pour signaler une exception, cela donnerait:

public void withdraw(int s) {
  if (balance < s)
    throw new Exception("Retrait impossible");
 
  balance -= s;
}

Remarquez au passage que le type de retour de la méthode est maintenant void et que l'on a plus besoin de signaler que "tout s'est bien passé" (return true).

Si l'on tente de compiler la classe Account après cette modification, on obtient l'erreur suivante:

 sh$ javac -classpath . Account.java Program.java
 Account.java:7: unreported exception java.lang.Exception;
 must be caught or declared to be thrown
                      throw new Exception("Erreur retrait");
                      ^
 1 error

En fait, le langage Java nous oblige à indiquer qu'une méthode peut renvoyer une exception. De cette manière, on peut savoir si une méthode est susceptible de déclencher une exception juste à partir de son prototype (sans avoir à examiner le code ligne par ligne):

public void withdraw(int s) throws Exception {
  if (balance < s)
    throw new Exception("Retrait impossible");
 
  balance -= s;
}

Note:

Le prototype est la "ligne" qui donne les informations nécessaires pour pouvoir utiliser une méthode:

public void withdraw(int s) throws Exception

S'interprète "voici la méthode publique withdraw qui prend un entier s en argument et ne renvoie aucun résultat. Cette méthode est susceptible de générer une exception de la classe Exception".

On peut maintenant compiler:

 sh$ javac -classpath . Account.java
 sh$ javac -classpath . Program.java
 Program.java:5: 'void' type not allowed here
                 if (account.withdraw(150) == false)

Bien évidemment: nous avons modifié la méthode withdraw, mais pas le programme principal qui l'utilise! C'est ce que nous allons faire maintenant.

Installer un gestionnaire d'exceptions

La première étape va être de supprimer le test qui nous servait à détecter si le retrait s'était bien passé ou pas. Ce test n'a plus de raison d'être puisque la méthode withdraw ne renvoie plus de booléen pour indiquer le succès ou l'échec:

public class Program {
        public static void main(String args[]){
                Account account = new Account();
                account.deposit(100);
                account.withdraw(150);
                int balance = account.getBalance();
                System.out.println(balance);
        }
}

Si l'on compile maintenant, nous obtenons le message d'erreur suivant:

 sh$ javac -classpath . Program.java
 Program.java:5: unreported exception java.lang.Exception; 
 must be caught or declared to be thrown
                 account.withdraw(150);
                        ^
 1 error

Si vous avez l'impression d'avoir déjà vu ce message, vous ne rêvez pas. dans le cas contraire, je vous encourage à "remonter" de quelques paragraphes pour le rechercher. Comme tout à l'heure, ce message signifie que notre code est susceptible de déclencher une exception, mais que ça n'apparaît pas dans le prototype de la méthode.

Une seconde. Ce code est susceptible de déclencher une exception? Mais où se trouve le throw? Vous ne le voyez pas? Et pourtant, même s'il n'apparaît pas explicitement, il est bien là! En fait, il est caché dans l'appel à la méthode withdraw: La méthode withdraw est susceptible de déclencher une exception, donc la méthode main qui appelle withdraw est susceptible de déclencher une exception.

Deux solutions s'offrent alors à nous: comme tout à l'heure, dire que le main peut lancer cette exception et ne plus s'en préoccuper. Ou au contraire, intercepter cette exception et la traiter. C'est ce qu'il y a de plus raisonnable à faire ici. La syntaxe est la suivante:

public class Program {
        public static void main(String args[]){
                Account account = new Account();
                try {
                        account.deposit(100);
                        account.withdraw(150);
                }
                catch(Exception e) {
                        System.err.println(e.toString());
                }
                finally {
                        int balance = account.getBalance();
                        System.out.println(balance);
                }
        }
}

Vous remarquez trois nouveaux blocs:

Maintenant, le programme doit compiler. - et fonctionner!

Utilisez les exceptions pour gérer les situations exceptionnelles!


Pas convaincu?

A titre d'illustration pour terminer cet article, comparez un peu, dans le cas de dépôts et de retraits multiples, le code du programme principal qui n'utilise pas la gestion des exceptions et celui qui l'utilise:

public class Program {
    public void main(String args[]) {
        Account account = new Account();
        account.deposit(100);
        if (account.withdraw(50) == false)
            System.err.println("Débit impossible");
        account.deposit(100);
        if (account.withdraw(50) == false)
            System.err.println("Débit impossible");
        if (account.withdraw(50) == false)
            System.err.println("Débit impossible");
        int balance = account.getBalance();
 
        System.out.println(balance);
    }
}
public class Program {
        public static void main(String args[]){
                Account account = new Account();
                try {
                        account.deposit(100);
                        account.withdraw(50);
                        account.deposit(100);
                        account.withdraw(50);
                        account.withdraw(50);
                }
                catch(Exception e) {
                        System.err.println(e.toString());
                }
                finally {
                        int balance = account.getBalance();
                        System.out.println(balance);
                }
        }
}

Quel code est le plus clair? quel code est le moins redondant? Dans lequel y a-t-il le plus de risque d'oublier quelque-chose lors d'une modification?