|
|
Design Pattern Décorateur et
protection d'objets en écriture par Patrick Smacchia |
Solution 1: Utilisation du design
pattern décorateur
Solution 2 : Une solution à
base d’interfaces
Solution 3 : Une autre solution
à base d’interfaces
Solution 4 : Utilisation du
clonage d’objet
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.
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.
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.
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.
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.
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 :
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;
}