M2TI : méthodes de conception de SI distribués 

TP 1 : conteneurs d'objets et inversion de contrôle

Application source

Vous allez travailler sur une application de gestion de bookmarks de sites web dont le diagramme de classes UML est disponible ici. Pour gagner du temps, une implémentation basique de cette appication est accessible .

Intégrez les sources de cette application dans un projet Eclipse et exécutez-la.

Dans son état actuel, la fonction main de l'application est située dans la classe d'interface utilisateur (AnnuaireUI), qui instancie une classe contrôleur (Annuaire), laquelle instancie à son tour les autres classes de l'application, et notamment la classe d'accès aux données (SiteXMLDAO). Vous allez remanier la structure de cette application pour qu'elle soit conforme à quelques patrons de conception de base.

1. Mise en place d'un conteneur

Pour cela, vous allez tout d'abord créer une arborescence de trois packages "annuaire", "serveur" et "client", le premier étant le père des des suivants. Créez ensuite deux classes supplémentaires :

Indications

A ce stade, vous avez réalisé une inversion de contrôle et créé un framework permettant d'instancier et de gérer vos objets côté serveur.

2. Isolation et uniformisation des objets côté serveur

2.1. Isolation

Bien entendu, vous ne pouvez pas laisser le client accéder directement à l'instance de l'annuaire créée dans le conteneur. Pour cela, vous allez implémenter le paradigme requête-réponse :

Remarquez que la classe Serveur masque désormais complètement l'implémentation du traitement des requêtes dans le programme principal et dans le client. Il suffit de connaître son API pour utiliser l'annuaire.

2.2. Uniformisation

Plutôt que d'avoir un objet Annuaire qui répond à différentes requêtes, vous allez créer plusieurs objets sur le même modèle, mais traitant chacun un type de requête spécifique. Pour cela :

A ce stade, vous avez réalisé un outil équivalent à un conteneur de servlets.

3. Création d'un contexte applicatif

L'objet SiteXMLDAO est commun pour tous les Site dans le conteneur que vous venez de créer. Vous allez implémenter une classe SiteContext qui permettra à chaque instance de Site créée d'accéder à ce DAO en respectant le pattern Context présenté en cours. Pour cela, vous allez en fait rajouter un niveau d'indirection entre Site et le DAO, qui vous permettra par exemple de modifier par configuration le DAO utilisé pour gérer la persistence des données de l'application.

3.1. Création du contexte

Pour rajouter une indirection entre le conteneur (et ses composants) et les objets de type SiteDAO, vous allez créer une interface "SiteContext" et sa classe d'implémentation, de façon à ce que :

Exemple de code pour ces deux éléments :

public interface SiteContext {
    
//Méthodes de configuration du contexte :
    //Spécification d'un type de DAO et du fichier (si XML) ou du port (si SGDB) qui va avec
    public void setDaoByType (String type, String FileOrPort) throws SiteDAOCreationException;
    //Utilisation d'une instance de DAO existante
    public void setSiteDAO(SiteDAO dao);

//Méthode de service du contexte
    public SiteDAO getSiteDao();
}

public class SiteContextImpl implements SiteContext {
    SiteDAO dao;

    public void setDaoByType (String type, String FileOrPort) throws SiteDAOCreationException {
        if (type.equals("XML")) {
            dao = new SiteXMLDAO (FileOrPort);
            System.out.println("SiteContext : DAO initialisé : type = " + type + " ; valeur = " + FileOrPort);
        } else { // rajouter ici d'autres types éventuels de DAO
            throw new SiteDAOCreationException ("Unrecognized DAO type : " + type);
        }
    }

    public void setSiteDAO(SiteDAO dao) {
        this.dao = dao;
    }

    public SiteDAO getSiteDao() {
        return this.dao;
    }
}

public class SiteDAOCreationException extends Exception {
    public SiteDAOCreationException (String message) {
        super(message);
    }
}

3.2. Modification de l'arbre de dépendances

L'objectif est ici d'intégrer l'objet SiteContext dans votre conteneur et de modifier les composants du conteneur ayant une dépendance sur un objet SiteDAO pour qu'ils dépendent de SiteContext 

3.3. Initialisation et utilisation du contexte

Enfin, initialisez la variable dao dans votre contexte avant le démarrage des différents composants Annuaire (méthode "start()"), en utilisant l'une des méthodes de configuration (tant qu'à faire, la première) :

        try {
            contexte.setDaoByType("XML", "test.xml");
        } catch (SiteDAOCreationException e) {
            e.printStackTrace();
        }

Dans la méthode start de Site, appelez la méthode getDao() du contexte pour positionner la variable "dao" avant l'utilisation du DAO.

L'avantage de cette méthode est qu'elle fonctionne quelles que soient les implémentations du conteneur et du composant, et qu'elle permet d'utiliser plusieurs implémentations différentes de l'objet DAO (si la classe SiteContextImpl est implémentée de façon plus approfondie).

4. Gestion du cycle de vie des objets

Actuellement, l'annuaire gère une liste de Site, qu'il augmente à chaque nouvel enregistrement. Tous n'ont pourtant pas besoin d'être instanciés en même temps. Vous allez remplacer cette liste par un conteneur (tant qu'à faire, utilisant un autre type d'injection de dépendances que le premier), modifier vos objets Site pour qu'ils soient compatible avec cette nouvelle implémentation, et en gérer le cycle de vie. Pour cela, vous allez suivre les étapes suivantes :

  1. Vous allez remplacer cette structure de stockage par un nouveau conteneur, fils du premier (voir partie "Container hierarchies" de l'introduction sur le site picocontainer) dans la classe Serveur. Pour cela :

    Reprenez le code de votre serveur pour que celui-ci déclare un deuxième conteneur. Vous passerez au constructeur de ce conteneur les paramètres suivants :

    • une ComponentFactory "new AnnotatedFieldInjection()" (c'est-à-dire qu'il permettra de résoudre les dépendances exprimées sous forme d'annotations de champs ; ici, une variable globale), 
    • une référence au conteneur parent, pour permettre la résolution de dépendances entre ses composants et ceux du père.

    //Création d'un conteneur fils de "container", de type AnnotatedFieldInjection : 2 méthodes
            //Méthode utilisant un builder
            PicoBuilder builder = new PicoBuilder(container);
            MutablePicoContainer sites = (DefaultPicoContainer) builder.withAnnotatedFieldInjection().build();
            //Méthode utilisant un constructeur spécifique
            //MutablePicoContainer sites = new DefaultPicoContainer(new AnnotatedFieldInjection(), container);

    //Rajout en tant que composant du conteneur principal (pour la propagation des dépendances)
            container.addComponent(sites);

    Supprimez la référence au DAO dans le constructeur de Site. En revanche, rajoutez à Site une variable globale de type "SiteDAO", et précédez sa déclaration d'une annotation @Inject (de org.picocontainer.annotations).

    En tant que composant, Site doit implémenter la méthode start() de l'interface Startable, dont vous vous servirez plus tard.

  2. Faites en sorte que votre serveur instancie dans ce deuxième conteneur autant de composants Site qu'il y en a dans l'annuaire. Pour cela, il faut :

    • créer une méthode d'initialisation du conteneur fils dans vos annuaires qui utilise la méthode listSites() du DAO pour peupler le conteneur et qui sera appelée (une seule fois !) depuis la méthode start() de votre annuaire,
          public void initListe() {
              Vector<Site> v = dao.getAllSites();
              Site s;
              int i=0;

              for(Iterator<Site> iter = v.iterator() ; iter.hasNext() ;) {
                  s = iter.next();
                  s.setContext((SiteContextImpl) sites.getComponent("Context"));
                  String name = "Site" + i;
                  sites.addComponent(name, s);
                  i++;
              }
          }
      Remarque : ici, la gestion des composants dans le conteneur fils est faite en fournissant à chaque instance créée un nom incluant son numéro d'index. Ce nom doit rester synchronisé avec l'ordre dans la liste, notamment lors de la suppression de sites. D'où le code un peu complexe de la méthode removeSite(), plus bas...
    • modifier la méthode add de l'annuaire pour qu'elle rajoute effectivement un nouveau site dans le conteneur fils,
          private boolean addSite(String desc, String url) {
              String name = "Site" + sites.getComponents().size();
              Site s = new Site(desc, url);
              s.setContext((SiteContext) sites.getComponent("Context"));
              sites.addComponent(name, s);
              try {
                  s.start();
                  s.save();
                  s.stop();
                  return true;
              } catch (Exception e) {
                  e.printStackTrace();
                  return false;
              }
          }
    • modifier la méthode remove pour qu'elle supprime la référence aux sites effacés du conteneur fils (et qu'elle décale les suivants s'ils sont gérés avec un nom qui dépend de leur numéro d'ordre).
          private int removeSite(String desc, String url) {
              //Remarque : on construit un Site sans contexte, uniquement pour utiliser sa méthode equals()
              Site s = new Site(desc, url);
              int result = 0;

              List<java.lang.Object> siteslist = sites.getComponents();
              Site temp;
              int i=0;

              try {
                  for(Iterator<java.lang.Object> iter = siteslist.iterator(); iter.hasNext() ;) {
                      temp = (Site) iter.next();
                      if (s.equals(temp)) {
                          temp.start();
                          temp.delete();
                          sites.removeComponent("Site" + i);
                          //On coupe le lien entre le conteneur et le Site
                          // Plus aucune référence n'existe sur cet objet
                          // On attend que le garbage collecting passe...
                          sites.removeComponentByInstance(temp);
                          temp.stop();
                          result++;
                      } else {
                          //il faut changer les noms de référence des composants suivant ceux qu'on a enlevés,
                          //sans quoi on aura un problème pour en rajouter d'autres...
                          if (result>0) {
                              sites.addComponent("Site" + (i-result), sites.getComponent("Site"+i));
                              sites.removeComponent("Site"+i);
                          }
                      }
                      i++;
                  }
                  s.stop();
              } catch (Exception e) {
                  e.printStackTrace();
                  result = -1;
              }
              return result;
          }

    A ce stade, vous avez dans votre application une arborescence de conteneurs gérant des dépendances entre leurs composants, en utilisant plusieurs types d'injection. Dans la question suivante, vous devrez détruire une partie de ce travail. En conséquence, quand votre application fonctionnera, sauvegardez-la et continuez dans un autre projet.

  3. Votre application n'accède jamais à toutes les instances de Site en même temps. A condition de les synchroniser avec votre support de persistance, vous pouvez vous passer de les avoir toutes à disposition. Pour cela :
    • modifiez votre conteneur fils pour qu'il n'instancie qu'un seul objet Site (plus besoin, donc, de de spécifier de nom pour ce composant) ; vous affecterez à ce composant un comportement (par exemple "Caching" ou "ThreadCaching"), permettant de conserver les valeurs des variables qui restent stables pour tous les sites, comme la référence au contexte ou au DAO,
    • créez deux composants de type String (nommé par exemple "Description" et "URL") du conteneur parent, et injectez-en les dépendances dans Site,
      Attention : vous devrez paramétrer l'injecteur à la création du composant pour que le conteneur arrive à résoudre les dépendances, Site dépendant désormais de deux objets de même type.
    • modifiez le code des différents objets de l'application pour l'adapter au nouveau fonctionnement : une instance de Site existe dans le conteneur, et tous les objets y accèdent tour à tour en positionnant les valeurs de la description et d'url du site dans les composants ad hoc.

5. Configuration de l'application

Écrivez un fichier de configuration en XML et stockez-y les dépendances de valeurs (type d'objet DAO, nom du fichier de stockage) et utilisez ces données dans la classe Serveur lors de l'instanciation des éléments des conteneurs et du contexte.

Valid XHTML 1.0 Strict