I just can't (Nu)Get enough!

par Olivier Spinelli 23. avril 2011 14:55

NuGet, pour ceux qui ne connaitrait pas encore est un projet Open Source sponsorisé par Microsoft qui adresse (enfin) le "packaging" de composants logiciels. Tout le monde en a besoin et cela manquait cruellement à l'eco-système .Net.

Ce projet est prometteur sur de nombreux aspects mais il n'adresse qu'une petite partie des enjeux. Ce post est, je l'espère, le premier d'une petite série dont l'objectif est de rendre compte de notre expérience à Invenietis en proposant des évolutions concrètes et/ou des compléments qui nous semblent cruciaux. Nous ne sommes pas les seuls à espérer plus, par exemple Davy Brion qui soulève le problème d'une dépendance à "au moins un de ces packages". Tout ce qui concerne le packaging et la gestion de dépendances n'est pas simple. Il est en fait plus souvent complexe que ce que l'on peut penser car il met en jeu notamment :

  • La stratégie de versionning des composants logiciels. Si Microsoft a donné un cadre général avec l'objet Version, il reste :
    • A décider de ce que l'on met dans les quatres nombres Major.Minor.Build.Revision (dans cet ordre! voir cette question sur stackoverflow), et ce n'est pas le plus simple (voir également cette question et ses réponses)
    • A prendre en compte qu'un assembly possède en réalité deux Versions: celle de l'Assembly proprement dite et celle du fichier (ce qui est expliqué ici). Pour faire simple :
      • La Version est utilisée par le chargeur d'Assembly de .Net. Lorsqu'une référence est spécifiée avec <specificVersion>true</specificVersion> (ce qui est le défaut), il faut que l'assembly soit exactement compatible (à propos de cette spécification de version et ses impacts sur les temps de compilation, ce billet vaut le détour).
        Cette information est stockée dans le manifeste de l'Assembly : c'est donc du "pure .Net".
      • FileVersion est plutôt pour "l'affichage"... certes mais attention : Windows Installer le le prend en compteau moment de mettre à jour (c'est pouquoi j'ai mis des guillemets à "affichage").
        Cette information est stockée dans les ressources du fichier selon le format Portable Executable (PE) : c'est donc une information "Windows".
  • La granularité des composants : le même mécanisme est-il compatible à la fois pour quelques "gros composants" et une multitude de mini-composants ?
  • Les "Saveurs" : Comment fait-on gérer les notions de Debug/Release/CodeContractInside/x32/x64/AnyCpu/SL/.Net4/etc. ?
  • La définition des dépendances, par exemple :
    • "Ceci" dépend de "cela" mais seulement à partir de sa version "x".
    • "Ceci" dépend de "cela" ou "cela" (cf. l'exemple pré-cité).
  • Le "contenu" des packages :
    • certains utilisent NuGet pour distribuer du code source. Les règles applicables à la distribution de binaire (dll, exe) sont-elles toujours valides ?
    • comment choisir les fichiers à packager ? Aujourd'hui NuGet est relativement pauvre sur cet aspect.
  • Le type de dépôt de code source : Subversion offre un Revision Number fiable et pratique car c'est un entier que l'on peut mettre dans le slot Revision de la version, mais Git et d'autres n'ont pas cette notion.
  • La Continuous Integration : car gérer toutes ces versions à la main est impossible en pratique (surtout si une granularité fine est nécessaire).

Je pense avoir fait le tour (si ce n'est pas le cas, dites le moi et je complèterai). A part un aspect : les impacts du temps qui passe... indépendamment de l'augmentation des numéros de version.

La nouvelle version du package ne dépend plus de "X". Le package "Y" a été découpé en deux. "Z" s'appelle maintenant "A". "B" n'existe plus, ses features sont intégrées dans "C". "D" que l'on croyait fiable doit impérativement être mis à jour pour des raisons de sécurité. (...)

Bref, vous voyez l'idée. il y-a-t-il UNE solution ? Peut-être pas, mais il y a certainement de bonnes idées/conventions à trouver et mettre en place pour mieux gérer ces aspects. Peut-être un jour travaillerons-nous avec NuGetMore ?

 

Un peu de Sql : Union d’intervalles

par Olivier Spinelli 16. septembre 2010 18:23

On dispose d’un ensemble de ressources et de leurs disponibilités modélisées par une liste d’intervalles disjoints { BegDate , EndDate }. Concrètement, cet exercice s’applique sur une unique table :

create table dbo.tResourceAvailability
(
	ResourceID int not null,
	BegDate datetime not null,
	EndDate datetime not null,
);

Etant donné un ensemble de ResourceID (typiquement obtenu via d’autres tables et contraintes), il faut construire l’union des intervalles de disponibilité.

Cela ne semble pas excessivement compliqué. Mais ce n’est pas non plus trivial. Bref, c'est une bonne illustration d'un aspect du métier de développeur : résoudre un problème simple, avec une solution qui, si possible, l’est aussi… à condition de la trouver.

La première chose à faire en abordant un problème de ce type est de tout faire pour bien le visualiser mentalement. Un problème bien « vu » est souvent un problème résolu. En l’occurrence, il est judicieux de créer un jeu d’essai sur la base de la table ci-dessus. Un tel jeu d’essai doit être à la fois complet et simple... et parfois on peine. Ici ce n’est pas le cas : on peut se contenter de 5 ressources et de quelques créneaux répartis sur une plage d’une vingtaine « d’instants ».

La table ci-dessous présente le terrain de jeux :

  0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
P0   B   E         B     E              
P1     B     E       B E                
P2   B     E           B   E       B E  
P3     B E                              
P4               B     E         B E    

Et ce que nous cherchons à obtenir est la ligne « d’union » ci-dessous :

U   B       E   B         E     B   E  

Tiens ! Tout se passe comme si l’on ne retenait que certains B ou certains E.

Tilt !... « Retenir » = « Filtrer »… = condition d’une clause where… cela semble compatible avec du Sql…

Il suffit de se poser la question : « Qu’est-ce qui fait que je garde tel B ou tel E ? ».

Prenez une minute pour regarder les deux tableaux ci-dessus…

Réponse : Les seuls B (ou E) à conserver sont ceux qui ne sont pas compris dans un autre intervalle.

On « voit » le problème à résoudre là… et on en entrevoit même la solution, il est temps de passer à l’action :

insert into dbo.tResourceAvailability( ResourceID, BegDate, EndDate )
 values( 0, '2001', '2003' ), ( 0, '2008', '2011' ),
       ( 1, '2002', '2005' ), ( 1, '2009', '2010' ),
	 ( 2, '2001', '2004' ), ( 2, '2010', '2012' ), ( 2, '2016', '2017' ),
	 ( 3, '2002', '2003' ), 
	 ( 4, '2007', '2010' ), ( 4, '2015', '2016' );

J’utilise une syntaxe d’insertion de lignes multiples standard Sql (mais disponible qu’à partir de Sql Server 2008), je ne mets que des années pour simplifier… et je procède tout doucement, étape par étape, en commençant par un select simple :

declare @T datetime = '2008';
select count(*)
	from dbo.tResourceAvailability a
	where BegDate <= @T and @T <= EndDate;

Cela calcule le nombre d’intervalles rencontrés à cette date. L’idée est la suivante (regarder le tableau) : pour un B que l’on doit conserver (par exemple 2007), le calcul donnera un (il se rencontrera lui-même), et pour un B qui ne doit pas être conservé, le calcul donnera 2 ou plus (outre lui-même, d’autres intervalles le contiennent). On progresse non ?

Oui mais… Le calcul donne 2 aussi pour 2001… car deux intervalles débutent pile sur cette date. Il est donc nécessaire d’affiner un peu. Idée : on change légèrement la condition de façon à, lorsque l’on cherche un B, ignorer les B strictement identiques à celui que l’on cherche. On enlève donc au moins un au résultat du calcul précédent, et les B que l’on garde seront ceux pour lequel on aura 0 intersection (et non plus une seule). On procède symétriquement pour E, on a donc deux requêtes légèrement différentes pour les B et les E :

declare @B datetime = '2005';
select count(*)
	from dbo.tResourceAvailability a
	where BegDate < @B and @B <= EndDate
	
declare @E datetime = '2012';
select count(*)
	from dbo.tResourceAvailability a
	where BegDate <= @E and @E < EndDate

J’ai utilisé l’opérateur count pour mieux « voir » mes requêtes, ce que je cherche à filtrer est tous les B et E qui ont un compte de 0, c’est-à-dire ceux qui n’existent pas.

select year(BegDate)
  from dbo.tResourceAvailability a
  where not exists( select * 
    from dbo.tResourceAvailability f 
    where BegDate < a.BegDate and a.BegDate <= EndDate );
Résultat :  2001  
   2001  ==> Doublon : un distinct le fera disparaitre.
   2007  
   2015  
select year(EndDate)
  from dbo.tResourceAvailability a
  where not exists( select * 
    from dbo.tResourceAvailability 
    where BegDate <= a.EndDate and a.EndDate < EndDate );
Résultat :  2005
   2012
   2017

Ce qui est presque parfait puisque nous cherchons à obtenir la liste d’intervalles suivante : 2001 – 2005, 2007 – 2012, 2015 - 2017

Nous avons le résultat final, mais « en deux morceaux ». Comment les réunir ? Avec une « union ». C’est une opération ensembliste on ne peut plus simple :

select year(BegDate)
  from dbo.tResourceAvailability a
  where not exists( select * 
    from dbo.tResourceAvailability 
    where BegDate < a.BegDate and a.BegDate <= EndDate )
union
select year(EndDate)
  from dbo.tResourceAvailability a
  where not exists( select * 
    from dbo.tResourceAvailability 
    where BegDate <= a.EndDate and a.EndDate < EndDate );
Résultat :  2001
   2005
   2007
   2012
   2015
   2017

On obtient tous les B, puis tous les E. Le doublon a disparu (2001) car c’est le comportement par défaut de l’union (pour ne pas filtrer les doublons, il faut explicitement utiliser union all). Il ne reste plus qu’à ordonner ces éléments en remarquant que l’on a nécessairement une séquence B, E, B, E, etc. Pour la rendre plus lisible, on le précise :

select 'B', year(BegDate)
  from dbo.tResourceAvailability a
  where not exists( select * 
    from dbo.tResourceAvailability 
    where BegDate < a.BegDate and a.BegDate <= EndDate )
union
select 'E', year(EndDate)
  from dbo.tResourceAvailability a
  where not exists( select * 
    from dbo.tResourceAvailability 
    where BegDate <= a.EndDate and a.EndDate < EndDate )
order by 2;
Résultat :  B  2001
   E  2005
   B  2007
   E  2012
   B  2015
   E  2017

On approche mais ce n’est pas encore idéal car on souhaite une forme de réponse similaire à celle de la donnée source : une liste d’intervalle, donc deux colonnes B et E. Transformer des lignes en colonnes, c’est le rôle de l’opérateur pivot, à condition de s’appuyer une fonction d’agrégation (count, sum, max, avg, etc.) : il ne nous est d’aucune utilité ici (en règle général, n’allez chercher l’opérateur pivot que s’il y a une agrégation possible ou souhaitable). Il nous faut trouver autre chose pour « mélanger » ces deux requêtes :

| 2001 |
  | 2012 |
  | 2001, 2005 |
| 2005 |
 x  | 2015 |
 ==>  | 2007, 2012 |
| 2007 |
  | 2017 |
  | 2015, 2017 |

Problème simple non ? Cherchez un peu…

……………

Le principe est de se servir de B comme « générateur », et de chercher la fin de l’intervalle correspondant :

select B = a.BegDate,
  	 E = << Take the first EndDate greater than a.BegDate >>
  from dbo.tResourceAvailability a
  where not exists( select * 
    from dbo.tResourceAvailability 
    where BegDate < a.BegDate and a.BegDate <= EndDate )

Ce qui revient à prendre la plus petite date supérieure à la date de début, et donne la requête finale suivante :

select distinct 
  B = BegDate,
  E = (select min(EndDate)
         from dbo.tResourceAvailability ae
	   where ae.EndDate > a.BegDate 
		and not exists( select * 
					from dbo.tResourceAvailability 
					where BegDate <= ae.EndDate 
and ae.EndDate < EndDate ))
  from dbo.tResourceAvailability a
  where not exists( select * 
			    from dbo.tResourceAvailability
			    where BegDate < a.BegDate and a.BegDate <= EndDate )

Une remarque sur les tris : Les dates obtenues peuvent être dans l’ordre croissant. C’est un hasard et il ne faut en aucun cas s’y fier  si vous voulez trier, triez explicitement (avec un order by).

 

Enseigner, une passion, un métier... avec des outils si possible.

par Olivier Spinelli 25. août 2010 10:08

Le système de contrôle de connaissance de mes rêves… est peut-être un peu compliqué (c’est, en puissance, un système d’EAO – Enseignement Assisté par Ordinateur – ou encore un LMS – Learning Management System). Mais il me tarabuste depuis des années… Ci-dessous quelques débuts d’ébauches de spécifications informelles de ce que j’aimerai qu’il fasse/supporte/offre. C’est brut, cela aborde de nombreux aspects par le petit bout de la lorgnette et je ne détaille pas le cadre pédagogique dans lequel j’entrevois vivre ces outils (cela demanderait beaucoup de réflexion et de travail - je n’ai pas le temps de m’y consacrer pleinement).

Le contrôle de connaissance passe par la gestion nécessaire d'un stock de Questions. Ce n'est pas très original, mais il faut bien commencer par un bout. Une Question est définit par :

  • Un énoncé multimédia
  • Des réponses qui peuvent être de type
    • QCM (1 parmi N, N parmi M).
    • Ordonnancement des réponses
    • Un fragment de code, du Java, du C#, du SQL, etc.
    • Autres...
  • Une durée normale de réponse (en minute).
  • Des évaluateurs de réponses qui projettent leur évaluation sur un modèle simple :
    • Une valeur flottante [0,1] - Oui, c'est l'horrible "note"... (On peut aussi l'appeler CES - "Coefficient d'Evaluation du Savoir", ou EON - "Estimateur d'Objectifs Normé".)
    • Une liste de chaînes étiquetées chacune par un type { par exemple: Error, Warning, Comment }
  • Certaines évaluations doivent pouvoir être faites par un être humain (cas des textes libres), mais autant que possible, les évaluateurs sont… du code.
  • Les « indices » :
    • une liste optionnelle d’aides constituée d’énoncés multimedia supplémentaires dont les accès sont traqués
    • à l’issue de l’examen, on connait le nombre d’indices consommés pour répondre à la question.

Les relations inter-questions sont multiples (et relativement subtiles). Commençons par la plus simple : une question ne peut avoir de sens qu’au sein d’un groupe de questions. C’est un « scenario » qui enchaîne des questions dépendantes les unes des autres.

Nous sommes devant une alternative classique du design pattern du Composite : doit-on appliquer le « vrai » Composite dans lequel une Question est susceptible de contenir d’autres Questions, ou introduit-on une entité différente qui permet de structurer des paquets de Questions ?

En l’occurrence, il existe une entité du domaine évidente qui regroupe des Questions : l’Exercice.
On introduit donc la notion d’Exercice : un Exercice est composé d’une ou plusieurs Questions. Et pendant que l'on y est, on obtient le schéma de composition plutôt naturel suivant : Un Examen est composé d’un ou plusieurs Exercices, eux-mêmes composés d’une ou plusieurs Questions. Et c'est tout.

La composition d’un Examen se fait donc en manipulant des Exercices : ce sont ces Exercices qui sont les éléments de contenus sélectionnables (les Questions ne sont manipulées directement que dans le cadre d’un Exercice).

Les Exercices peuvent avoir certaines relations entre eux relativement à leur sélection au sein d’un Examen :

  • Si deux exercices sont sélectionnés dans un examen, l’une doit suivre l’autre mais retenir l’un n’entraîne pas la sélection de l’autre. C’est un pré requis d’ordre de présentation uniquement.
  • Un Exercice peut en requérir réellement un ou plusieurs autres.
    • Si l’Exercice est sélectionné, cela entraîne nécessairement la sélection de ses prérequis dans l’Examen. (Note : ce type de prérequis implique celui de l’ordre de présentation.)
    • Une piste à creuser: un pré-requis pourrait-être remplacé par son "résultat" (s'il existe). C'est l'idées des "lemmes" en mathématique (un résultat intermédiaire qui participe à la démonstration d'un théorème plus important).
  • Un Exercice peut requérir que le candidat ait précédemment réussi un ou plusieurs autres Exercices. Ce point soulève la problématique de la mémoire du système… et on voit poindre l’artillerie lourde (bon, c’est un simple serveur).

Le choix d’un tel référentiel (arborescent) est tout à fait discutable, mais je le considère comme un compromis acceptable (suffisamment expressif - correctement opérationnel).

Voilà, cela suffit pour aujourd’hui. Pour continuer il faut introduire la « catégorisation de l’apprenant » (par rapport au référentiel), qui est assimilable aux caractéristiques de son avatar, la fonction d’Evaluation qui fait évoluer les caractéristiques des apprenants, la carte du territoire des compétences à conquérir, etc.

Cela ressemble à un jeu ? Oui, c’est une des idées-forces qui, je pense, devrait être intégrée dans ce type d’outils pédagogiques.

Cela existe déjà ? Tant mieux cela m'intéresse ! Mais ne serait-ce pas une usine à gaz ? Ce ne serait guère étonnant car ce Système dans sa globalité intègre tellement d'aspects (CMS, KM) que je l'imagine bien comme une agglutination de machins divers... Ce qui me motive est de penser le système "from scratch", de modéliser ce qui est nécessaire et uniquement ce qui est nécessaire à la mise en oeuvre de ces idées.

Si d’aventure, cher lecteur, vous êtes intéressé, ou connaissez des systèmes – ou approches – similaires ou approchant(es), quel que soit leur maturité, nous sommes intéressés. Invenietis a les compétences nécessaires au développement/adaptation du système ainsi que le partenaire pédagogique pour l’expérimenter (l’école Intech’Info)… Mais nous ne sommes aucunement spécialistes de ces questions, de l'offre en la matière, des outils existants. N’hésitez pas à me contacter.

Les opérations sur les modes de clavier

par Olivier Spinelli 11. juin 2010 17:59

Les Keyboard Modes ont chacun (cf. le post précédent), une liste des modes atomiques qui les composent : c’est un ensemble au sens mathématique du terme et les caractéristiques et opérations traditionnelles sur les ensembles sont utiles dans notre cas :

  • L’ensemble vide : Θ est notre mode vide (EmptyMode).
  • L’union : « Ctrl + Shift » U « Ctrl + Frigo » = « Ctrl + Shift + Frigo »
  • L’intersection : « Ctrl + Shift » ∩ « Ctrl + Frigo » = « Ctrl »
  • La soustraction : « Ctrl + Shift » - « Ctrl + Frigo » = « Shift »

Les prédicats suivants sont pratiques :

  • ContainsOne (teste que l’intersection de deux modes est non vide) : X.ContainsOne( Y ) <=> X ∩ Y != Θ
  • ContainsAll (teste qu’un mode est totalement contenu dans un autre) : X.ContainsAll( Y ) <=> X ∩ Y = Y

La liste des modes atomiques est ordonnée (selon la comparaison de chaîne ordinale évoquée précédemment) et cette propriété permet d’implémenter les différentes opérations efficacement. L’idée est d’avancer deux index dans les 2 listes (Gauche contient m éléments et Droite en contient n) à traiter en comparant les éléments : à chaque comparaison, on détecte un élément à gauche uniquement, à droite uniquement ou dans les deux.

Lorsque les deux éléments sont égaux, on peut avancer les deux index simultanément, sinon seul l’un des index avance : au pire des cas il y a donc m+n comparaisons et dans le meilleur des cas min(m,n).

L’implémentation repose sur une unique fonction qui applique l’algorithme ci-dessus sur les deux listes et qui est paramétrée par 3 prédicats (des fonctions booléenes prenant un élément en paramètre) :

static void Process( KeyboardMode left, 
                     IKeyboardMode right, 
                     Predicate<IKeyboardMode> onLeft, 
                     Predicate<IKeyboardMode> onRight,  
                     Predicate<IKeyboardMode> onBoth );

Ces prédicats sont appelés dans l’ordre d’apparition des modes atomiques dans les deux modes left et right selon que l’élément apparait à gauche uniquement, à droite uniquement ou dans les deux. Ces fonctions peuvent retourner false pour arrêter le processus. Process est utilisé pour toutes les opérations sur les modes.

Une seule fonction : pour combien d'opérations différentes ?

Voyons d'abord l'implémentation des deux prédicats.

Les prédicats ContainsOne/ContainsAll

ContainsOne est peut-être la plus simple : dès qu’un élément commun est trouvé, on peut conclure positivement et arrêter le processus. Il suffit donc de fournir une fonction qui arrête le processus et mémorise le succès dans le cas où un élément a été trouvé dans les deux listes.

bool IKeyboardMode.ContainsOne( IKeyboardMode mode )
{
  bool found = false;
  Process( this, mode, 
           null, 
           null,
           delegate( IKeyboardMode m ) { found = true; return false; } );
  return found;
}

ContainsAll est presque identique : c’est le cas « à droite seulement » qui est pisté et l’opposé qui est retourné car si on trouve un mode à droite seulement, c'est que c'est un "alien" qui ne devrait pas être là. (C'est une façon de parler, hein! Les étrangers ont bien le droit d'être là, sinon où serions nous ?)

bool IKeyboardMode.ContainsAll( IKeyboardMode mode )
{
  bool foundAlien = false;
  Process( this, mode,
           null,  // Use lambda syntax below instead of delegate keyword.
           m => { foundAlien = true; return false; }, 
           null );
  return !foundAlien;
}

Un Process, deux prédicats. Il est temps d'aborder les opérations plus complexes.

L’intersection

L’intersection est la plus simple des combinaisons : seuls les éléments qui apparaissent dans les deux listes doivent apparaitre dans le résultat final. On utilise donc une fonction (paramètre onBoth) qui ajoute l’élément à une liste (un collecteur) et continue le processus.

Problème : il nous faut donc appeler la méthode List.Add (qui ne retourne rien) alors que la méthode Process attend un prédicat (une fonction qui retourne un booléen).

Le code est le suivant :

IKeyboardMode IKeyboardMode.Intersect( IKeyboardMode mode )
{
  List m = new List<IKeyboardMode>();
  Process( this, mode, null, null, Adapter.AlwaysTrue<IKeyboardMode>( m.Add ) );
  return Context.ObtainMode( m );
}

L’adaptateur qui suit permet d’envelopper (wrapper) une action dans un prédicat toujours vrai :

public class Adapter 
{
  static public Predicate<T> AlwaysTrue<T>( Action<T> a )
  {
    return delegate( T o ) { a( o ); return true; };
  }
}

L’air de rien, ces quelques lignes mettent en œuvre simultanément les génériques, les fonctions anonymes et la clôture.

Personnellement je trouve ça superbe et c’est - à mon humble avis - un exemple de code qui démontre bien la puissance de ce drôle de langage que devient C#.

J’ai cependant essayé de trouvé plus élégant, l’appel devenant par exemple :

// Perfection (at least for me)… Sadly, it does not work.
var actionAlwaysTrue = m.Add.ToPredicate( true ); 

Malheureusement, cette solution (une méthode d’extension sur le delegate Action) ne permet que d’écrire :

var actionAlwaysTrue = ((Action<IKeyboardMode>)m.Add).ToPredicate( true );

Car la méthode Add doit être explicitement considérée comme un delegate afin que les méthodes d’extension puissent s’appliquer. De même, la forme suivante n’est pas syntaxiquement correcte :

// Not perfect… And does not work.
var actionAlwaysTrue = Adapter.ToPredicate( m.Add, true ); 

Le compilateur exige la définition explicite du type, celui-ci ne pouvant être inférée par le type de l’action, ce qui nous ramène à :

var actionAlwaysTrue = Adapter.ToPredicate<IKeyboardMode>( m.Add, true ); 

Alors, en désespoir de cause, je préfère conserver mes deux petits helpers AlwaysTrue (et AlwaysFalse) :

var actionAlwaysTrue = Adapter.AlwaysTrue<IKeyboardMode>( m.Add );

L’union, la soustraction et le toggle

L’union se définit ainsi : que l’élément apparaisse à droite, à gauche ou dans les deux, il doit apparaitre dans le résultat final. C’est donc la même fonction qui est fournie aux 3 cas et qui ajoute l’élément à la liste (le collecteur).

IKeyboardMode IKeyboardMode.Add( IKeyboardMode mode )
{
  List<IKeyboardMode> m = new List<IKeyboardMode>();
  var add = Adapter.AlwaysTrue<IKeyboardMode>( m.Add );
  Process( this, mode, add, add, add );
  return Context.ObtainMode( m );
}

Ici, j’ai utilisé var pour définir la variable car le contexte est suffisamment explicite et l’algorithme suffisamment simple. En règle générale, je type explicitement mes variables : d’expérience le code est nettement plus lisible et maintenable ainsi.

La soustraction consiste à ne conserver que les modes qui apparaissent à gauche :

IKeyboardMode IKeyboardMode.Remove( IKeyboardMode mode )
{
  List<IKeyboardMode> m = new List<IKeyboardMode>();
  Process( this, mode, Adapter.AlwaysTrue<IKeyboardMode>( m.Add ), null, null );
  return Context.ObtainMode( m );
}

Le petit dernier est le « Toggle ». Intuitivement, il s’agit de l’opération qui « inverse » l’état d’un mode particulier, par exemple :

  • Toggle( « Ctrl + Shift », « Ctrl » ) => « Shift »
  • Toggle( « Shift », « Ctrl » ) => « Ctrl + Shift »

En quelques mots: si le mode est présent je le retire, sinon je l’ajoute. Evidemment, cela doit fonctionner avec plusieurs modes :

  • Toggle( « Ctrl + Shift », « Ctrl + Frigo » ) => « Shift + Frigo »
  • Toggle( « Shift + Frigo », « Ctrl + Frigo » ) => « Ctrl + Shift »

Ce toggle est trivial à implémenter grâce à la fonction Process car il suffit simplement de collecter ceux qui sont à gauche, ceux qui sont à droite, mais pas les éléments communs.

IKeyboardMode IKeyboardMode.Toggle( IKeyboardMode mode )
{
  List<IKeyboardMode> m = new List<IKeyboardMode>();
  var add = Adapter.AlwaysTrue( m.Add );
  Process( this, mode, add, add, null );
  return Context.ObtainMode( m );
}

This is the end. Mais j’ai pourtant eu envie de continuer…

Toutes les opérations possibles ?

Intrigante cette fonction Process qui permet de tout implémenter. A-t-on oublié des opérations intéressantes ?

Mon idée est de me servir de Process pour vérifier que je n’ai rien oublié : le tableau suivant recense toutes les utilisations possibles de la méthode d’ajout à un collecteur (il y a 3 slots, chacun pouvant être null ou add, il y a donc 23 = 8 possibilités).

onLeft    onRight    onBoth    Name & description    Comments
null null null “Empty” Useless.
null null add Intersect (keep commons) Implemented. Kind of symmetric of ‘Toggle’.
null add null “Cleanup” (keep theirs only) Useless since it is the symmetric of ‘Remove’
null add add “Other” (keep theirs and commons, reject mine) Useless. Opposite of ‘This’.
add null null Remove (keep mine only) Implemented. Opposite of ‘Cleanup’
add null add “This” (keep mine and commons and reject theirs) Useless. Opposite of ‘Other’.
add add null Toggle (keep mine, theirs, but reject commons) Implemented.
add add add Add Implemented.

Ces 4 fonctions couvrent bien l’ensemble des opérations sensées. Vous certainement pas, mais moi, ce constat me rassure Smile.

 

PS: Pourquoi le tableau ci-dessus est-il en anglais ? Parceque qand je code, je "suis" en anglais. Et que ce tableau, je l'ai fais dans le code.

Keyboard Modes version 2.5

par Olivier Spinelli 11. juin 2010 12:50

Ce billet est le premier d’une série consacré aux Modes d’un clavier. Il explique la modélisation et l’implémentation des Modes dans CiviKey. D’autres billets traiteront notamment des points suivants :

  • Les Modes se combinent, s’intersectent, se contiennent : quelles sont les opérations de base que doivent supporter les modes et surtout comment les implémenter efficacement ?
  • Le « Fallback d’un Mode », où comment calculer de la meilleure à la pire les combinaisons de modes qui découlent d’un mode combiné. Ci-dessous un exemple :
    1. « Ctrl » + « Frigo » + « Shift »
    2. « Ctrl » + « Frigo »
    3. « Ctrl » + « Shift »
    4. « Frigo » + « Shift »
    5. « Ctrl »
    6. « Frigo »
    7. « Shift »
    8. «»
    Combien y-a-t-il de fallbacks en fonction de du nombre de modes atomiques ? Comment les générer ?
  • La gestion de « AvailableMode » et « CurrentMode »
    Le fait est que sur un clavier, il y a deux collections de modes très différents : les modes possibles et les modes courants. Exemple de modes possibles « Alt+Ctrl+ Frigo+Word » et de modes courants « » (aucun) ou « Ctrl+ Frigo ». Le fait de supprimer un mode possible entraîne nécessairement la suppression de celui-ci des modes courants, ce qui entraîne la suppression des KeyMode et LayoutKeyMode qui étaient définis par ce mode…
  • Le « Fallback » des KeyMode et des LayoutKeyMode : le Mode courant d’un clavier pilote le comportement (KeyMode) et l’affichage (LayoutKey) d’une touche (Key). Quelle implémentation efficace pour la gestion des touches peut-on envisager ?

 

Un cache centrale : un objet, un mode.

Les Modes sont totalement « objectivés ». Les objets IKeyBoardMode ne peuvent être obtenus que via l’objet Context : sa méthode ObtainMode( string m ) se charge de la normalisation et de trouver l’objet-mode correspondant dans un cache interne (de le créer et de l’y ajouter si besoin). Grâce à ce cache, les « objets-modes » sont uniques (dans le cadre d’un Contexte) : il n’y a qu’un et un seul objet « Ctrl + Shift » (il y a également un unique EmptyMode – le mode vide qui correspond à la chaîne vide).

L’empreinte mémoire est diminuée et on peut se servir de ces « objets-modes » pour porter de la donnée potentiellement plus lourde (ce que l’on va exploiter pour stocker la liste des fall backs de chaque mode) et comme clef efficace de hash (l’identité de l’objet garantit celle du mode).

IKeyboardMode m = Context.ObtainMode( "Alpha+Beta" );
Assert.That( Context.ObtainMode( "\r++\t Beta ++ Alpha ++" ) == m, 
                                        "Canonical ordering and trimming." );

Combined & Atomic Modes : les débuts de l’implémentation

Ces deux types de modes doivent rentrer dans un moule commun : le design pattern du Composite s’impose. Tout mode expose donc les modes atomiques qui le composent.

public interface IKeyboardMode : IComparable<IKeyboardMode>
{
    /// <summary>
    /// Gets the <see cref="IContext"/>. 
    /// </summary>
    IContext Context { get; }

    /// <summary>
    /// Gets the atomic modes that this mode contains.
    /// </summary>
    IReadOnlyList<IKeyboardMode> AtomicModes { get; }

    /// <summary>
    /// Gets a value indicating whether this mode contains zero 
    /// (the empty mode is considered as an atomic mode) or only one atomic mode.
    /// </summary>
    bool IsAtomic { get; }

}

Comme le montre le support de l’interface correspondante, les modes sont comparables (en mathématique, on dit que l’on peut définir une relation d’ordre – strict ou total, au choix – sur l’ensemble des modes) ce qui permet de les ordonner (donc de les trier) en s’appuyant sur la chaîne de caractères normalisée, mais avec une astuce :

 public int CompareTo( IKeyboardMode other )
 {
    if( _modes == other ) return 0;
    int cmp = _modes.Count - other.AtomicModes.Count;
    if( cmp == 0 ) cmp = StringComparer.Ordinal.Compare( other.ToString(), _mode );
    return cmp;
}

Tout mode contenant plus de mode atomique qu’un autre est plus grand. Si les deux modes ont autant de modes atomiques l’un que l’autre, alors la chaîne est utilisée. Cela est rendu possible grâce à la normalisation des chaînes de modes (ordonnancement des modes atomiques du plus fort au plus petit séparés par des +), et il faut inverser la comparaison : « Ctrl » est meilleur que « Shift » (alors qu’il est « plus petit » dans l’ordre lexicographique).

Toutes les opérations liées aux modes utilisent la comparaison ordinale : elle est plus rapide et tout aussi solide que celle indépendante de la culture (sous réserve de normaliser les chaînes, au niveau de la représentation interne de l’Unicode, ce qui est fait dans ObtainMode).

Dans l’implémentation, on retrouve les 3 types de modes (vide, atomiques, combinés) via les 3 constructeurs :

class KeyboardMode : IKeyboardMode
{
  Context _context;
  string _mode;
  IReadOnlyList<IKeyboardMode> _modes;

  /// <summary>
  /// Initializes the new empty mode of a Context.
  /// </summary>
  internal KeyboardMode( Context ctx )
  {
    Debug.Assert( ctx.EmptyMode == null, 
                                "There is only one empty mode per context." );
    _context = ctx;
    _mode = String.Empty;
    _modes = ReadOnlyListEmpty<IKeyboardMode>.Empty;
    _fallbacks = new ReadOnlyListMono<IKeyboardMode>( this );
  }

  /// <summary>
  /// Initializes a new atomic mode.
  /// </summary>
  internal KeyboardMode( Context ctx, string atomicMode )
  {
    _context = ctx;
    _mode = atomicMode;
    _modes = new ReadOnlyListMono<IKeyboardMode>( this );
    _fallbacks = ctx.EmptyMode.Fallbacks;
  }

  /// <summary>
  /// Initializes a new combined mode.
  /// </summary>
  internal KeyboardMode( Context ctx, string combinedMode, IReadOnlyList<IKeyboardMode> modes )
  {
     Debug.Assert( combinedMode.IndexOf( '+' ) > 0 && modes.Count > 1, 
                            "There is more than one mode in a Combined Mode." );
     Debug.Assert( modes.All( m => m.IsAtomic ), "Provided modes are all atomic." );
     Debug.Assert( modes.GroupBy( m => m ).Where( g => g.Count() != 1 ).Count() == 0, 
                            "No duplicate in atomic in modes." );
     _context = ctx;
     _mode = combinedMode;
     _modes = modes;
  }
}

Notez l’utilisation des helpers ReadOnlyListEmpty et ReadOnlyListMono. Le premier implémente la liste des modes du mode vide : le mode vide ne contient aucun mode. Le deuxième implémente les modes d’un mode atomique : un mode atomique contient un seul mode atomique : lui-même.

Attention ! Note importante !

Un mode est immuable (immutable in English), exactement comme une chaîne de caractère en C# ou en Java. Ce qui veut dire qu’ajouter un mode à un mode existant en faisant (en utilisant une des opérations qui feront l'ob jet d'un prochain billet) :

m.Add( m2 );

aboutit à… rien de rien de nada. Comme pour les chaînes de caractères, il faut faire

m = m.Add( m2 )

car toutes les opérations sont des fonctions (qui seraient décorées par des const - http://www.parashift.com/c++-faq-lite/const-correctness.html - en C++ mais ce modificateur n’existe malheureusement pas en C#).

ObtainMode : le cœur

La méthode ObtainMode est centrale à la gestion de ces objets. Elle gère le cache qui est un dictionnaire dont la clef est la version normalisée de la chaîne de caractère d’un mode. Sans être complexe, son implémentation effectue le moins possible d’opérations et elle est un peu longue à commenter de façon détaillée ici. Le code source est disponible ici : https://svn.invenietis.com/svn/CK/Core/trunk/CK.Context/Context/KeyboardMode.cs (anonymous/anonymous)

Le billet suivant traite des opérations possibles sur les modes (et de la façon dont elles sont implémentés).

 

Le modes d’un clavier

par Olivier Spinelli 23. décembre 2009 12:36

Une des idées que je préfère dans le CVK, ce sont les « Modes » du clavier qui généralisent les touches « modifiantes » telles que Ctrl, Alt, Shift et autres Fn, et Alt Gr. Dans la première version du CVK (celle écrite en C++), les modes étaient gérés par un entier dans lequel chaque bit était assigné à un mode : on ne pouvait pas en définir plus de 32 – ce qui ne représentait pas un inconvénient majeur – et leur sémantique était fixée, figée, gravée dans ces bit flags – ce qui était beaucoup plus gênant.

L’idée

Le principe retenu, depuis la version 2, s’affranchit totalement de ces deux limitations et permet d’introduire quelques notions, design patterns et algorithmes intéressants.

L’idée est simple :

  • Les modes sont identifiés par des chaînes de caractères quelconques : « Ctrl », « Frigo », « Majuscule », etc. On les appelle modes atomiques (Atomic Mode).
  • Ils sont combinés simplement en les appariant séparés avec des ‘+’ : « Ctrl+Majuscule », « Frigo+Ctrl+Majuscule ». Ce sont les modes combinés (Combined Mode).
  • A chaque « touche » (Key) est associée un ensemble de « touches réelles » (Actual Keys) qui, chacune, est dédiée à un mode (combiné ou atomique) précis.

L’exemple

Un exemple très simplifié de contexte est donné ci-dessous, qui respecte la structure des objets d’un Contexte : Context / Keyboard / Zone / Key / ActualKey. Ce contexte contient un clavier avec deux touches :

  • Clavier « Démo »
    • Zone « Alphabet »
      • Touche n°1
        • Par défaut (pas de mode actif) : Label = ‘a’, Command = ‘SenKey « a »’
        • En mode « Maj » : Label = ‘A’, Command = ‘SenKey « A »’
        • En mode « Frigo » : Label = ‘Ouvrir Porte’, Command = ‘Domo.OpenFridgeDoor()’
      • Touche n°2
        • Par défaut (pas de mode actif) : Label = ‘b’, Command = ‘SenKey « b »’
        • En mode « Frigo » : Enabled = false

En résumé ce clavier va pouvoir envoyer les caractères ‘a’, ‘A’ et ‘b’ ainsi qu’ouvrir la porte du frigo. Il n’y a plus qu’à se poser quelques questions :

  • En mode « Maj + Frigo », qui gagne ? ‘A’ ou ‘Ouvrir Porte’ ?
  • Si on décide de supprimer le mode « Maj » des modes possibles, que devient l’ActualKey correspondante ? (Bon, là, on se doute qu’il faut la supprimer.)
  • « Shift + Ctrl » est bien le même mode que « Ctrl + Shift », non ?

Il existe bien d’autres questions à se poser ET à résoudre. Un gros paquet. Vraiment.

Normalisation : « Shift + Ctrl » est bien le même mode que « Ctrl + Shift » !

Il est donc nécessaire d’introduire une forme canonique (une représentation pivot, normée) qui permette de s’affranchir de cette dépendance à l’ordre des modes.

Ce n’est pas compliqué : « Shift + Ctrl » est transformé en « Ctrl + Shift » car… « Ctrl » précède « Shift » dans l’ordre lexicographique.

Grâce à cet ordonnancement, les modes combinés sont aussi facilement manipulables que les modes atomiques. D’autre part, l’ordre lexicographique nous apporte aussi une possibilité de désambiguïsation : si « Ctrl » et plus fort que « Shift » (il est devant), alors « Frigo » est plus fort que « Maj » !

Et je peux alors répondre à la première des questions précédentes : en mode « Frigo+Maj » (remarquer que Frigo est devant), c’est ‘Ouvrir Porte’ que voit l’utilisateur.

Cette normalisation ayant été comprise, on peut aborder l’implémentation. La version 2.5 n’a plus grand-chose à voir avec la version 2.0 en termes de gestion des modes. Avant de détailler la nouvelle, je voudrai revenir rapidement sur la précédente afin de montrer ses limites.

La première version

La première version implémentait les Modes comme une classe qui encapsulait la mise en forme canonique dans son constructeur. Un Mode n’était qu’un objet (immuable) autour d’une chaîne de caractère : utiliser ce type de classe est aisé, notamment en définissant des opérateurs de casting implicite de et vers les chaînes de caractères.

Le constructeur de l’objet KeyboardMode acceptait une chaîne de caractère et la normalisait avant de la conserver et de l’exposer en lecture seule : toute chaîne de caractère, à tout moment, pouvait donc être « transformé » en mode. L’aspect « lecture seule » de la chaîne une fois le mode construit garantissait ensuite l’immuabilité d’un tel mode.

L’inconvénient majeur de cette approche est qu’il n’y a pas de centralisation, de réutilisation des différents modes en mémoire : chaque objet mode est, en fait, une « valeur » (au sens des Types Valeurs) qui existe là où elle est nécessaire (dans chaque définition d’ActualKey par exemple).

Ce principe de programmation (une classe avec une sémantique de valeur) a eu plusieurs impacts négatifs :

  1. 1 - Il n’a pas été facile à mettre en œuvre par les étudiants : on ne sait jamais, finalement, s’il vaut mieux utiliser un objet Mode ou une chaîne de caractère. Cette ambigüité a polluée l’API à plusieurs endroits.
  2. 2 - Il n’était pas spécialement performant ni particulièrement extensible (pas d’opportunités pour cacher des informations lourdes ou coûteuse à produire notamment).

Ajoutons à cela que la manipulation de chaînes de caractères systématiquement requise pour travailler sérieusement avec les Modes (intersection, combinaison, soustraction, etc.) a freiné leur mise en œuvre et les développements des fonctionnalités s’y attachant.

Il était temps (et amusant :-)) de faire quelque chose. L’implémentation nouvelle est disponible dans la 2.5 et fera l’objet de très prochains billets.

Sexe aléatoire, nullité et Sql

par Olivier Spinelli 14. décembre 2009 12:19

Avec un titre comme ça, je suis sûr d’avoir 2 ou 3 lecteurs. Je lis actuellement « Sql for smarties » de Joe Celko. Tout n’est pas parfait dans ce livre, mais il y a certaines choses amusantes que je découvre avec plaisir : par exemple que le genre est normalisé par ISO 5218 « Information technology — Codes for the representation of human sexes ». Il s’agit d’un chiffre unique désigné dans le standard par « SEX » dont les quatre valeurs possibles sont :

  • 0 = inconnu (unknown),
  • 1 = homme (male),
  • 2 = femelle (female),
  • 9 = non applicable (not applicable).

Note 1 : Les rédacteurs du standard ont pris la peine de préciser que le fait que Homme était 1 et la Femme 2 n’avait aucune signification particulière... que cela était dû aux pays qui ont « poussé » ce standard. Et vous reconnaissez le premier chiffre du numéro de sécurité sociale français : c’est une preuve irréfutable du rayonnement culturel de la France dans le monde.

Note 2 : Sexe « non applicable » ne recouvre pas d’accident biologique particulier, il s’agit d’une valeur à utiliser pour des personnes morales (une entreprise n’est donc ni homme ni femme – bien au contraire aurait ajouté Pierre Dac).

Note 3 : Je viens, l’air de rien, de vous faire économiser 92 francs suisses, soit environ 65 euros. C’est en effet le prix à payer pour le PDF officiel de la norme ISO 5218. C’est scandaleux mais c’est comme ça : les standard ISO sont payants.

Revenons au SEX (avec ce billet, ce blog va perdre des points de ranking) et à SQL pour insister sur « l’inconnu » et le « non applicable ». Ces deux notions se cachent habituellement derrière le NULL, ici elles ont été explicitées et c’est une bonne chose d’un point de vue lisibilité/expressivité ainsi qu’en terme de mise en œuvre (moins on utilise de colonne NULLABLE, mieux on se porte et je ne vais pas faire ici le topo classique de la logique ternaire et des problèmes subtils introduits par le NULL car on les trouve partout).

Une autre découverte pour moi de la lecture du livre de Joe Celko est un aspect de l’instruction CASE que j’ignorais. Supposons que l’on veuille initialiser une valeur aléatoire pour un SEX. Nous avons 4 valeurs à tirer au sort (de façon équiprobable), le code suivant semble correct :

select case CAST( (4*rand()) as int )
          when 0 then '0' -- Unknown
          when 1 then '1' -- Male
          when 2 then '2' -- Female
          when 3 then '9' -- Unapplicable
       end

Et pourtant il ne l’est pas. Ci-dessous un test tout bête : il remplit 200 000 lignes d’une table avec une colonne SEX aléatoire selon le code précédent.

create table dbo.tTest( Sex char );
-- Caution: Declare and initialize in the same statement like this 
-- works only on Sql Server 2008, not 2005. 
declare @i int = 200000; 
while @i > 0
begin
	insert into dbo.tTest( Sex )
		select	case CAST( (4*rand()) as int )
					when 0 then '0' -- Unknown
					when 1 then '1' -- Male
					when 2 then '2' -- Female
					when 3 then '9' -- Unapplicable
				end
	set @i = @i-1;
end

Et on affiche le contenu de la table résultante avec le compte et la probabilité résultante.

select 	Sex, 
        COUNT(*), 
        Prob = cast(COUNT(*) as float)/200000 
    from dbo.tTest
   group by Sex
   order by 1;

Première observation : NULL apparait.

Deuxième constat : les nombres respectifs de valeurs (et donc leurs probabilités) ne sont pas équitablement répartis.

SexCountProb
NULL633380,31669
0500670,250335
1373200,1866
2279180,13959
9213570,106785

Les matheux auront compris. L’instruction CASE est, en fait, réécrite ainsi :

select	case 
		when CAST( (4*rand()) as int ) = 0 then '0' -- Unknown
		when CAST( (4*rand()) as int ) = 1 then '1' -- Male
		when CAST( (4*rand()) as int ) = 2 then '2' -- Female
		when CAST( (4*rand()) as int ) = 3 then '9' -- Unapplicable
	end

Ce qui ne correspond pas du tout à l’idée initiale, et pouf pas marche.

La solution est évidemment de capturer la valeur fournie par la fonction rand() dans une variable:

declare @p int = CAST( (4*rand()) as int );
select	case @p
			when 0 then '0' -- Unknown
			when 1 then '1' -- Male
			when 2 then '2' -- Female
			when 3 then '9' -- Unapplicable
		end

N'incriminez pas SQL Server : ce sont les spécifications de SQL. C’est comme ça et pas autrement que le CASE doit être implémenté, la forme « classique » du switch-case que l’on connait dans de nombreux langages n’est pas celle réellement supportée par SQL, elle n’est qu’un sucre syntaxique autour du « râteau de si ». SQL ne connait vraiment que le râteau !

Pour finir, un exercice : montrer en calculant les probabilités formellement que le comportement observé (NULL et probabilités différentes) est parfaitement logique.

Les arguments d’un évènement

par Olivier Spinelli 17. septembre 2009 11:19

Suite à une remarque d’Antoine, je me sens obligé aujourd’hui d’expliquer un point de conception lié à la modélisation des évènements dans CK.

Néanmoins, avant de plonger dans la technique, une remarque préliminaire s'impose:

L'Académie française, dans la neuvième édition de son Dictionnaire, écrit, en accord avec les recommandations du Conseil supérieur de la langue française de 1990, évènement. La graphie ancienne événement n'est cependant pas considérée comme fautive, encore que rien ne la justifie plus. Sa survivance s'explique par le fait que la régularisation de ce mot, ainsi que de quelques autres, d'abrègement à vènerie, avait été oubliée lors de la préparation tant de la septième édition (1878) que de la huitième (1935).
www.academie-francaise.fr/langue/rectifications_1990.pdf

Hériter… ou pas.

En règle générale, les concepteurs essayent d’éviter autant que possible les arbres d’héritage profonds : une structure d’héritage « flat » (et orthogonale) est toujours préférable (à une horreur de type MFC) car elle garantit à la fois une meilleure lisibilité (et donc maintenabilité) et réutilisabilité. Pourtant, dans CK, il existe un endroit où la structure d’héritage n’est pas du tout, mais alors vraiment pas, plate… Il s’agit des objets arguments d’évènements. Par exemple, l’argument de l’évènement correspondant à l’échange des modes de deux touches réelles. Cette classe s’appelle ActualKeyModeSwappedEventArgs et voici sa chaîne d’héritage:

ActualKeyModeSwappedEventArgs Les modes de deux touches ont été échangés.
 > ActualKeyModeChangedEventArgs  Le mode d’une touche a changé.
  > ActualKeyEventArgs L’évènement concerne une touche réelle.
   > KeyEventArgs L’évènement concerne une touche.
    > KeyboardEventArgs L’évènement concerne un clavier.
     > ContextEventArgs L’évènement concerne un contexte CK.
      > EventArgs Un évènement s’est produit.

Six classes de base !

Pourquoi les arguments d’évènements s’inscrivent-ils dans une chaîne d’héritage si profonde ? Le concepteur aurait-il abusé de substances illicites ? Que nenni (enfin si mais cela n’a pas eu d’impact sur ce qui nous occupe). Nous avons là une exception qui confirme la règle de la platitude (je ne sais pas si c’est la seule exception), car cette chaîne d’héritage permet aux évènements d’être interceptés à n’importe quel niveau de précision. Et c’est bien pratique.

Le bon héritage

Relevons d’abord un aspect important : cette conception respecte totalement la sémantique de la spécialisation et ne crée aucun des problèmes souvent rencontrés lorsque l’on abuse de l’héritage.

  • Sur le respect de la sémantique d’abord, analysons la chaîne présentée ci-dessus :
    Le fait d’échanger les modes de deux touches est un (is_a) changement de mode qui est un (is_a) évènement d’une touche réelle qui est un (is_a) évènement d’une touche qui est un (is_a) évènement d’un clavier qui est un (is_a) évènement d’un contexte qui est un (is_a) évènement.
    Ce respect de la règle est un (is a en anglais) est la règle fondamentale à respecter lorsque l’on spécialise une classe. Si on ne peut pas dire A est un B, alors A ne devrait pas hériter de B (les exceptions à cette règle existent - mixins et autres héritages de réutilisation – mais ne les commettez qu’en toute connaissance de causes et d’effets).
  • Sur la mise en œuvre d’autre part, il faut noter que ces « classes évènements » sont implémentées sans mettre en œuvre de propriétés ou méthodes protégées : ici, l’héritage ne peut « casser l’encapsulation » (chercher dans votre moteur préféré « inheritance breaks encapsulation » pour plus d’information sur ce sujet parfois un peu subtil). Chaque classe apporte des propriétés publiques qui décrivent, précisent, l’évènement correspondant : aucune réutilisation d’implémentation n’est faite ici, on est dans le cas d’un sous-typage (subtyping) plutôt que d’un héritage (inheritance).

Cet usage de l’héritage est donc correct en regard des canons de l’ingénierie logicielle. Mais à quoi cela sert ?

« Client side » : je consomme l’évènement

Comme je l’ai déjà écrit ci-dessus, l’intérêt est de faciliter la prise en compte de ces évènements au « niveau de détail » que l’on souhaite. Considérons l’évènement correspondant à un des arguments ci-dessus :

public interface IKey
{
   ...
   event EventHandler ActualKeyModeChanged;
   ...
}

En tant que client, je peux inscrire ces évènements auprès de n’importe quelle fonction dont la signature respecte le type de l’argument en tenant compte de l’héritage. Je peux, bien évidemment, m’inscrire ainsi à l’évènement :

   IKey k = ...;
   k.ActualKeyModeChanged += OnKeyModeChanged;
   ...

void OnKeyModeChanged( object source, ActualKeyModeChangedEventArgs e )
{
   ...
}

Mais, je peux aussi m’intéresser à ce même évènement ainsi :

   IKey k = ...;
   ...
   k.ActualKeyModeChanged += OnSomethingChangedInKey;
   k.KeyPropertyChanged += OnSomethingChangedInKey;
   ...

void OnSomethingChangedInKey( object source, KeyEventArgs e )
{
   ...
}

Ou même :

   IKey k = ...;
   ...
   k.ActualKeyModeChanged += OnSomethingHappened;
   k.Keyboard.AvailableModeChanged += OnSomethingHappened;
   k.Context.CurrentKeyboardChanged += OnSomethingHappened;
   ...

void OnSomethingHappened( object source, EventArgs e )
{
   ...
}

Vous voyez l’idée : comme le changement de mode d’une touche réelle est un (is_a) évènement, je peux y souscrire justement comme à un évènement quelconque, je ne suis pas contraint d’utiliser le type précis de l’argument.

« Server Side » : j’émets un évènement

Il existe une autre utilisation possible de cet héritage si l’on se place de l’autre coté de la barrière : celui où on émet l’évènement plutôt que de le recevoir. En l’occurrence, il se trouve que ActualKeyModeSwappedEventArgs et ActualKeyModeChangedEventArgs illustrent cette utilisation.

Le premier correspond à l’échange des modes de deux touches, le deuxième (qui généralise le premier) à un changement de mode d’une touche.
Les interfaces IKey et IKeyboard exposent toutes deux l’évènement ActualKeyModeChanged. Le fait que le changement soit dû à un échange plutôt qu’à un changement direct est un détail pour la grande majorité des clients (les plugins en l’occurrence), mais on souhaite quand même exposer la cause de l’évènement à ceux qui sont susceptibles de s’y intéresser. La première idée est d’exposer un deuxième évènement :

public interface IKey
{
   ...
   event EventHandler<ActualKeyModeChangedEventArgs> ActualKeyModeChanged;
   event EventHandler<ActualKeyModeSwappedEventArgs> ActualKeyModeSwapped;
   ...
}

Mais cela pose un vrai problème de conception : tout déclenchement de l’évènement Swapped doit être doublé d’un déclenchement de Changed sinon un client qui ne s’est inscrit qu’au premier ne verra jamais les changements causés par un échange de mode. Outre la pollution de l’interface IKey par un évènement peu utilisé, cela entraîne aussi le fait que le client qui s’intéresse aux deux devra dédoublonner les évènements pour éviter de les prendre en compte deux fois… Ce ne sera pas fait. Ou mal fait. C’est ce qu’on appelle une usine à bugs.

La bonne solution consiste à utiliser l’héritage dans l’autre sens. On ne publie qu’un seul évènement :

public interface IKey
{
   ...
   /// 
   /// Fires whenever the mode of one of our <see cref="ActualKeys"/> changed.
   /// The event argument may be an instance of the 
   /// <see cref="ActualKeyModeSwappedEventArgs"/> class if the change 
   /// is the result of a call to <see cref="IActualKey.SwapModes"/> 
   /// instead of <see cref="IActualKey.ChangeMode"/>.
   /// 
   event EventHandler<ActualKeyModeChangedEventArgs> ActualKeyModeChanged;
   ...
}

Et son argument sera du type ActualKeyModeSwappedEventArgs dans le cas du Swap, ce qui est facile à tester par les clients que cela intéresse :

void OnKeyModeChanged( object source, ActualKeyModeChangedEventArgs e )
{
   ActualKeyModeSwappedEventArgs eSwap = e as ActualKeyModeSwappedEventArgs;
   if( eSwap != null )
   {
      // The change is due to a swap. The event argument gives us the swapped key.
      IActualKey other = eSwap.SwappedKey;
      ...
   }
   else
   {
     // Handle ChangeMode event.
     ...
   }
}

En résumé, cette technique permet de ne pas polluer les interfaces avec des évènements rarement utilisés et de permettre une prise en compte simple et efficace de « sous-catégories » précises d’évènements.

Conclusion

Le modèle des évènements de .Net, qui repose sur les délégués (delegates) et la signature standard (object source, EventArgs e) est simple et puissant. A condition de l’exploiter correctement, et donc de l’avoir compris.

Cette discussion, qui nous a amené à traiter du bon usage de l’héritage, introduit deux aspects de l’utilisation de l’héritage en conception. Pour aller plus loin, je vous encourage à regarder du coté de la co-variance et de la contra-variance qui sont des notions très proches de ce que nous avons présenté ici.

Dans un prochain post traitant des évènements, j’aborderai de ce que je considère comme une des (rares) erreurs de conception du framework .Net : la classe EventArgs elle-même…

La roadmap de CiviKey : déjà une V2.5 ?

par Olivier Spinelli 26. août 2009 15:25

Le nom final du produit CVK a été trouvé et accepté lors d’une réunion des partenaires actuels : il s’agit de CiViKey (ou CiviKey ?). Compromis certes (l’historique est préservé) mais promesse d’une communication grand public facilitée.
 
C’est l’occasion pour moi de faire un petit point sur les aspects techniques : un état des lieux d’abord et un éclairage sur le futur ensuite.
 
Ce que l’on appelle aujourd’hui CiviKey a pour nom de code CVK-V2. A l’origine, ce projet n’était qu’un support de cours qui avait pour objectif :

  • D’effectuer une « Modélisation » en direct, « Les claviers » étant la réalité à modéliser.
    (Note : Le fait que des notions « irréelles », applicables uniquement à la modélisation était un aspect particulièrement intéressant n’a guère excité que moi… Solitude du pédagogue…
    Exercice : isoler ces aspects propres au modèle dans le code.)
  • De mettre en œuvre des collections, des accesseurs complexes, d’encapsuler, de sécuriser une API (au sens de limiter le développeur à des actions en accord avec la donnée), de couvrir autant de types de relations directs entre objets possibles (cycle de vie, attachement/détachement, unicité de nommage, multiplicité d’accesseurs), etc.
  • De réaliser une application graphique en .Net (un petit peu plus complexe que les applications scolaires traditionnelles).

Nous n’avons pas eu le temps, au moment où la décision a été prise d’utiliser cette base pour la V2, de revoir le noyau. De fait, la conception initiale du Contexte (Keyboard, Zone, Key, etc.) a couvert les besoins durant deux ans en supportant très correctement l’ajout des fonctionnalités : la gestion des plugins, des propriétés dynamiques (le Shared Dictionary), de la persistance, des éditeurs, etc.
 
Ayant relu attentivement le code dernièrement, j’ai néanmoins tiqué sur 2 aspects très discutables de l’implémentation actuelle car peu ou mal spécifié et réfléchis (ce qui est nettement plus grave J) que sont :

  1. la gestion des Modes du clavier (pour des raisons qui seraient un peu longue à détailler ici) ;
  2. le support du copier-coller, principalement car elle se contente d’utiliser « de l’extérieur » l’import/export xml original (destiné à la persistance) alors qu’il eut fallu une refonte/adaptation/extension de l’import afin de supporter naturellement le collage.

Nous avions par ailleurs pris la décision il y a quelques mois de masquer l’implémentation (absolument tous les objets d’implémentation) au profit d’un modèle public purement abstrait (constitué uniquement d’interfaces) et ce afin de maximiser les capacités d’évolutions du noyau. Ce travail de refactoring est un très classique processus de découplage que nous pensons nécessaire compte tenu des objectifs de pérennité de l’application.
 
Enfin, une évolution très importante en termes de fiabilité est actuellement en attente dans les cartons : la création dynamique de proxy d’interception des Services supportés par les Plugins qui ont pour objectifs :

  • d’isoler les plugins mal programmés (qui ne respectent pas les règles du jeu en terme d’appel des autres plugins ou d’émission d’événements selon l’état – Running/Stopped – des plugins) ;
  • de pouvoir éventuellement tracer tous les appels inter-plugins (idéalement, il est possible de décider des traces pour chaque méthode ou événements du système en cours d’exécution) ;
  • d’être une démonstration de la <pub>capacité de .Net à faire des trucs impressionnants et ce, tout bien pesé, assez facilement </pub>.

Le proxy (le modèle ainsi que la génération dynamique de code IL) est prêt. Il ne contient pas les fonctionnalités d’interception en tant que telle (et évidemment encore moins sa « configurabilité »). Il est maintenant nécessaire de l’intégrer et de fournir les objets proxy aux plugins plutôt que les objets Plugins eux-mêmes.

Regroupées, ces évolutions sont assez lourdes et nous avons donc décidé d’en faire une mile stone importante de release dans les prochains mois : CiviKey (CVK-V2) n’est pas encore (vraiment) sorti qu’une version 2.5 se profile déjà.

Le Nom de la Chose

par Olivier Spinelli 12. août 2009 11:09

Quel doit être le nom du CVK ?
Je ne sais pas. Il y a des gens autrement plus qualifiés que moi qui se posent la question. En attendant, on a besoin d’avancer et ce genre de détail a un impact sur le code.
Dans la version actuelle, un très grand nombre de classes sont préfixées par CVK (CVKContext par exemple). Nous avons décidé de supprimer ce préfixe.
Il reste cependant l’espace de nommage : CVK.Core, CVK.Model, etc.
Si le nom retenu finalement est Ubikey (par exemple ;-)), va-t-il falloir tout renommer en UBK ? Si oui, il faudra aussi renommer les répertoires du système de fichier (il serait très désagréable de travailler avec un tel décalage).
 
Non seulement cela demande quelques heures de travail, mais renommer des répertoires proche de la racine « casse » assez violemment le dépôt de code source sous Subversion (notamment la consultation de l’historique). (Oui, je sais, normalement cela fonctionne très bien le renommage dans SVN… J’ai néanmoins eu des très mauvaises expériences.)
 
Parallèlement à cela, en essayant de faire passer FxCop sur les sources, cette règle est apparue :
 
 Resolution   : "Correct the casing of 'CVK' in assembly name 'CVK.SharedDic.dll'
                 by changing it to 'Cvk'."
 Help         : http://msdn2.microsoft.com/library/ms182240(VS.90).aspx  (String)
 RuleFile     : Naming Rules  (String)
 Info         : "Type, namespace, and member identifiers are Pascal-cased.
                 Parameter identifiers are camel-cased. Two letter acronyms
                 within these identifiers should be upper-cased, for
                 example, use System.IO instead of System.Io. Acronyms
                 of three or more letters should be Pascal-cased, for
                 example, use System.Xml instead of System.XML. The
                 pascal-casing convention capitalizes the first letter
                 of each word, as in BackColor. The camel-casing convention
                 formats the first letter of the first word in lowercase
                 and capitalizes the first letter of all subsequent
                 words, as in backgroundColor. Although it may be common
                 practice for some two letter acronyms to not be fully
                 capitalized, violations of this rule should not be
                 excluded for this reason. For example, 'DbConnection',
                 is common but incorrect; use DBConnection. A violation
                 of this rule might be required for compatibility with
                 existing, non-managed symbol schemes. In general, however,
                 these symbols should not be visible outside the assembly
                 that uses them."

 
En substance, il faut renommer CVK en Cvk pour être parfaitement en ligne avec les règles de nommage… Ou alors, si l’on souhaite conserver les majuscules, il faut retirer une lettre…
 
Du coup, j’ai décidé de nommer (une bonne fois pour toute je l’espère) l’objet technique qu’est ce Custom Keyboard en… Custom Keyboard. Ce qui nous fait un assez satisfaisant (de mon point de vue) espace de nommage (et préfixe éventuel) CK.
 
Pour conclure, il est clair que CK : « C’est le Kode ! »
Cela ne présage nullement de la dénomination officielle du produit. Mais au moins, on peut bosser.

Tags: ,

CiviKey

Powered by BlogEngine.NET