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

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

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.