Extension des listes génériques (design pattern "decorateur")

Soyez le premier à donner votre avis sur cette source.

Vue 8 857 fois - Téléchargée 314 fois

Description

La source que je vous présente aujourd'hui permet de mettre en avant plusieurs principes de développement assez intéressants :
- La création d'une classe avec les génériques,
- L'implémentation de interface IList,
- L'extension des actions de la classe générique List<T>,
- Les méthodes anonymes,
- La sauvegarde des évènements lors d'une sérialisation,
- Le principe "Thread-safe".

Le but principal de la classe EvtList (qui fait l'objet principal de cette source), est de fournir des évènements lors de l'ajout/suppression d'un élément dans celle-ci. On travaille ici sur une liste de dates, mais peu importe, vous pouvez utiliser cette liste avec n'importe quel type d'objets.

La création d'une classe avec les génériques :
La classe EvtList doit être instanciée en fournissant le type de la donnée qui est stockée dans la liste. On ne va pas revenir sur les avantages des génériques ici, mais je suppose que vous en connaissez les bénéfices (sécurité de type, ect...). Au passage, si vous voulez contraindre la liste à ne stocker qu'un certain type d'objet (et ses sous-types), vous pouvez utiliser la clause where, que j'ai ici laissée en commentaire.

L'implémentation de interface IList :
Vous pouvez voir ici une implémentation de la classe IList.

L'extension des actions de la classe générique List<T> :
On a dans l'EvtList une instance (privée) d'une classe implémentant l'interface IList<T>. Comme on fait de nouvelles actions lors de l'ajout/suppression d'un élément, on peut dire que l'on attache de nouvelles responsabilités à l'objet. Fin du fin, on peut déclarer la EvtList de deux manières : Sans paramètres dans le constructeur (C'est comme si on déclarait une List, mais avec des évènements en plus), ou avec un objet implémentant l'interface IList<T> en paramètre d'entrée du constructeur. Dans ce cas de figure, cela signifie que l'on attache de nouvelles responsabilités à un objet déjà existant ! Par exemple, la méthode Add, en plus d'ajouter un élément à la liste, appelle maintenant divers évènements. Si ce principe vous dit quelque chose, c'est car il s'agit ici du design pattern décorateur.

Les méthodes anonymes :
Vous pourrez voir dans le code du formulaire principal quelques méthodes anonymes. Autrement dit, on ne prend plus la peine de déclarer des méthodes pour répondre aux évènements, mais on fait déclare des méthodes dans des méthodes. A voir si vous trouvez ça plus lisible ou pas, mais rappelons nous que nous sommes dans un tutorial ;-)

La sauvegarde des évènements lors d'une sérialisation :
Notre liste lance des évènements mais il faut savoir une chose : ils sont sauvegardés lors de la sérialisation. Or, même si notre objet est sérialisable, la sauvegarde va échouer si l'objet posant l'écouteur sur la liste n'est pas sérialisable. Dans ce but, les évènements onAdd et onRemove sont des champs (fields) marqués non sérialisables.
Le souci, c'est que l'on peut vouloir inclure cette définition de la classe EvtList dans notre DLL pour gérer nos objets métiers. Or, tous les objets de notre DLL sont sérialisables, et nous voulons pouvoir sauvegarder les références aux différents évènements et ne pas les perdre lors des opérations de sérialisation/désérialisation. C'est pour cela que nous avons dans la EvtList les évènements onAddInternal et onRemoveInternal, qui sont sérialisables, comme le reste de la classe, mais qui ne sont accessibles qu'à notre namespace (internal).

Le principe "Thread-safe" :
Les Thread.BeginCriticalRegion(); permettent ici d'avoir des listes qui sont thread-safe, en quelque sorte.
Je ne sais pas si vous avez vu, mais on lève une exception si un même élément est ajouté 2 fois à la liste. Imaginons 2 threads qui utilisent tous les 2 la méthode Add en même temps, et avec le même objet à ajouter. S'ils arrivent tous les 2 en même temps à la ligne "if (InternalList.Contains(item))", ils vont passer le test avec succès, et ajouter 2 fois (1 fois par thread) le même élément à la liste. Le lock permet d'éviter ce souci en indiquant à la CLR que tout ce qui ce trouve entre les accolades de ce mot clé ne doit être compris que comme une seule et même instruction "unitaire". Il s'agit d'un vérou par exclusion mutuelle. (merci à Yxion : j'avais oublié de mettre ça dans la description de ma classe).

Source / Exemple :


/// <summary>
    /// Classe permettant de stocker une liste d'objets dont le type est de type T.
    /// Cette liste peut lever des évènements lors d'un ajout ou lors d'une suppression.
    /// Cette liste peut aussi lever des exception lorsque :
    /// - On tente d'ajouter un objet null à la liste,
    /// - On tente de retirer un objet null de la liste,
    /// - On tente d'ajouter un même objet plusieurs fois à la liste,
    /// - On tente de supprimer un objet de la liste alors que celui-ci n'existe pas.
    /// </summary>
    /// <typeparam name="T">Type de l'objet stocké dans la liste.</typeparam>
    [Serializable]
    public class EvtList <T> : IList<T>, IEnumerable, IEnumerable<T> // where T : VotreTypeObjetFari
    {
    	#region Privates

        /// <summary>
        /// Permet de stocker les éléments de la liste.
        /// </summary>
        private IList<T> InternalList = null;

        #endregion

        #region Events

        /// <summary>
        /// Survient lors de l'ajout d'un élément à la liste.
        /// </summary>
        public event EventHandler onAddInternal;

        /// <summary>
        /// Survient lors de l'ajout d'un élément à la liste.
        /// </summary>
        [field: NonSerialized]
        public event EventHandler onAdd;

        /// <summary>
        /// Survient lors de la suppression d'un élément de la liste.
        /// </summary>
        internal event EventHandler onRemoveInternal;

        /// <summary>
        /// Survient lors de la suppression d'un élément de la liste.
        /// </summary>
        [field: NonSerialized]
        public event EventHandler onRemove;

        /// <summary>
        /// Permet de lancer tous les évènements associés à l'ajout d'un élément à la liste.
        /// </summary>
        /// <param name="itemAdded">L'objet ajouté.</param>
        protected void LaunchAddEvents(T itemAdded) {
            try {
        		// Construction de l'objet AddEventArgs permettant de communiquer quel objet
                // a été ajouté à la liste.
                EvtListAddEventArgs args = new EvtListAddEventArgs(itemAdded);

                // Lancement des évènements onAddInternal puis onAdd.
                if (onAddInternal != null) onAddInternal(this, args);
                if (onAdd != null) onAdd(this, args);

        	} catch (Exception ex) {
                throw ex;
            }
        }

        /// <summary>
        /// Permet de lancer tous les évènements associés à la suppression d'un élément à la liste.
        /// </summary>
        /// <param name="itemRemoved">L'objet supprimé.</param>
        protected void LaunchRemoveEvents(T itemRemoved) {
            try {
                // Construction de l'objet AddEventArgs permettant de communiquer quel objet
                // a été supprimé à la liste.
                EvtListRemoveEventArgs args = new EvtListRemoveEventArgs(itemRemoved);

                // Lancement des évènements onAddInternal puis onAdd.
                if (onRemoveInternal != null) onRemoveInternal(this, args);
                if (onRemove != null) onRemove(this, args);

        	} catch (Exception ex) {
                throw ex;
        	}
        }

        #endregion

        #region Constructors
        public EvtList(){
        	InternalList = new List<T>();
        }
        
        public EvtList(IList<T> list){
        	InternalList = list;
        }
    	#endregion
    	
        #region IList<T> Members

        /// <summary>
        /// Permet de récupérer l'index d'un élément de la liste.
        /// </summary>
        /// <param name="item">L'élément recherché.</param>
        /// <returns>L'index de l'élément recherché.</returns>
        public int IndexOf(T item) {
            return InternalList.IndexOf(item);
        }

        /// <summary>
        /// Permet d'insérer un élément dans la liste, à un index précis.
        /// </summary>
        /// <param name="index">L'index ou l'objet doit être ajouté.</param>
        /// <param name="item">L'élément à ajouter.</param>
        public void Insert(int index, T item) {
            try{
        		lock(InternalList){
        		
	                // Si l'élément qui est ajouté est null ou déjà dans la liste, on lève une exception.
	                if (item == null) throw new ObjectAddedToEvtListNullError();
	                if (InternalList.Contains(item)) throw new ObjectAllreadyInEvtListError();
	
	                InternalList.Insert(index, item); // Insertion de l'élément dans la liste.
	                LaunchAddEvents(item); // Lancement des évènements qui doivent survenir lors de l'ajout d'un élément de la liste.
        		}
        	} catch (Exception ex) {
        		throw ex;
        	}
        }
        
        /// <summary>
        /// Permet de savoir si la liste est en lecture seule ou non.
        /// </summary>
        public bool IsReadOnly
        {
            get { return false; }
        }

        /// <summary>
        /// Permet de supprimer un élément de la liste à l'emplacement spécifié.
        /// </summary>
        /// <param name="index">L'index de l'élément à supprimer.</param>
        public void RemoveAt(int index) {
            try{
                T RemovedObject = InternalList[index]; // Stockage de l'élément supprimé pour l'envoyer aux gestionnaires d'évènements.
                InternalList.RemoveAt(index); // Suppression de l'élément dans la liste.
                LaunchRemoveEvents(RemovedObject); // Lancement des évènements qui doivent survenir lors de la suppression d'un élément de la liste.
            
        	} catch (Exception ex) {
        		throw ex;
        	}
        }

        /// <summary>
        /// Accesseur indexé. Permet d'accéder ou de modifier l'un des éléments de la liste à partir
        /// d'un index.
        /// </summary>
        /// <param name="index">L'index recherché.</param>
        /// <returns>L'élémént recherché.</returns>
        public T this[int index] {
            get { return InternalList[index]; }
            
            set {
                try{
                    //if (InternalList[index] != value) {
                        LaunchRemoveEvents(InternalList[index]);
                        InternalList[index] = value;
                        LaunchAddEvents(InternalList[index]);
                    //}
                } catch (Exception ex) {
            		throw ex;
            	}
            }
        }

        #endregion

        #region ICollection<T> Members

        /// <summary>
        /// Permet d'ajouter un élément à la liste.
        /// </summary>
        /// <param name="item"></param>
        public void Add(T item) {
            try {
                //Thread.BeginCriticalRegion(); // Début d'une région de thread critique.
                lock(InternalList){
                	// Si l'élément qui est ajouté est null ou déjà dans la liste, on lève une exception.
	                if (item == null) throw new ObjectAddedToEvtListNullError();
	                if (InternalList.Contains(item)) throw new ObjectAllreadyInEvtListError();
	
	                InternalList.Add(item); // Insertion de l'élément dans la liste.
	                LaunchAddEvents(item); // Lancement des évènements qui doivent survenir lors de l'ajout d'un élément de la liste.
                }
            } catch (Exception ex) {
            	throw ex;
            }
        }

        /// <summary>
        /// Permet de vider la liste de tous ses éléments.
        /// </summary>
        public void Clear() {
            while (Count > 0) Remove(this[0]);
        }

        /// <summary>
        /// Permet de savoir si l'élément spécifié appartient à la liste.
        /// </summary>
        /// <param name="item">L'élément recherché.</param>
        /// <returns>True si l'élément appartient à la liste, false sinon.</returns>
        public bool Contains(T item) {
            return InternalList.Contains(item);
        }

        /// <summary>
        /// Permet de copier les éléments de la liste dans un tableau.
        /// </summary>
        /// <param name="array">Le tableau dans lequel copier les éléments.</param>
        /// <param name="arrayIndex">L'index de l'élément à partir duquel doit commencer la copie.</param>
        public void CopyTo(T[] array, int arrayIndex) {
            InternalList.CopyTo(array, arrayIndex);
        }

        /// <summary>
        /// Permet de récupérer le nombre d'éléments de la liste.
        /// </summary>
        public int Count {
            get { return InternalList.Count; }
        }

        /// <summary>
        /// Permet de supprimer un élément de la liste.
        /// </summary>
        /// <param name="item">L'élément à supprimer.</param>
        public bool Remove(T item) {
            try {
        		lock(InternalList){
	        		// Si l'élément qui est ajouté est null ou déjà dans la liste, on lève une exception.
	                if (item == null) throw new ObjectRemovedToEvtListNullError();
	                if (!InternalList.Contains(item)) throw new ObjectRemovedFromHLD_ListObjectsDoesNotExistsError();
	
	                InternalList.Remove(item); // Suppression de l'élément dans la liste.
	                LaunchRemoveEvents(item); // Lancement des évènements qui doivent survenir lors de la suppression d'un élément de la liste.
        		}
        	}catch (Exception ex) {
        		throw ex;
        	}
            return true;
        }

        #endregion

        #region IEnumerable<T> Members

        public IEnumerator<T> GetEnumerator(){
            return InternalList.GetEnumerator();
        }

        #endregion

        #region IEnumerable Members

        IEnumerator IEnumerable.GetEnumerator(){
            return InternalList.GetEnumerator();
        }

        #endregion
    }

Conclusion :


Code créé avec SharpDevelop.
Les listes génériques sont apparues avec la version 2 du Framework .net. Toute tentative de compilation avec une version antérieure du Framework se soldera par un échec.

Codes Sources

A voir également

Ajouter un commentaire

Commentaires

cs_yoannd
Messages postés
313
Date d'inscription
lundi 7 janvier 2002
Statut
Membre
Dernière intervention
10 août 2011
1 -
très juste TheManu ;)
TheManu
Messages postés
12
Date d'inscription
jeudi 26 juillet 2007
Statut
Membre
Dernière intervention
11 mai 2010
-
Sympa ta classe !
Par contre :
- Dans 'Clear()', pourquoi ne pas utiliser 'RemoveAt(0)' plutôt que 'Remove(this[0])' ?
De plus (et de toute façon), si un autre thread supprime entre le 'while' et le 'Remove' (ou le 'this[0]') un élément ET que celui-ci été le dernier t'es marron ! Je sais que c'est plus pour le tutorial mais je ne pense pas que tu puisses (pour simplifier l'écriture) te passer d'un 'lock' englobant la boucle d'effacement et le lancement d'évènements. Pour simplifier il faudrait (je pense) l'appel en boucle (et dans un 'lock') d'un "RemoveAtNonSafeThread(0)"...
- De même : l'énumération sur la liste (grâce à 'GetEnumerator') n'est pas "thread-safe" !
flashkel
Messages postés
5
Date d'inscription
vendredi 4 juin 2004
Statut
Membre
Dernière intervention
23 novembre 2007
-
Sauf qu'ici il fait un lock(base) ce qui revient (dans le contexte d'une implémentation d'une IList<T>) à ce que tu conseilles MALKUTH mais le conseil est judicieux pour d'autre cas.
cs_Malkuth
Messages postés
278
Date d'inscription
samedi 22 février 2003
Statut
Membre
Dernière intervention
24 avril 2013
2 -
Petite précision sur le lock, il est déconseillé de faire un
lock(this){ ... }

Car d'autre partie du programme pourais vouloir posé un vérou
sur l'objet pour des raison totalement différente que celle de
l'ajout d'un nouvel item dans la liste, pour ma part, je déclare
un un objet suplémentaire dans ma classe pour géré le lock :
private object modif_LOCK = new object();

On utilise ensuite :
lock(this.modif_LOCK) { ... }

On peut aussi noté que lock n'est pas une instruction du framework
à proprement parlé mais un artifice du compilateur C# qu'on peut
interprété comme ceci :
try
{
Sytem.Threading.Monitor.Enter(this.modif_LOCK);
...
}
finally
{
Sytem.Threading.Monitor.Exit(this.modif_LOCK);
}

Il est possible de dans certain cas d'utilisé d'autre classe que
Sytem.Threading.Monitor pour géré les vérroux comme par example :
ReaderWriterLock
qui permet de diférencié les opérations de lectures(qui ne se lock pas entre elles) des opérations d'écriture (qui sont exclusive).
cs_yoannd
Messages postés
313
Date d'inscription
lundi 7 janvier 2002
Statut
Membre
Dernière intervention
10 août 2011
1 -
Je viens de compulser l'aide msdn, et il s'avère que le lock correspond à ce que je voulais faire, alors que le thread.BeginCriticalRegion fait autre chose.

Petites explications :
lock : bloque l'accès à une ressource et traite toutes les instructions qu'il contient en une fois. Autrement dit, il y a exclusion mutuelle : ce mot clé permet de s'assurer qu'un thread ne se trouve pas dans la partie de code concernée en même temps qu'un autre thread.

thread.BeginCriticalRegion : permet de spécifier au sein d'un thread que la région entourée par le begin et le end ne peut pas être annulée sans mettre en péril les données de l'application. On indique ainsi qu'il y a une région critique entre le begin et le end. Définition officielle de la msdn : "Avertit un hôte que l'exécution est sur le point d'entrer dans une région de code dans laquelle les effets d'un abandon de thread ou d'une exception non gérée peuvent compromettre d'autres tâches dans le domaine d'application.".

Voila pour ce point, cela veut dire qu'il me faut modifier un peu ma source :-)

Vous n'êtes pas encore membre ?

inscrivez-vous, c'est gratuit et ça prend moins d'une minute !

Les membres obtiennent plus de réponses que les utilisateurs anonymes.

Le fait d'être membre vous permet d'avoir un suivi détaillé de vos demandes et codes sources.

Le fait d'être membre vous permet d'avoir des options supplémentaires.