Pattern Paramètres contextuels

Par Philippe PRADOS - 1998
www.prados.fr
Objectif
- Modifier des paramètres pour un contexte particulier.
- Modifier partiellement des paramètres.
- Découpler la mémorisation des paramètres de leur utilisation.
Motivation
Il est souvent utile de modifier partiellement les attributs d'un objet pour un contexte donné. Par exemple, dans un traitement de texte, un style de paragraphe est défini globalement. Un paragraphe particulier désire modifier certains attributs mais pas tous. La modification du style global va impacter tous les paragraphes dérivés de celui-ci, exception faite des attributs redéfinis localement.
Pour cela, il faut dans un premier temps déclarer un objet possédant l'ensemble des attributs utiles. Cet objet doit déclarer des accèsseurs virtuels pour chacun des attributs. Une classe dérivée va ajouter un champ de bit, avec un bit par attribut de la classe de base. Ce champ va servir à indiquer si l'attribut correspondant a été modifié ou non. Les méthodes d'accès vont alors être redéfinies. Elles vont vérifier la valeur du bit correspondant à l'attribut. Si celui-ci est à true, la valeur retournée est celle présente dans l'objet. Sinon, la valeur est demandée à la classe originale dont elle dépend. Cela correspond à une utilisation du pattern « Chain of Responsibility (223) ».

La valorisation d'un attribut dans la classe Modif peut vérifier si la nouvelle valeur est différente de la valeur obtenue dans l'originale pour ne garder le flag de modification que si cette valeur est différente.
void setNom(T x)
{
if (_link->getNom()!=x)
{
_flag=true;
Attribut::setNom(x);
}
else
_flag=false;
}
Le lien entre la classe Modif et l'originale peut être une relation comme dans le schéma précédant ou un lien d'utilisation vers un objet connu globalement. Par exemple, il peut exister un conteneur d'instances de type Attribut dont les instances sont identifiées par une clef. Une classe Modif peut alors chercher l'original dans le conteneur en utilisant sa propre clef dans le conteneur global.

Il peut y avoir plusieurs classes dérivées dans le conteneur. Chacune ayant sa version modifiée.

Ces classes contenant des attributs figés sont appelées « brique ». La clef permet de retrouver une brique et de convertir le pointeur vers la classe correspondante.
Il est possible d'avoir un conteneur contextuel des briques modifiées. Celui-ci recherchera la brique demandée. Plusieurs possibilités sont à envisager s'il ne la trouve pas.
- Déléguer la recherche au conteneur global. Ce conteneur implantera alors, lui aussi, une « Chain of Responsibility (223) ». Il est possible d'ajouter plusieurs niveaux de délégation en cascade. Le conteneur contextuel cherche chez lui ; s'il ne trouve pas la clef demandée, il transmet la demande à un autre conteneur qui en fait de même sur un troisième, etc.
- Ajouter dans le conteneur contextuel une brique modifiée pour toute recherche de clef insatisfaite.
- Il n'est pas légal de demander une brique non présente dans le conteneur contextuel. Le service génère une exception en cas de recherche infructueuse.
Le conteneur contextuel ne possédera que les briques modifiées utiles. Ce contexte est alimenté par une méthode spécifique de chaque brique par une approche équivalente à « Abstract Factory (87) ». Seule les briques utiles aux traitements futures seront présentes dans ce conteneur. Il est alors possible d'offrir une interface utilisateur n'autorisant la modification que des briques présentes dans ce conteneur contextuel. L'interface ne sera pas parasitée par des briques inutiles.
Par exemple, pour une application de trading, il existe plusieurs produits financiers possibles. Pour calculer les prix de ces produits, il faut utiliser des paramètres concernant une devise, le produit lui-même et éventuellement son sous-jacent. L'utilisateur désire modifier ces paramètres localement pour effectuer des simulations. Par contre, le responsable de la salle de marché déclare des paramètres globaux pour l'ensemble de la salle. Pour effectuer un calcul de prix, l'algorithme doit utiliser certaines de ces briques de paramètres mais pas toutes. Une méthode du produit va alimenter le conteneur contextuel avec les briques dont il a besoin. Par la suite, lors d'un calcul, ce conteneur sera redonné au produit financier. L'algorithme pourra alors retrouver les informations qu'il désire. Celles-ci auront éventuellement été modifiées ; l'algorithme ne le saura pas.

Il est possible de vérifier les doublons pour ne pas avoir plusieurs fois une brique modifiée d'une devise par exemple.
Certains paramètres peuvent être dynamiques. Par exemple, une brique de paramètre peut posséder la valeur courante du spot. Une brique modifiée peut demander cette valeur à un gestionnaire de flux. Lors du calcul, il ne faut pas que cette valeur évolue. Les calculs de prix sont relativement lents. Ils peuvent utiliser plusieurs fois à différents moments la valeur du spot. Avant de donner le conteneur contextuel à l'algorithme, il faut figer celui-ci. Toutes les valeurs sont alors stabilisées. L'algorithme peut prendre tout son temps par la suite. Pour une brique donnée, cela donne :

La brique BriOption est une version figée des paramètres. La brique ModBriOpt ajoute le champ de bits pour tous les attributs. Les méthodes setAttr() sont résolues, et les méthodes getAttr() sont déclarées en virtuelles pures. Celles-ci sont définies dans les briques dérivées. La brique ModBriOptRT demande la valeur d'un attribut aux gestionnaires « temps réel » si celui-ci n'est pas figé. La brique ModBriOptCtx demande la valeur au conteneur global si celui-ci n'est pas figé. Pour pouvoir transformer une brique modifiée par une brique figée, la méthode cloneFixe() est ajoutée à la classe Attribut. Celle-ci alimente un conteneur reçu en paramètre avec la brique correspondante.
D'autres dérivées de ModBriOpt peuvent être envisagées. Chacun aura un comportement différent vis-à-vis du transfert de responsabilités. Par exemple, l'application de trading décrite plus haut peut posséder :
- un conteneur de briques global à la salle de marché
- un conteneur pour un groupe de traders
- un conteneur particulier pour chaque trader
- et enfin, un conteneur contextuel associé à une fenêtre de l'application.

Applicabilité
Utilisez les « Paramètres contextuels » si vous manipulez beaucoup de paramètres devant être contextuellement et/ou partiellement modifiés.
Structure

Participants
- Attributs
Classe de base de tous les paquets d'attributs. Possède une clef identifiant l'instance. Plusieurs instances peuvent avoir la même clef.
- Brique
Ensemble cohérent d'attributs. Une brique peut posséder de un à n attributs. Il faut regrouper les attributs entre eux suivant les cas d'utilisations combinées. Par exemple, pour un produit financier, les paramètres de la devise sont séparés des paramètres d'une option. Par contre, la valeur du spot d'une devise et la volatilité de celle-ci sont dans la même brique.
- ModBrique
Modification des accèsseurs des attributs d'une brique pour dériver la demande vers une autre brique (via un conteneur global ou une relation) si la valeur de l'attribut n'a pas été modifiée.
- ContAttr
Conteneur de brique permettant de les retrouver par une clef. Possède en générale que des Briques non modifiées.
- ContContext
Conteneur de brique permettant de les retrouver par une clef. Utilisée pour maintenir un contexte particulier de briques. Possède en général que des ModBriques.
- Utilisation
Objet utilisant un conteneur de briques. Une méthode permet d'alimenter le conteneur contextuel, et une autre permet de l'utiliser. Entre les deux, l'utilisateur peut modifier les briques. Si un objet particulier utilise simultanément plusieurs objets manipulant un contexte, celui-ci peut être initialisé par l'ensemble des objets. Par exemple, un contexte peut être alimenté par l'ensemble des produits financiers d'un portefeuille, mais ne possédera pas l'ensemble des produits financiers du marché.
Collaborations
- La classe ModBrique utilise la classe de base si l'attribut est figé, sinon, utilise une instance de même clef retrouvée dans un ContAttr.
- Une instance de ContContext est alimentée par une instance Utilisation.
- La méthode addCtx() ajoute des briques ModBrique dans un conteneur reçu en paramètre.
- La méthode useCtx() utilise le conteneur contextuel reçu en paramètre. Entre l'alimentation du conteneur contextuel par addCtx() et l'utilisation par useCtx() l'utilisateur peut modifier les paramètres.
Conséquences
- L'accès aux attributs est légèrement ralenti par l'utilisation de méthodes virtuelles.
- L'utilisateur a la possibilité de modifier localement les attributs.
- Il est possible de ne lui présenter que les briques nécessaires au comportement.
- La manipulation des paramètres est externe à l'utilisation qui en est faite. Il est possible d'ajouter de nouvelles briques pour de nouveaux objets sans en modifier la gestion.
- Il est possible de maintenir un historique des paramètres.
Exemple de Code
class CAttributs
{
const char _key[10];
public:
CAttributs(const char* key)
{ strcpy(_key,key); }
};
typedef CList<CAttributs*> CContAttr;
extern CContAttr GlobalAttr;
class CBrique : public CAttributs
{
int _attr;
public:
CBrique(const char* key)
: CAttributs(key) {}
virtual int getAttr() const
{ return _attr; }
virtual void setAttr(int x)
{ _attr=x; }
void cloneModif(CContAttr* cont) const;
};
class CModBrique : public CBrique
{
bool _modattr;
public:
CModBrique(const char* key) : CBrique(key), _modattr(false) {}
int getAttr() const
{
return (_modattr) ? CBrique::getAttr()
: GlobalAttr.find(_key)->getAttr();
}
void setAttr(int x)
{
_modattr=true;
CBrique::setAttr(x);
}
void resetAttr()
{
_modattr=false;
}
};
inline void CBrique::cloneModif(CContAttr* cont) const
{
cont->insert(new CModBrique(_key);
}
class CUtilisation
{
public:
void addCtx(CContAttr* cont) const
{
GlobalAttr.find("key")->cloneMod(cont);
}
void usrCtx(const CContAttr* cont)
{
const CBrique* bri=(const CBrique*)cont->find("key");
assert(bri!=NULL);
bri->getAttr();
//...
}
};
Utilisations connues
- L'application de trading « Fox action » du Crédit Lyonnais utilise cela pour manipuler les paramètres du marché.
- Le framework de persistance de l’application Letlaw, utilise une version simplifiée de ce pattern. Un conteneur permet de paramétrer les chargements des instances depuis la base de données. Le client alimente un conteneur appelé Stratégies (ContAttr) avec des valeurs associées à des clefs. Les clefs sont les noms des classes à charger ; les valeurs sont des informations binaires permettant de raffiner le chargement d’une instance.
Par exemple, si un objet Personne agrège un objet Addresse, il est possible de demander au framework de charger les instances Personne, avec un chargement paresseux pour les instances Adresses. Pour cela, le conteneur Stratégies doit posséder la valeur Personne.LeasyLoadAddress associée à la clef « Address ». De même, l’adresse peut charger tous ces champs à partir de la base de données ou sélectionner certains champs. Pour cela, le conteneur de stratégie devra posséder les drapeaux correspondants. Le client du framework peut paramétrer finement les chargements directs ou indirects des instances, en alimentant un dictionnaire de stratégie. Ce dictionnaire sera livré en paramètre lors du chargement des premières instances.
Si la clef associée à une classe n’est pas trouvée dans le dictionnaire, un comportement par défaut est choisi par le framework. En général, l’instance est entièrement chargée et les objets en agrégation également.
Patterns en relation
- Abstract Factory (87) est utilisé pour alimenter le conteneur contextuel.
- Chain of Responsibility (223) est utilisé pour retrouver un attribut d'une brique et/ou une brique dans un conteneur.
