Design Pattern Décorateur et protection d'objets en écriture par Patrick Smacchia

 

La problématique. 1

Le mot clé const en C#.. 1

Le mot clé const en C++.. 1

Solution 1: Utilisation du design pattern décorateur 1

Solution 2 : Une solution à base d’interfaces. 2

Solution 3 : Une autre solution à base d’interfaces. 3

Solution 4 : Utilisation du clonage d’objet 4

Conclusion. 5

 

La problématique

La problématique qui m’a été proposé par Thierry Paul qui était gêné par le fait qu’en C# le mot clé const ne peut pas être utilisé comme en C++, afin de rendre un type accessible en lecture seule.

 

Le mot clé const en C#

En effet, rappelons que le mot clé const en C# n’est utilisable que dans la déclaration d’un champ. L’application du mot clé const sur un champ a pour effet de le rendre non modifiable dans toutes les méthodes de la classe, y compris dans le constructeur. Il ne peut être initialisé que lors de sa déclaration. En fait le compilateur C# rend un tel champ automatiquement statique.

 

Le mot clé const en C++

Rappelons que le mot clé const en C++ utilisé devant un type, permet de définir un nouveau type. Le compilateur vérifie que les champs des instances d’un tel type ne sont pas accédées en écriture :

 

#include <string>

#include <iostream>

#include <cassert>

using namespace std;

 

class Article

{

public:

       string Nom;

       int Prix;

};

 

void fct(const Article &A)

{

       A.Prix = 110; // ERREUR DE COMPILATION

       cout << A.Nom << ": " << A.Prix << " euros";

}

 

int _tmain(int argc, _TCHAR* argv[])

{

       Article A;

       A.Nom = "Chaussure";

       A.Prix = 100;

       fct( A );

       assert( A.Nom == "Chaussure" );

       assert( A.Prix == 100 );

       return 0;

}

 

Pour pallier ce manque de C# par rapport à C++, nous allons utiliser les notions de propriétés, de getter et de setter. 

Solution 1: Utilisation du design pattern décorateur

 

La première solution qui m’est venue à l’esprit était d’utiliser le design pattern décorateur afin de décorer l’interface IArticle avec une classe ArticleReadOnly. Cette classe délègue les accès en lecture aux propriétés. En revanche cette classe intercepte les accès en écriture aux propriétés et les signale en envoyant des exceptions. Voici le code :

 

using System;

interface IArticle

{

       string Nom{get;set;}

       int Prix{get;set;}

       bool IsReadOnly{get;}

}

class Article : IArticle

{

       private string m_Nom = "N/A";

       private int m_Prix = 0;

       public string Nom

       {

               get{return m_Nom;}

               set{m_Nom = value;}

       }

       public int Prix

       {

               get{return m_Prix;}

               set{m_Prix = value;}

       }

       public bool IsReadOnly{get{return false;}}

}

class ArticleReadOnly : IArticle

{

       private IArticle m_IArticle;

       public ArticleReadOnly(IArticle A)

       {

               if(A == null)

                      throw new System.NullReferenceException();

               m_IArticle = A;

       }

       public string Nom

       {

               get{return m_IArticle.Nom;}

               set{throw new UnauthorizedAccessException(

           "Dans ce contexte le membre IArticle.Nom n'est accessible qu'en lecture");}

       }

       public int Prix

       {

               get{return m_IArticle.Prix;}

               set{throw new UnauthorizedAccessException(

                  "Dans ce contexte le membre IArticle.Prix n'est accessible qu'en lecture");}

       }

       public bool IsReadOnly{get{return true;}}

}

class Prog

{

       static void fct(IArticle A)

       {

               try

               {

                      A.Prix = 110; // EXCEPTION LANCEE A L’EXECUTION

                      Console.WriteLine("{0}:{1} euros", A.Nom, A.Prix);

               }

               catch(Exception e)

               {

                      Console.WriteLine("Exception Type:{0} Msg:{1}",e.GetType(),e.Message);

               }

       }

       static void Main(string[] args)

       {

                      IArticle A = new Article();

                      A.Nom = "Chaussure";

                      A.Prix = 100;

                      fct( new ArticleReadOnly(A) );

                      System.Diagnostics.Debug.Assert( A.Nom == "Chaussure" );

                      System.Diagnostics.Debug.Assert( A.Prix == 100 );

       }

}

 

Cette solution se révèle très facile d’utilisation. En revanche elle paraît compliquée à maintenir. De plus, son problème principal est que les accès en écriture ne sont pas détecté statiquement (i.e par le compilateur) mais dynamiquement (i.e à l’exécution). Or un des principaux bénéfices du mot clé const de C++ est la détection statique des accès en écriture. L’objectif n’est donc pas atteint par cette solution.

Solution 2 : Une solution à base d’interfaces

A la lumière de ce problème de détection statique il m’a paru clair qu’il fallait introduire une nouvelle interface IArticleReadOnly qui n’aurait aucun setter. Il suffit alors que la classe Article implémente IArticle et IArticleReadOnly. Les getters des propriétés sont alors accessibles à partir des deux interfaces mais les setters ne sont accessibles qu’à partir de l’interface IArticle. En castant un paramètre de type IArticle en un paramètre de type IArticleReadOnly on obtient la détection statique des accès en écriture :

 

using System;

interface IArticleReadOnly

{

       string Nom{get;}

       int Prix{get;}

}

interface IArticle

{

       string Nom{get;set;}

       int Prix{get;set;}

}

class Article :IArticle, IArticleReadOnly

{

       private string m_Nom = "N/A";

       private int m_Prix = 0;

       public string Nom

       {

               get{return m_Nom;}

               set{m_Nom = value;}

       }

       public int Prix

       {

               get{return m_Prix;}

               set{m_Prix = value;}

       }

}

class Prog

{

       static void fct(IArticleReadOnly A)

       {

               A.Prix = 110; // ERREUR DE COMPILATION

               Console.WriteLine("{0}:{1} euros", A.Nom, A.Prix);

       }

       static void Main(string[] args)

       {

               IArticle A = new Article();

               A.Nom = "Chaussure";

               A.Prix = 100;

               fct( (IArticleReadOnly) A );

               System.Diagnostics.Debug.Assert( A.Nom == "Chaussure" );

               System.Diagnostics.Debug.Assert( A.Prix == 100 );

       }

}

 

Cette solution satisfait complètement à notre problématique. Elle est simple d’utilisation et la détection d’accès en lecture se fait statiquement. Cependant elle a pour défaut d’obliger toutes les implémentations de IArticle d’implémenter IArticleReadOnly. Cette contrainte est assez lourde, car peu logique. Tôt ou tard un développeur oubliera de l’appliquer. Dans ce cas la détection de l’impossibilité de caster certains IArticle en IArticleReadOnly ne se ferra pas à la compilation mais à l’exécution (et potentiellement à l’exécution chez le client !). En fait, on a remplacer la contrainte ‘localement pas d’accès en écriture’ par une autre contrainte.

Solution 3 : Une autre solution à base d’interfaces

Pour pallier la contrainte de la solution 2 il fallait lier d’une manière ou d’une autre IArticleReadOnly à IArticle. Or le compilateur C# accepte que IArticle étende IArticleReadOnly moyennant l’utilisation du mot clé new sur chaque propriété. En effet, dans ce cas le compilateur considère que IArticle redéfinie chaque propriété de IArticleReadOnly. Notez que la non utilisation du mot clé new entraîne le même comportement du compilateur et ne provoque pas d’erreur de compilation mais simplement des avertissements :

 

using System;

interface IArticleReadOnly

{

       string Nom{get;}

       int Prix{get;}

}

interface IArticle : IArticleReadOnly

{

       new string Nom{get;set;}

       new int Prix{get;set;}

}

class Article :IArticle

{

       private string m_Nom = "N/A";

       private int m_Prix = 0;

       public string Nom

       {

               get{return m_Nom;}

               set{m_Nom = value;}

       }

       public int Prix

       {

               get{return m_Prix;}

               set{m_Prix = value;}

       }

}

class Prog

{

       static void fct(IArticleReadOnly A)

       {

               A.Prix = 110; // ERREUR DE COMPILATION

               Console.WriteLine("{0}:{1} euros", A.Nom, A.Prix);

       }

       static void Main(string[] args)

       {

               IArticle A = new Article();

               A.Nom = "Chaussure";

               A.Prix = 100;

               fct( (IArticleReadOnly) A );

               System.Diagnostics.Debug.Assert( A.Nom == "Chaussure" );

               System.Diagnostics.Debug.Assert( A.Prix == 100 );

       }

}

 

Cette solution impose la seule contrainte de maintenir IArticleReadOnly lors de l’évolution de IArticle. Cette contrainte est assez faible car son non respect entraine :

Solution 4 : Utilisation du clonage d’objet

Sami m’a soufflé une quatrième solution utilisée dans le monde Java. Cette solution consiste tout simplement à communiquer un clone de l’objet à l’appel de la fonction. Ainsi, la fonction peut effectuer toute les modifications sur l’objet cloné, l’objet original ne sera jamais altéré.

 

 

using System;

 

class Article :ICloneable

{

       private string m_Nom = "N/A";

       private int m_Prix = 0;

       public string Nom

       {

               get{return m_Nom;}

               set{m_Nom = value;}

       }

       public int Prix

       {

               get{return m_Prix;}

               set{m_Prix = value;}

       }

       public object Clone()

       {

               Article A = new Article();

               A.Nom = this.Nom;

               A.Prix = this.Prix;

               return A;

       }

}

class Prog

{

       static void fct(Article A)

       {

               A.Prix = 110;

               Console.WriteLine("{0}:{1} euros", A.Nom, A.Prix);

       }

       static void Main(string[] args)

       {

               Article A = new Article();

               A.Nom = "Chaussure";

               A.Prix = 100;

               fct( (Article) A.Clone() );

               System.Diagnostics.Debug.Assert( A.Nom == "Chaussure" );

               System.Diagnostics.Debug.Assert( A.Prix == 100 );

       }

}

 

Ou pour ceux qui n’aiment pas la méthode Clone qui renvoie un objet non typé (beurk !), utilisons un constructeur de copie :

 

using System;

 

class Article

{

       private string m_Nom = "N/A";

       private int m_Prix = 0;

       public string Nom

       {

               get{return m_Nom;}

               set{m_Nom = value;}

       }

       public int Prix

       {

               get{return m_Prix;}

               set{m_Prix = value;}

       }

       public Article(Article A)

       {

               this.Nom = A.Nom;

               this.Prix = A.Prix;

       }

       public Article(){}

}

class Prog

{

       static void fct(Article A)

       {

               A.Prix = 110;

               Console.WriteLine("{0}:{1} euros", A.Nom, A.Prix);

       }

       static void Main(string[] args)

       {

               Article A = new Article();

               A.Nom = "Chaussure";

               A.Prix = 100;

               fct( new Article(A) );

               System.Diagnostics.Debug.Assert( A.Nom == "Chaussure" );

               System.Diagnostics.Debug.Assert( A.Prix == 100 );

       }

}

 

 

A l’instar de la solution 1, l’utilisation d’un objet cloné ne permet pas au compilateur de détecter les accès en écriture à l’intérieur de la fonction. De plus, il n’est pas toujours logiquement évident de cloner un objet (que signifie cloner une instance de la classe Thread !). Cependant, cette solution est très simple à implémenter. De plus elle présente l’avantage de déléguer aux différents appels à une même fonction la responsabilité de laisser l’objet passé en paramètre d'être modifié ou non.

Conclusion

Nous avons exposé ici des petites ficelles applicables dans vos propres projets C# pour localement empêcher les accès en écriture à un objet. Nous avons exposé aussi quelques éléments de comparaison entre différentes architectures objets. Notez que nous ne savons pas encore si C# 2.0 permettra des solutions plus élégantes à cette problématique.

 

 

Auteurs : Patrick Smacchia

Copyright © Septembre 2003

Patrick Smacchia, email [patrick@smacchia.com]

Patrick Smacchia assure de nombreuses formations sur .NET, à la fois dans l’industrie et dans le milieu universitaire (Université de Nice). Passionné par l’architecture logicielle, il aide les entreprises à concevoir et à développer leurs applications. Ingénieur diplômé de l’ENSEEIHT, il a notamment collaboré avec Amadeus et avec les divisions espace et téléphonie mobile d’Alcatel. Son site expose plus en détail ses activités. Ses compétences ont été reconnues par Microsoft France, ce qui lui a valu la distinction MVP .NET (Most Valuable Professional sur les technologies .NET).

L’ouvrage Pratique de .NET et C# (O’Reilly 2003)

L’ ouvrage Pratique de .NET et C# (O’Reilly 2003) couvre la plupart des aspects du développement sous .NET avec le langage C# (architecture .NET sous jacente; langage C#, bibliothèques ADO.NET, XML, WinForm, GDI+, architectures distribuées avec COM+, .NET Remoting et ASP.NET etc.). Cet ouvrage contient de nombreux rappels pour le rendre accessible aux étudiants et aux débutants. Les développeurs confirmés pourront quant à eux rapidement exploiter les subtiles possibilités proposées par .NET, que sont par exemple la réflexion, la programmation orientée aspect ou le mécanisme d’attribut.