Tutoriel Java 2

Cette page a été rédigée il y a fort fort longtemps, et n'a pas tellement été mise à jour.

 

Vous savez, moi je ne crois pas qu'il y ait de bonne ou de mauvaise page. Moi, si je devais résumer mon wiki aujourd'hui avec vous, je dirais que c'est d'abord des rencontres. Des gens qui m'ont tendu la main, peut-être à un moment où je ne pouvais pas, où j'étais seul chez moi. Et c'est assez curieux de se dire que les hasards, les rencontres forgent une destinée... Parce que quand on a le goût de la chose, quand on a le goût de la chose bien faite, le beau geste, parfois on ne trouve pas l'interlocuteur en face je dirais, le miroir qui vous aide à avancer. Alors ça n'est pas mon cas, comme je disais là, puisque moi au contraire, j'ai pu ; et je dis merci au wiki, je lui dis merci, je chante le wiki, je danse le wiki... je ne suis qu'amour ! Et finalement, quand des gens me disent « Mais comment fais-tu pour avoir cette humanité ? », je leur réponds très simplement que c'est ce goût de l'amour, ce goût donc qui m'a poussé aujourd'hui à entreprendre une construction logicielle... mais demain qui sait ? Peut-être simplement à me mettre au service de la communauté, à faire le don, le don de soi.

Présentation

Cet article consistue la deuxième partie du tutoriel Java, dont l’objectif est de réaliser une application "Todo List" en langage Java avec une interface graphique écrite en Swing. Dans la première partie, nous avons développé les classes model et dao. Nous allons maintenant écrire les classes service, qui vont notamment mettre en oeuvre les règles de gestion.

Création des classes de service métier

Nous allons continuer à travailler dans le projet TodoListService créé sous NetBeans dans la première partie du tutoriel. Pour rappel, ce projet va contenir à terme toute la couche métier de l’application.

Spécification du service

Commençons donc par créer l’interface TodoListService que nous plaçons dans le package info.jtips.todolist.service :

package info.jtips.todolist.service;

import info.jtips.todolist.model.Tache;
import info.jtips.todolist.model.Utilisateur;
import java.util.List;

public interface TodoListService {
   Utilisateur verifierLogin(String id, String pwd);
   void enregistrerUtilisateur(Utilisateur u);
   Tache lireTache(int id);
   List<Tache> lireListeTaches();
   void sauvegarderTache(Tache t);
   void supprimerTache(Tache t);
}

Cette interface représente le contrat du service métier, c’est à dire qu’elle se contente d’énoncer la liste des méthodes qui doivent être disponibles dans un service de ce type. Elle ne dit pas comment les méthodes doivent être mises en oeuvre, car cela est de la responsabilité des classes d’implémentation (voir un peu plus loin).

Par ailleurs, nous ajoutons une classe d’exception personnalisée dans le même package, car les méthodes du service métier seront susceptibles de lever un exception si une règle de gestion échoue :

package info.jtips.todolist.service;

public class TodoListException extends RuntimeException {

   public TodoListException(Throwable cause) {
       super(cause);
   }

   public TodoListException(String message, Throwable cause) {
       super(message, cause);
   }

   public TodoListException(String message) {
       super(message);
   }

   public TodoListException() {
   }

}

j’ai choisi d’hériter de la classe RuntimeException, plutôt que de la classe Exception. Ce choix est discutable, mais il apporte de la simplicité au développement pour les raisons suivantes :

  • syntaxe try..catch pas imposée

  • rollback automatique des transactions lorsque les services métier sont hébergés par un conteneur Spring ou EJB (ce sujet n’est pas couvert dans cet article)

Implémentation du service

Ajoutons maintenant la classe d’implémentation de l’interface définie précédemment, à savoir la classe TodoListServiceImpl placée dans le package info.jtips.todolist.service.impl. Cette implémentation s’appuie sur la couche DAO pour interroger et mettre à jour la base de données. Les méthodes du service vont donc utiliser des instances de UtilisateurDao et TacheDao pour effectuer le requêtage SQL.

Comme les DAO sont utilisées dans toutes les méthodes du service, nous les déclarons en champs d’instance (en fait, on peut procéder de cette façon car les DAO sont dites stateless, c’est à dire qu’elles ne possèdent pas d’état propre à un objet client).

Dans le code ci-dessous, examiner les règles de gestion et notamment l’utilisation de l’exception personnalisée TodoListException :

package info.jtips.todolist.service.impl;

import info.jtips.todolist.dao.TacheDao;
import info.jtips.todolist.dao.UtilisateurDao;
import info.jtips.todolist.dao.impl.TacheDaoImpl;
import info.jtips.todolist.dao.impl.UtilisateurDaoImpl;
import info.jtips.todolist.model.Tache;
import info.jtips.todolist.model.Utilisateur;
import info.jtips.todolist.service.TodoListException;
import info.jtips.todolist.service.TodoListService;
import java.util.List;

public class TodoListServiceImpl implements TodoListService {

   private UtilisateurDao utilisateurDao = new UtilisateurDaoImpl();
   private TacheDao tacheDao = new TacheDaoImpl();

   /**
    * Cette méthode permet de vérifier un couple "identifiant/mot de passe"
    * issue d'un écran de login. Si l'utilisateur correspondant à l'identifiant
    * ou si le mot de passe est incorrect, alors une exception est levée
    */
   public Utilisateur verifierLogin(String id, String pwd) {
       Utilisateur u = utilisateurDao.findById(id);
       if (u != null &amp;&amp; pwd != null &amp;&amp; pwd.equals(u.getMotDePasse())) {
           return u;// connexion OK
       }
       throw new TodoListException("Echec de l'authentification");
   }

   /**
    * Un utilisateur peut être enregistré si son identifiant n'est pas déjà utilisé.
    * Si l'identifiant est déjà utilisé, une exception est levée.
    */
   public void enregistrerUtilisateur(Utilisateur u) {
       // vérification login déjà utilisé
      Utilisateur utilisateurExistant = utilisateurDao.findById(u.getIdentifiant());
      if (utilisateurExistant != null) throw new TodoListException("Identifiant déjà utilisé");

      // ok, on peut enregistrer
      utilisateurDao.create(u);
   }

   /**
    * Dans cette implémentation, l'association "Utilisateur <-> Tache" est
    * initialisée uniquement côté "Tache", ie la propriété "utilisateur".
    * Côté "Utilisateur", la collection de tâches n'est pas initialisée.
    */
   public List<Tache> lireListeTaches() {
       List<Tache> taches = tacheDao.findAll();
       for (Tache t : taches) {
           Utilisateur u = utilisateurDao.findById(t.getUtilisateur().getIdentifiant());
           t.setUtilisateur(u);
       }
       return taches;
   }

   /**
    * Dans cette méthode, l'association "Utilisateur <-> Tache" est complètement
    * initialisée : ie la propriété "utilisateur" côté "Tache" et la propriété
    * "taches" coté "Utilisateur".
    */
   public Tache lireTache(int id) {
       Tache tache = tacheDao.findById(id);
       String idUtilisateur = tache.getUtilisateur().getIdentifiant();
       Utilisateur u = utilisateurDao.findById(idUtilisateur);
       List<Tache> taches = tacheDao.findByUtilisateur(u);
       for (Tache t : taches) {
           u.ajouterTache(t);
           if (t.getId() == id) tache = t;// évite d'avoir deux instances différentes
                                          // pour l'entité recherchée dans le graphe d'objet
       }
       tache.setUtilisateur(u);
       return tache;
   }

   /**
    * Cette méthode consiste à effectuer un INSERT s'il s'agit de sauvegarder
    * une nouvelle tâche (reconnue par id == -1). S'il s'agit d'une tâche
    * existante (reconnue par id != -1), alors un UPDATE est généré.
    */
   public void sauvegarderTache(Tache t) {
       if (t.getId() == -1) {
           tacheDao.create(t);
       }
       else {
           tacheDao.update(t);
       }
   }

   /**
    * Suppression de la tâche passée en paramètre
    */
   public void supprimerTache(Tache t) {
       tacheDao.delete(t);
   }

}

Tests unitaires

De même que pour la couche DAO, il convient de prévoir des tests unitaires afin de vérifier le bon fonctionnement de notre service métier.

La classe de test ci-dessous est similaire à celles que nous avons écrites pour les DAO. Remarquez cependant la méthode de test testEnregistrerUtilisateurExistant, pour laquelle nous paramétrons l’annotation @Test avec expected=TodoListException.class : cela signifie que le test est correct si l’exception TodoListException est levée par la méthode testée.

package info.jtips.todolist.service;

import info.jtips.todolist.model.Tache;
import info.jtips.todolist.model.Utilisateur;
import info.jtips.todolist.service.impl.TodoListServiceImpl;
import java.util.Date;
import java.util.List;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.*;

public class TodoListServiceTest {

   @Test
   public void testEnregistrerUtilisateur() {
       Utilisateur u = new Utilisateur();
       u.setIdentifiant("bb");
       u.setPrenom("Bugs");
       u.setNom("Bunny");
       u.setMotDePasse("mp");

       TodoListService service = new TodoListServiceImpl();
       service.enregistrerUtilisateur(u);

       System.out.println("Terminé");
   }

   @Test(expected=TodoListException.class)
   public void testEnregistrerUtilisateurExistant() {
       Utilisateur u = new Utilisateur();
       u.setIdentifiant("bb");
       u.setPrenom("Bugs");
       u.setNom("Bunny");
       u.setMotDePasse("mp");

       TodoListService service = new TodoListServiceImpl();
       service.enregistrerUtilisateur(u);

       System.out.println("Terminé");
   }

   @Test
   public void testVerifierLoginOK() {
       TodoListService service = new TodoListServiceImpl();
       Utilisateur u = service.verifierLogin("bb", "mp");

       assertNotNull(u);
       assertEquals("Bugs", u.getPrenom());
       assertEquals("Bunny", u.getNom());
   }

   @Test(expected=TodoListException.class)
   public void testVerifierLoginKO() {
       TodoListService service = new TodoListServiceImpl();
       Utilisateur u = service.verifierLogin("bb", "pas le bon mot de passe");
   }

   @Test
   public void testSauvegarderTache1() {// test INSERT
       Utilisateur u = new Utilisateur();
       u.setIdentifiant("bb");
       u.setPrenom("Bugs");
       u.setNom("Bunny");
       u.setMotDePasse("mp");

       Tache t1 = new Tache();
       t1.setLibelle("Une première tâche");
       t1.setDateFin(new Date());
       t1.setPriorite(1);
       u.ajouterTache(t1);

       Tache t2 = new Tache();
       t2.setLibelle("Une deuxième tâche");
       t2.setDateFin(new Date());
       t2.setPriorite(1);
       u.ajouterTache(t2);

       TodoListService service = new TodoListServiceImpl();
       service.sauvegarderTache(t1);
       service.sauvegarderTache(t2);
   }

   @Test
   public void testSauvegarderTache2() {// test UPDATE
       TodoListService service = new TodoListServiceImpl();
       Tache t = service.lireTache(2);
       t.setLibelle("Tâche modifiée");
       service.sauvegarderTache(t);

   }

   @Test
   public void testLireTache() {
       TodoListService service = new TodoListServiceImpl();
       Tache t = service.lireTache(1);
       assertEquals(1, t.getId());
       assertEquals("Une première tâche", t.getLibelle());
       assertNotNull(t.getUtilisateur());
       assertEquals("bb", t.getUtilisateur().getIdentifiant());
       assertEquals(2, t.getUtilisateur().getTaches().size());

   }

   @Test
   public void testLireListeTaches() {
       TodoListService service = new TodoListServiceImpl();
       List<Tache> taches = service.lireListeTaches();
       assertEquals(2, taches.size());
   }

   @Test
   public void testSupprimerTaches() {
       TodoListService service = new TodoListServiceImpl();
       Tache t = service.lireTache(1);
       service.supprimerTache(t);
       List<Tache> taches = service.lireListeTaches();
       assertEquals(1, taches.size());
   }

}

Conclusion

Notre couche métier est désormais terminée. Elle peut être utilisée depuis une couche de présentation.

Tut2Java1.png

Notez cependant quelques imperfections :

  • Lors d’un appel de méthode sur le service métier, plusieurs connexions JDBC sont ouvertes du fait de l’invocation de plusieurs méthodes DAO (cf méthode lireTache()). Cette façon de procéder est coûteuse en ressources machine.

  • La gestion transactionnelle n’est pas gérée correctement. En effet, nous nous basons actuellement sur l’auto-commit pour gérér les transactions. Ce mécanisme opère au niveau des DAO, ie plusieurs commit ont lieu lors de l’appel à une méthode du service (cf méthode enregistrerUtilisateur(). La démarcation de la transaction devrait plutôt être située au niveau de la classe TodoListServiceImpl

Pour résoudre ces imperfections, plusieurs solutions existent :

  • Gérer l’ouverture / fermeture de la connexion JDBC, ainsi que l’appel aux commit et rollback à l’aide d’un thread local : nous ne détaillerons pas cette approche ici, car elle nécessite l’écriture de code technique, qui peut être très avantageusement remplacé par une des deux solutions ci-dessous

    • Utiliser un conteneur EJB

    • Utiliser Spring Framework

Téléchargement du projet

Le projet NetBeans correspondant à la couche métier.

Suite du tutoriel

Bientôt un nouvel article pour la partie présentation avec Swing…​