Langage de programmation:
Design pattern: 

Exemple d’implémentation du pattern Singleton (et Multiton) en Java sur le thème de la journalisation d’un compte bancaire.

 

Description du problème

Afin de mettre en pratique le pattern Singleton en Java, prenons un court exemple d’implémentation dans le milieu bancaire. Tout d’abord nous allons concevoir une classe CompteBancaire qui permet de déposer ou retirer de l’argent sur un compte. Mais nous souhaiterions pouvoir afficher les opérations (effectuées ou refusées) dans la console en cas de litige (il serait aussi possible d’utiliser en sortie un fichier texte). Cette petite application devra rapidement évoluer et il est fort probable que par la suite d’autres classes soient concerner par cette journalisation. Pour cela, nous allons implémenter une classe distincte nommée Journalisation reprenant le pattern Singleton. Ainsi nous allons garantir que notre programme va utiliser une seule et même instance de la classe Journalisation. Une troisième classe intitulée Main permettra d’exécuter l’application et d’obtenir un résultat en console.


Implémentation de la solution basée sur Singleton

Diagramme UML de l'implémentation en Java du design pattern Singleton

Sur ce diagramme UML, on retrouve les deux classes Jounalisation et CompteBancaire. On remarque aisément que la classe Journalisation est basée sur le pattern Singleton. En effet, on trouve un attribut statique de type Singleton, un constructeur déclaré en privé et une méthode statique servant de pseudo-constructeur. Il existe aussi deux méthodes propres à ’utilisation de cette classe que sont ajouterLog(string) et afficherLog().

Implémentation de la classe Journalisation

// Classe basée sur le pattern Singleton qui permet la journalisation de l'application.
public class Journalisation
{
        private static Journalisation uniqueInstance;// Stockage de l'unique instance de cette classe.
        private String log;// Chaine de caractères représentant les messages de log.
       
        // Constructeur en privé (donc inaccessible à l'extérieur de la classe).
        private Journalisation()
        {
                log = new String();
        }
       
        // Méthode statique qui sert de pseudo-constructeur (utilisation du mot clef "synchronized" pour le multithread).
        public static synchronized Journalisation getInstance()
        {
                if(uniqueInstance==null)
                {
                        uniqueInstance = new Journalisation();
                }
                return uniqueInstance;
        }
       
        // Méthode qui permet d'ajouter un message de log.
        public void ajouterLog(String log)
        {
                // On ajoute également la date du message.
                Date d = new Date();
                DateFormat dateFormat = new SimpleDateFormat("dd/MM/yy HH'h'mm");
                this.log+="["+dateFormat.format(d)+"] "+log+"\n";
        }
       
        // Méthode qui retourne tous les messages de log.
        public String afficherLog()
        {
                return log;
        }
}

L’attribut uniqueInstance permet de stocker l’unique instance de cette classe. Le constructeur est déclaré privé donc accessible uniquement depuis la classe elle même. C’est la méthode getInstance() qui permet d’instancier cette classe. Pour que notre application fonctionne dans le cas du multithread nous avons utilisé le mot clef « synchronized » [1] dans la déclaration de getInstance(). Plus classiquement la méthode ajouterLog(string) permet d’ajouter un message dans l’attribut log et afficherLog() retourne le contenu de cet attribut.

Implémentation de la classe CompteBancaire

// Classe représentant un compte bancaire simpliste.
public class CompteBancaire
{
        private int numero;// Numéro du compte.
        private double solde;// Argent disponible sur le compte.
       
        // Constructeur d'un CompteBancaire à partir de son numéro.
        public CompteBancaire(int numero)
        {
                this.numero=numero;
                this.solde=0.0;
        }
       
        // Méthode qui permet de déposer de l'argent sur le compte.
        public void deposerArgent(double depot)
        {
                if(depot>0.0)
                {       
                        solde+=depot;// On ajoute la somme déposée au solde.
                        Journalisation.getInstance().ajouterLog("Dépôt de "+depot+"€ sur le compte "+numero+".");
                }
                else
                {
                        Journalisation.getInstance().ajouterLog("/!\\ Dépôt d'une valeur négative impossible ("+numero+").");
                }
        }
       
        // Méthode qui permet de retirer de l'argent sur le compte.
        public void retirerArgent(double retrait)
        {
                if(retrait>0.0)
                {
                        if(solde>=retrait)
                        {
                                solde-=retrait;// On retranche la somme retirée au solde.
                                Journalisation.getInstance().ajouterLog("Retrait de "+retrait+"€ sur le compte "+numero+".");
                        }
                        else
                        {
                                Journalisation.getInstance().ajouterLog("/!\\ La banque n'autorise pas de découvert ("+numero+").");
                        }
                }
                else
                {
                        Journalisation.getInstance().ajouterLog("/!\\ Rerait d'une valeur négative impossible ("+numero+").");
                }
        }
}

La classe CompteBancaire correspond à un compte. Celui-ci possède un numéro (identifiant) et un solde. Il est possible de déposer de l’argent ou d’en retirer grâce aux méthodes deposerArgent(double) et retirerArgent(double). De plus, certaines vérifications sont effectuées notamment pour éviter un découvert (notre banque ne fait pas crédit). Ces deux dernières méthodes utilisent la classe Journalisation pour tracer les opérations. On remarque les appels à la méthode getInstance() pour obtenir une instance unique de cette classe.

Implémentation de la classe Main

// Classe principale de l'application.
public class Main
{
        // Méthode principale.
        public static void main(String[] args)
        {
                // Création et utilisation du CompteBancaire cb1.
                CompteBancaire cb1 = new CompteBancaire(123456789);
                cb1.deposerArgent(100);
                cb1.retirerArgent(80);
                // Création et utilisation du CompteBancaire cb2.
                CompteBancaire cb2 = new CompteBancaire(987654321);
                cb2.retirerArgent(10);
                // Affichage des logs en console.
                String s = Journalisation.getInstance().afficherLog();
                System.out.println(s);
        }

}

La classe Main est la classe principale de notre programme. Elle va permettre d’exécuter un exemple et d’afficher son résultat en sortie. En effet, sur le compte numéro 123456789 on va déposer 100€ et retirer 80€. Puis sur le compte 987654321 on va retirer 10€ (alors qu’il n’y a pas d’argent sur ce compte).

Résultat en console

[30/04/07 13h46] Dépôt de 100.0€ sur le compte 123456789.
[30/04/07 13h46] Retrait de 80.0€ sur le compte 123456789.
[30/04/07 13h46] /!\ La banque n'autorise pas de découvert (987654321).

Le résultat obtenu est conforme à notre attente. On constate que tous les messages ont été ajoutés à une seule instance de Journalisation.


Extension du problème

Notre application fonctionne à merveille, mais le banquier nous fait remonter un souci. La banque dispose de beaucoup de clients et il est difficile d’isoler les messages concernant les opérations effectuées des messages concernant les opérations refusées. Or les messages confirmant une opération sont utilisés à des fin de statistiques alors que les messages concernant les opérations refusées doivent être étudiés plus en détails. Pour séparer ces deux types de messages nous allons nous servir du pattern Multiton.

 

Implémentation de la solution basée sur Multiton

Diagramme UML de l'implémentation en Java du Multiton


Le diagramme UML du Multiton diffère peu de celui du Singleton. On retrouve le constructeur en privé et le pseudo-constructeur getInstance(). En revanche l’attribut nommé instances est de type table de hashage et peut donc contenir plusieurs instances de Journalisation. La structure de la classe CompteBancaire est identique à celle présente.

Partie modifiée de l’implémentation de la classe Journalisation

//Classe basée sur le pattern Singleton qui permet la journalisation de l'application.
public class Journalisation
{
        private static HashMap instances = new HashMap();// Table de hashage d'instance (clef, valeur).
        private String log;// Chaine de caractères représentant les messages de log.
       
        // Constructeur en privé (donc inaccessible à l'extérieur de la classe).
        private Journalisation()
        {
                log = new String();
        }
       
        // Méthode statique qui sert de pseudo-constructeur (utilisation du mot clef "synchronized" pour le multithread).
        public static synchronized Journalisation getInstance(String clef)
        {
                Journalisation inst = instances.get(clef);
                if (inst == null)
                {
            inst = new Journalisation();
            instances.put(clef, inst);
                }
                return inst;
        }
//...
}

Voici les modifications apportées à la classe Journalisation pour répondre au pattern Multiton. A la place d’un attribut contenant l’unique instance de Jounalisation on place une table de hashage. Ainsi on peut stocker plusieurs instances chacune identifiée par une clef. On peut donc obtenir une instance pour les opérations effectuées et une instance pour les opérations refusées.

Partie modifiée de l’implémentation de la classe CompteBancaire

//Classe représentant un compte bancaire simpliste.
public class CompteBancaire
{
        //...

        // Méthode qui permet de déposer de l'argent sur le compte.
        public void deposerArgent(double depot)
        {
                if(depot>0.0)
                {       
                        solde+=depot;// On ajoute la somme déposée au solde.
                        Journalisation.getInstance("informations").ajouterLog("Dépôt de "+depot+"€ sur le compte "+numero+".");
                }
                else
                {
                        Journalisation.getInstance("erreurs").ajouterLog("/!\\ Dépôt d'une valeur négative impossible ("+numero+").");
                }
        }
       
        // Méthode qui permet de retirer de l'argent sur le compte.
        public void retirerArgent(double retrait)
        {
                if(retrait>0.0)
                {
                        if(solde>=retrait)
                        {
                                solde-=retrait;// On retranche la somme retirée au solde.
                                Journalisation.getInstance("informations").ajouterLog("Retrait de "+retrait+"€ sur le compte "+numero+".");
                        }
                        else
                        {
                                Journalisation.getInstance("erreurs").ajouterLog("/!\\ La banque n'autorise pas de découvert ("+numero+").");
                        }
                }
                else
                {
                        Journalisation.getInstance("erreurs").ajouterLog("/!\\ Rerait d'une valeur négative impossible ("+numero+").");
                }
        }
}

Les modifications de CompteBancaire concerne l’implémentation des méthodes deposerArgent(double) et retirerArgent(double). En effet, maintenant que nous gérons deux instances il faut spécifier l’instance dans laquelle le message doit être stocké.

Implémentation de la classe Main

//Classe principale de l'application.
public class Main
{
        // Méthode principale.
        public static void main(String[] args)
        {
                // Création et utilisation du CompteBancaire cb1.
                CompteBancaire cb1 = new CompteBancaire(123456789);
                cb1.deposerArgent(100);
                cb1.retirerArgent(80);
                // Création et utilisation du CompteBancaire cb2.
                CompteBancaire cb2 = new CompteBancaire(987654321);
                cb2.retirerArgent(10);
                // Affichage des logs "informations" en console.
                String s = Journalisation.getInstance("informations").afficherLog();
                System.out.println("Informations :\n"+s);
                // Affichage des logs "erreurs" en console.
                s = Journalisation.getInstance("erreurs").afficherLog();
                System.out.println("Erreurs :\n"+s);
        }

}

Retouchons notre classe Main pour afficher les modifications dans la console. En effet, nous allons afficher distinctement les informations (opérations réussies) des erreurs (opérations ayant échouées).

Résultat en console

Informations :
[30/04/07 13h48] Dépôt de 100.0€ sur le compte 123456789.
[30/04/07 13h48] Retrait de 80.0€ sur le compte 123456789.

Erreurs :
[30/04/07 13h48] /!\ La banque n'autorise pas de découvert (987654321).

Le résultat obtenu est conforme aux souhaits du banquier. Pour cela on a utilisé deux instances de Journalisation une gérant les informations l’autre les erreurs.


Conclusion

L’implémentation du pattern Singleton en Java est relativement simple. Il en va de même pour le pattern Multiton. Attention tout de même à garder à l’esprit que le pattern Multiton tel qu’il est présenté ici ne permet pas de garantir un nombre maximum d’instances. En effet, il suffit d’utiliser une nouvelle clef pour créer une nouvelle instance. On remarque également que le passage du pattern Singleton à Multiton engendre des modifications dans toutes les parties de l’application utilisant la classe. Mieux vaut donc bien réfléchir avant de choisir le pattern Singleton ou Multiton. Maintenant que vous avez compris le principe vous pouvez adapter ces patterns à des besoins spécifiques.

Code source: 
Commentaires

Bonjour,

Tout d'abord chapeau pour votre site qui contient des informations intéressantes.

Je pense qu'il y a un problème avec la journalisation des évènements en cas d'accès concurrent. Le "synchronized" sur getInstance() permet d'éviter de créer l'instance plusieurs fois. Si vous l'avez fait c'est pour les accès concurrents, mais dans ce cas que se passe-t-il en cas d'accès simultané de plusieurs comptes à la méthode ajouterLog()

Journalisation.getInstance().ajouterLog();

La variable interne "String log" n'est-elle pas exposée à des modifications simultanées?

Ajouter un commentaire