Aller au menu - Aller au contenu

Icône TP événements et météo

Mise à jour : 02/02/2012
Difficulté : Intermédiaire Intermédiaire Creative Commons BY-NC-SA
22 955 visites depuis 7 jours, dont 196 sur ce chapitre classé 15/786
Bienvenue dans le dernier TP de cette partie.
Tenez bon, après on change de domaine pour aborder d'autres notions.
Dans ce TP, nous allons pratiquer les événements. Le but est de savoir en créer un et de pouvoir s'y abonner pour être notifié d'une information.

Vous êtes prêts ? Alors c'est parti ! :)
Sommaire du chapitre :
Icône du chapitre
Chapitre précédent Sommaire Chapitre suivant

Instructions pour réaliser le TP

Nous allons réaliser ici un mini simulateur de météo qui sera utilisé par un statisticien afin d’en faire des statistiques (logique :p ). Bon, c’est le contexte, mais c’est juste un exemple. N’espérez pas non plus réaliser un vrai simulateur météo dans ce TP. :)

Bref.
Nous devons créer un simulateur de météo. Lorsque celui-ci est démarré, il génère autant de nombres aléatoires que demandé, des nombres entre 0 et 100.
Si le nombre aléatoire est inférieur à 5, alors cela veut dire que le temps est au soleil.
S’il est supérieur ou égal à 5 et inférieur à 50, alors nous aurons des nuages.
S’il est supérieur ou égal à 50 et inférieur à 90, alors nous aurons de la pluie. Sinon, nous aurons de l’orage.

Un événement sera levé à chaque changement de temps. Le but de notre statisticien est de s’abonner aux événements du simulateur météo afin de compter le nombre de fois où il a fait soleil et le nombre de fois où le temps a changé. Il affichera ensuite son rapport en indiquant ces deux résultats et le pourcentage de fois où il a fait soleil. (Je veux bien que ce pourcentage soit un entier).

C’est tout pour l’énoncé. Maintenant, vous avez assez de connaissances pour que je ne détaille pas plus.
Bon courage !

Correction

Allez, c’est parti pour la correction.
Tout d’abord, nous devons créer notre simulateur météo. Il sera représenté par une classe :

Code : C#
1
2
3
public class SimulateurMeteo
{
}


Nous aurons également besoin de quelque chose pour représenter le temps, soleil, nuage, pluie et orage. Une énumération semble appropriée :

Code : C#
1
2
3
4
5
6
7
public enum Temps
{
    Soleil,
    Nuage,
    Pluie,
    Orage
}


Enfin, nous aurons notre statisticien :

Code : C#
1
2
3
public class Statisticien
{
}


Commençons par le simulateur de météo. Nous aurons besoin de plusieurs variables membres afin de stocker notre générateur de nombre aléatoires, le dernier temps qu’il a fait et le nombre de répétitions.
Le nombre de répétitions pourra être un paramètre du constructeur :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class SimulateurMeteo
{
    private Temps? ancienTemps;
    private int nombreDeRepetitions;
    private Random random;

    public SimulateurMeteo(int nombre)
    {
        random = new Random();
        ancienTemps = null;
        nombreDeRepetitions = nombre;
    }
}


Étant donné que nous allons déterminer plusieurs nombres aléatoires, il est pertinent de ne pas ré-allouer à chaque fois le générateur de nombre aléatoire. C’est pour cela qu’on le crée une unique fois dans le constructeur de la classe.
Créons désormais une méthode permettant de démarrer le simulateur et codons les règles métiers du simulateur :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class SimulateurMeteo
{
    //[…] Code supprimé pour plus de clarté […]

    public void Demarrer()
    {
        for (int i = 0; i < nombreDeRepetitions; i++)
        {
            int valeur = random.Next(0, 100);
            if (valeur < 5)
                GererTemps(Temps.Soleil);
            else
            {
                if (valeur < 50)
                    GererTemps(Temps.Nuage);
                else
                {
                    if (valeur < 90)
                        GererTemps(Temps.Pluie);
                    else
                        GererTemps(Temps.Orage);
                }
            }
        }
    }
}


C’est très simple, on boucle sur le nombre de répétitions. Un nombre aléatoire est déterminé à chaque itération. La méthode GererTemps prend en paramètre le temps déterminé à partir du nombre aléatoire.
C’est cette méthode GererTemps qui aura pour but de lever un événement quand le temps change :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class SimulateurMeteo
{
    public delegate void IlFaitBeauDelegate(Temps temps);
    public event IlFaitBeauDelegate QuandLeTempsChange;

    // […] Code supprimé pour plus de clarté […]

    private void GererTemps(Temps temps)
    {
        if (ancienTemps.HasValue && ancienTemps.Value != temps && QuandLeTempsChange != null)
            QuandLeTempsChange(temps);
        ancienTemps = temps;
    }
}


Ici, j’ai choisi de créer un seul événement quand le temps change et de lui indiquer le temps qu’il fait en paramètres.
Nous avons donc besoin d’un délégué qui prend un Temps en paramètres et qui ne renvoie rien. (C’est d’ailleurs souvent le cas des événements). Puis nous avons besoin d’un événement du type du délégué.
Ensuite, si le temps a changé et que quelqu’un s’est abonné à l’événement, alors nous levons l’événement.

Il ne reste plus qu’à remplir notre classe Statisticien. Cette classe a besoin de travailler sur une instance de la classe SimulateurMeteo, nous pouvons donc lui en passer une dans les paramètres du constructeur. Nous utiliserons également des variables membres privées permettant de stocker ses analyses :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Statisticien
{
    private SimulateurMeteo simulateurMeteo;
    private int nombreDeFoisOuLeTempsAChange;
    private int nombreDeFoisOuIlAFaitSoleil;

    public Statisticien(SimulateurMeteo simulateur)
    {
        simulateurMeteo = simulateur;
        nombreDeFoisOuLeTempsAChange = 0;
        nombreDeFoisOuIlAFaitSoleil = 0;
    }

    public void DemarrerAnalyse()
    {
        simulateurMeteo.QuandLeTempsChange += simulateurMeteo_QuandLeTempsChange;
        simulateurMeteo.Demarrer();
    }

    public void AfficherRapport()
    {
        Console.WriteLine("Nombre de fois où le temps a changé : " + nombreDeFoisOuLeTempsAChange);
        Console.WriteLine("Nombre de fois où il a fait soleil : " + nombreDeFoisOuIlAFaitSoleil);
        Console.WriteLine("Pourcentage de beau temps : " + nombreDeFoisOuIlAFaitSoleil * 100 / nombreDeFoisOuLeTempsAChange + " %");
    }

    private void simulateurMeteo_QuandLeTempsChange(Temps temps)
    {
        if (temps == Temps.Soleil)
            nombreDeFoisOuIlAFaitSoleil++;
        nombreDeFoisOuLeTempsAChange++;
    }
}


Notons que dans la méthode DemarrerAnalyse, nous nous abonnons à l’événement de changement de temps. La méthode qui est appelée lors de la notification est très simple, elle incrémente les compteurs.
Enfin, l’affichage du rapport est trivial. Ici, comme nous n’avons que des entiers, la division produira un entier également.
Il ne reste plus qu’à faire fonctionner nos objets :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Program
{
    static void Main(string[] args)
    {
        SimulateurMeteo simulateurMeteo = new SimulateurMeteo(1000);
        Statisticien statisticien = new Statisticien(simulateurMeteo);
        statisticien.DemarrerAnalyse();
        statisticien.AfficherRapport();
    }
}


Ici, je travaille sur 1000 répétitions.
Lorsque j’exécute l’application, j’obtiens :

Image utilisateur


Évidemment, vu que nous travaillons avec des nombres aléatoires, chacun aura un résultat différent.

Et voilà, c’est terminé pour ce TP. Notre application est fonctionnelle …
Terminé ? mmmhhh … pas tout à fait, allons un peu plus loin.

Aller plus loin

En l’état, ce code est fonctionnel. C’est parfait. Mais que se passe-t-il si nous démarrons plusieurs fois l’analyse ?
Nous pouvons essayer :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
static void Main(string[] args)
{
    SimulateurMeteo simulateurMeteo = new SimulateurMeteo(1000);
    Statisticien statisticien = new Statisticien(simulateurMeteo);
    statisticien.DemarrerAnalyse();
    statisticien.AfficherRapport();

    statisticien.DemarrerAnalyse();
    statisticien.AfficherRapport();

    statisticien.DemarrerAnalyse();
    statisticien.AfficherRapport();
}


Nous obtiendrons quelque chose du genre :

Image utilisateur


Les valeurs augmentent … alors qu’elles ne devraient pas.
Eh oui, nous ne réinitialisons pas les entiers permettant de stocker les statistiques. Ce n’est pas forcément un bug ici, comme je n’avais rien dit dans l’énoncé, il peut paraitre pertinent de continuer à incrémenter ces valeurs, comme ça, je peux travailler sur une moyenne.
Bon, disons que nous souhaitons les réinitialiser à chaque fois, il suffit de remettre à zéro les compteurs dans la méthode :

Code : C#
1
2
3
4
5
6
7
public void DemarrerAnalyse()
{
    nombreDeFoisOuLeTempsAChange = 0;
    nombreDeFoisOuIlAFaitSoleil = 0;
    simulateurMeteo.QuandLeTempsChange += simulateurMeteo_QuandLeTempsChange;
    simulateurMeteo.Demarrer();
}


Cependant, les compteurs augmentent toujours, moins vite, mais quand même !
Je suis sûr que vous l’avez deviné et que vous n’avez-vous-même pas fait l’erreur. En fait, cela vient de l’abonnement à l’événement. Chaque fois que nous démarrons l’analyse, nous nous réabonnons à l’événement. Comme l’événement est multidiffusion, nous rajoutons en fait à chaque fois un appel à la méthode avec le +=. Ce qui veut dire qu’à la deuxième fois, nous appellerons la méthode deux fois, ce qui fera doubler les résultats. À la troisième fois, ils triplent …
Nous avons donc une erreur. Soit il faut s’abonner une seule fois à l’événement, par exemple dans le constructeur, soit nous pouvons nous désabonner à la fin de l’analyse, quand ce n’est plus utile de recevoir l’événement.
C’est cela que je souhaite montrer. Il suffit de faire :

Code : C#
1
2
3
4
5
6
7
8
public void DemarrerAnalyse()
{
    nombreDeFoisOuLeTempsAChange = 0;
    nombreDeFoisOuIlAFaitSoleil = 0;
    simulateurMeteo.QuandLeTempsChange += simulateurMeteo_QuandLeTempsChange;
    simulateurMeteo.Demarrer();
    simulateurMeteo.QuandLeTempsChange -= simulateurMeteo_QuandLeTempsChange;
}


On utilise l’opérateur -= pour enlever la méthode du délégué.

D’une manière générale, il est bienvenu de se désabonner d’un événement lorsque l’on sait qu’on ne va plus s’en servir.

Cela permet d’éviter d’encombrer la mémoire qui ne saurait pas forcément se libérer toute seule. Je n’en dis pas plus car ceci est un concept avancé de gestion de mémoire. Gardez seulement à l’esprit que si on a l’opportunité de se désabonner d’un événement, il faut le faire.

Enfin, nous pouvons simplifier notre code en ne créant pas notre délégué. Effectivement, dans la mesure où celui-ci possède un seul paramètre, il est possible de le remplacer par le délégué Action<T>.
Il faut juste supprimer la déclaration du délégué et remplacer la déclaration de l’événement par :

Code : C#
1
2
3
4
public class SimulateurMeteo
{
    public event Action<Temps> QuandLeTempsChange;
}


Voilà pour ce TP sur les événements. Il nous a permis de nous entrainer un peu sur les événements et de continuer à nous entrainer sur la modélisation d’applications en utilisant la POO.
Pas très compliqué en soit, mais on peut vite se rendre compte qu’une application peut fonctionner dans un cas, mais avoir un comportement inadapté dans un autre cas.
D’une manière générale, il est important de retenir qu’il faut se désabonner d’un événement quand cela est possible.
Chapitre précédent Sommaire Chapitre suivant

Partager

3 commentaires pour "TP événements et météo"
Note moyenne : 3.05 / 4 (230 votes)
Pseudo Commentaire
Hors ligne gfox78 # Posté le 07/04/2012 à 16:14:29
Avatar

Avis : Très bon

Ville : Marcq
Pays : France métropolitaine

La correction de ce TP m'éclaire sur l'utilisation de "event" dont je n'avais pas bien compris l'utilisation.

Par ailleurs je pense qu'un tout petit rappel sur la convention "?" dans :
" private Temps? ancienTemps; "
serait nécessaire car elle n'a été évoquée que dans la toute dernière ligne du chapitre "Nullable" et elle n'est pas facile à retrouver dans la doc MSDN.
Le TP montre donc aussi l'intérêt de ces types Nullables pour éviter les bugs de bordure.
Hors ligne Eagleseb # Posté le 02/05/2012 à 16:41:54
Avatar

Avis : Décevant

Quel tp original, constructif et intéressant... :(
Hors ligne gfox78 # Posté le 06/05/2012 à 11:26:59
Avatar

Avis : Très bon

Ville : Marcq
Pays : France métropolitaine

Bonjour,
Je pense qu'il faut changer impérativement le libellé de certains résultats dans le corrigé du TP-météo.
Code : PHP
1
2
3
4
5
6
7
8
<?php
 public void AfficherRapport()
    {
        Console.WriteLine("Nombre de fois où le temps a changé : " + nombreDeFoisOuLeTempsAChange);
        Console.WriteLine("Nombre de fois où il a fait soleil : " + nombreDeFoisOuIlAFaitSoleil);
        Console.WriteLine("Pourcentage de beau temps : " + nombreDeFoisOuIlAFaitSoleil * 100 / nombreDeFoisOuLeTempsAChange + " %");
    }
?>

Le "pourcentage de beau temps" est fixé à 5 pcents par les données et ne correspond pas au résultat obtenu. En fait, le troisième résultat correspond au "pourcentage des changements qui ont conduit vers le beau temps". Il faut, de la même façon changer le libellé du deuxième résultat.

J'ai aussi une autre remarque sur les précédents chapitres: les noms des variables, méthodes, etc... sont trop longs. Même s'ils sont très indicatifs, ils gênent fortement la lecture et la compréhension, surtout lorsque ces noms se ressemblent. Mais après tout, c'est peut-être nécessaire pour un tutoriel et c'est à l'utilisateur de renommer ces éléments : c'est déjà un bon exercice!

J'en profite pour donner ma solution du TP réécrite en se basant sur le corrigé. J'ai créé un modèle climatique sous forme d'un tableau de caractères représentant les types de temps et leurs fréquences. Cela permet de fournir facilement les fréquences cumulées et le nombre de types de temps dans le 'Main', sans modifier le reste du code. J'ai ajouté aussi des résultats sur les 'changements de temps conduisant vers une amélioration'. Cette fréquence doit se rapprocher évidemment de 50 pcents, ce qui permet de vérifier la qualité du générateur de nombres pseudo-aléatoires. L'ajout de ce calcul me permet aussi d'utiliser le délégué générique 'Action'.

Secret (cliquez pour afficher)

Code : PHP
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
<?php
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace tpmeteo3
{
    class PgMeteo
    {      
        public class FaitMeteo
        {
            private static bool dejaVu = false;
            private char tempsHier = '\0', tempsAuj;   // temps d'Hier et temps d'Aujourd'Hui         
            private int nbJours;                 // nombre de répetitions
            private Random rnd;                  // initialisation du générateur dans le Main, par sécurité   
            private char[] modelClimat = new char[100];
            private static int nbClim;
            private string[] nomClimat;
            private int[] repartClimat;

            public delegate void ChgDelegate(char tempsAuj);
            public event ChgDelegate ChgTemps;   // changement de temps
            //public event ChgDelegate AmelTemps;  // amélioration ...< Tempête < Pluie < Nuages < ... < Beau temps
            public event Action<char> AmelTemps;   // option en utilisant Action<> plutot que le delelegate

            // constructeur
            public FaitMeteo(int NbJours, Random random, int[] RepartClimat, string[] nomClim)
            {
                tempsHier = '\0';
                nbJours = NbJours;
                rnd = random;
                repartClimat = RepartClimat;
                nbClim = RepartClimat.Length - 1;  
                nomClimat = nomClim;
            }

            public void RepartitionClimat()
              /* les types de climats sont codés par un seul caractère (A,B,C,D...) et sont placés consécutivement 
               *   dans un  tableau climat[100] selon la répartition imposée par repartClimat. */
            {
                for (int x = 0; x < repartClimat.Length - 1; x++)
                {
                    string suite = "";
                    for (int n = repartClimat[x]; n < repartClimat[x + 1]; n++)
                    { 
                        if(n == repartClimat[x+1]-1) suite = "(suite)";
                        modelClimat[n] = (char)(x + 65);   // + 65 pour faire un caractère majuscule A,B,C,...
                        Console.Write(modelClimat[n]);
                        if (n > 0 && (n + 1) % 50 == 0) Console.Write("   modèle climatique {0}\n", suite);
                    }
                }
                Console.WriteLine();
            }

            public void GenereClimat()
            {
                int[] statClim = new int[nbClim];           // pour compter le nombre d'occurence des types de climats
                int verifTotal = 0;
                for (int n = 0; n < nbClim; n++) { statClim[n] = 0; } // initialisation
                for (int i = 0; i < nbJours; i++)
                {
                    int valeur = rnd.Next(0, 100);
                    tempsAuj = modelClimat[valeur];
                    EventClimat();                   
                    statClim[tempsAuj - 65]++;       // - 65 pour revenir à un indice acceptable 1, 2,...            
                }
                for (int n = 0; n < nbClim; n++)
                {
                    Console.Write("{0}={1,5}", nomClimat[n], statClim[n]);
                    if (n < nbClim - 1) { Console.Write(", "); } else { Console.Write("."); }
                    if (n != 0 && n % 4 == 0) Console.Write("\n");
                    verifTotal += statClim[n];              // verification peu utile
                }
                if (!dejaVu)
                {
                    Console.WriteLine("\nNombre de jours étudiés = {0,6}", verifTotal);
                    dejaVu = true;
                }
                else { Console.WriteLine(); }
            }

            private void EventClimat()    
            {
                if ((tempsHier != '\0') && (tempsHier != tempsAuj))
                {
                    ChgTemps(tempsAuj);
                    if ((tempsHier != '\0') && (tempsAuj < tempsHier)) AmelTemps(tempsAuj);
                }
                tempsHier = tempsAuj;
            }
        }

        public class EtudeMeteo
        {
            private FaitMeteo faitMeteo;
            private int nbTempsChg;
            private int nbAmelTemps;
            private int nbJBeauT;  // nb de jour de beau temps
            int nbJours;
            private void initData()
            {
                nbTempsChg = 0;
                nbJBeauT = 0;
                nbAmelTemps = 0;
            }

            // constructeur
            public EtudeMeteo(FaitMeteo fabMeteo, int NbJours)  // nbjours = nombre de répétitions
            {
                faitMeteo = fabMeteo;
                initData();               // Est-ce utile ? non!
                nbJours = NbJours;
            }

            public void AnalyseMeteo()
            {
                faitMeteo.ChgTemps += faitMeteo_ChgTemps;  // abonnements aux événements
                faitMeteo.AmelTemps += faitMeteo_AmelTemps;
                
                faitMeteo.GenereClimat();    // noeud du programme
                
                faitMeteo.ChgTemps -= faitMeteo_ChgTemps;   // désabonnements nécessaire pour une utilisation suivante
                faitMeteo.AmelTemps -= faitMeteo_AmelTemps; //  .
            }

            public void DonneResultat()
            {
                Console.WriteLine("Nombre de fois où le temps a changé : " + nbTempsChg);
                Console.WriteLine("Nombre de fois où le changement a conduit au beau temps : " + nbJBeauT);
                Console.WriteLine("Nombre de fois où le temps s'améliore : " + nbAmelTemps);
                Console.WriteLine("Pourcentage de jours où le temps change : {0:F2}%", (double)(nbTempsChg * 100) / nbJours);
                Console.WriteLine("Pourcentage de changement vers le beau temps : {0:F2}%", (double)(nbJBeauT * 100) / nbTempsChg);
                Console.WriteLine("Pourcentage de changement vers une amélioration : {0:F2}%", (double)(nbAmelTemps * 100) / nbTempsChg);
                initData();                 // réinitialisation pour utilisation ultérieure               
            }

            private void faitMeteo_ChgTemps(char tempsAuj)
            {
                if (tempsAuj == 'B')
                    nbJBeauT++;
                nbTempsChg++;
            }
            private void faitMeteo_AmelTemps(char tempsAuj)
            {
                nbAmelTemps++;
            }
        }

        class Program
        {
            static void LanceEtudeMeteo(int[] repartClimat, string[] nomClimat, int nbJours, int nbRepEtude)
            {
                Random random = new Random();        //  new Random(100) pour Mise au Point (MSP)                
                    // iniatialisation du générateur ici, au début, pour éviter probleme en cas d'exécution trop rapide           
                FaitMeteo faitMeteo = new FaitMeteo(nbJours, random, repartClimat, nomClimat);              
                EtudeMeteo etudeMeteo = new EtudeMeteo(faitMeteo, nbJours);
                faitMeteo.RepartitionClimat();
                for (int n = 0; n < nbRepEtude; n++)
                {
                    etudeMeteo.AnalyseMeteo();
                    etudeMeteo.DonneResultat();
                    Console.WriteLine();
                }
            }
            static void Main(string[] args)
            {
                // à transformer pour une entrée manuelle des données
                int nbJours = 1000000;               // nombre de jours etudiés
                int nbRepEtude = 4;
                int[] repartClimat = new int[] { 0, 10, 30, 60, 90, 97, 100 }; // en cumul, du beau temps vers le mauvais temps
                string[] nomClimat = new string[] { "Soleil", "Variable", "Nuage", "Pluie", "Tempête", "Neige" };
                if (repartClimat.Length - 1 != nomClimat.Length)
                {
                    Console.Write("Incohérence dans les données 'repartClimat' et 'nomClimat'");
                    return;
                }
                LanceEtudeMeteo(repartClimat, nomClimat, nbJours, nbRepEtude);
            }
        }
    }
}
?>


Amicalement

Voir tous les commentaires