Le modes d’un clavier

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.

L’intégration continue avec Cruise Control .NET

Est-il nécessaire de présenter la notion d'intégration continue (Continuous Integration) ?

Le principe (en résumé) est le suivant : chaque fois que le développeur contribue au projet en ajoutant une mise à jour ou un fichier au dépôt central (on parle de commits), le projet est automatiquement recompilé, testé, et éventuellement délivré à nouveau.

C'est le travail du serveur d’intégration que d’automatiser ces tâches. En l'occurence, nous utilisons Cruise Control.Net que nous avons configuré pour supporter le principe d'organisation des codes sources mis en place dans le projet CiviKey et présenté précédemment.

Les quatres actions de base sont :

  1. Compilation en mode Debug
  2. Tests unitaires
  3. Compilation en mode Release
  4. Tests FxCop

Ces actions sont lancées après un commit (toute modification) dans :

  • /trunk
  • /releases/{VERSION}/trunk
  • /releases/{VERSION}/tags/{TAG}

Lors d’un commit dans /trunk il y a en plus une vérification du contenu du commentaire. S’il contient [DoRelease] ET que le fichier release.txt a été modifié (ie. il fait partie du commit - ceci est une sécurité) alors une nouvelle version sera créée automatiquement lors du commit (et donc une compilation du trunk de la version sera effectuée).

Le fichier release.txt se présente de la façon suivante :

# This file is used to generate a new Release branch.
# It contains:
# - ReleaseNumber: RELEASE-NUMBER (format X.X.X like 2.4.12 - major.minor.revision expected)
# - RevNumber: OPTIONAL-REVNUMBER-TO-RELEASE
# This is optional: it enables a "release from the past" if needed.
# By default, we take the SVN « RevNumber » of this file:
# since this file necessarily belongs to the commit, it is the current (the 'head' revision) /trunk version.

ReleaseNumber: 2.4.7

# RevNumber: 10 (Optional: to be used only if one want a Release based on a /trunk past version.)

Lors d’un commit dans /releases/{VERSIONS}/trunk il y a en plus une vérification du contenu du commentaire. Si il contient [Tag] ET que le fichier tags.txt a été modifié alors un tag sera créé dans cette version automatiquement lors du commit (et donc une autre compilation, mais de la version taguée sera effectuée).

Le fichier tags.txt se présente de la façon suivante :

# This file is used to generate a new Tag.
# It contains:
# - TagName: OPTIONAL-TAG-SUFFIX
# - RevNumber: OPTIONAL-REVNUMBER-TO-TAG
# This is optional: it enables a "Tag from the past" if needed.
# By default, we take the SVN « RevNumber » of this file:
# since this file necessarily belongs to the commit, it is the current (the 'head' revision) /releases/{VERSIONS}/trunk version.

TagName: Final

#RevNumber: 10

Lors de la création d’un tag /releases/{VERSION}/tags/{TAG} il y a la génération de l’aide correspondant à cette version (CHM + site web) ainsi que la génération du package pour cette version.

Grâce à tout cela, CiviKey est définitivement sous contrôle !

Organisation des Codes Sources du projet CiviKey

Nous avons vu dans un post précédant que le serveur SVN pour CiviKey était accessible via l’url http://svn.invenietis.com/svn/CK et que le serveur d’intégration continue était accessible à l’URL http://ci.civikey.invenietis.com/.

Dans ce post nous allons voir la gestion du repository SVN ainsi que de Cruise Control .NET pour le projet CiviKey. Ce qui est présenté ici n'est mis en place que pour la version 2.5 de CiviKey (celle sur laquelle nous concentrons actuellement nos efforts).

Le repository SVN

Le noyau de CiviKey est divisé en deux: le Core, qui contient le noyau proprement dit et Certified qui contient les plugins certifiés. L’architecture globale est la suivante :

Structure des fichiers de CiviKey

/CK Root du repository Svn de CiviKey
/CK/Core Projet CK.Core
/CK/Core/trunk Trunk des développements du projet CK.Core
/CK/Core/releases Dossier des releases de CK.Core (cf. ci-dessous)
/CK/Certified Projet CK.Certified
/CK/Certified/trunk Trunk des développements du projet CK.Certified
/CK/Certified/releases    Dossier des releases de CK.Certified
/CK/Document/trunk Ensemble des documents

Organisation des Versions

Les "releases" sont gérées à de la façon suivante. Le projet DEMO (qui est un projet similaire à Core) détaille l'organisation de l'arborescence des releases :

/trunk Trunk principale du Produit.
/releases Dossier contenant l’ensemble des versions du Produit.
/releases/VER Une des versions du Produit.
/releases/VER/trunk Trunk de développement permettant de corriger cette version.
/releases/VER/tags Dossier contenant l’ensemble des tags pour cette version du Produit.
/releases/VER/tags/TAGS Un tag contenant les sources a un certain stade.

Ce système nous permet de gérer au mieux la montée en version du projet tout en préservant la liberté du développeur. Et il y tient le développeur à sa liberté.

Une petite histoire

Les développeurs travaillent dans /trunk qui est l’espace de développement principal.
Ils font des évolutions, des changements dans l’API pour mettre en place les nouveautés, et, un jour une version est prête à voir le jour, les développeurs décident donc de créer une Version, disons la 2.5.0.

Un nouveau dossier est créé dans /releases : /releases/2.5.0/trunk. Une Version est née, elle n'est pas parfaite (elle va avoir besoin de quelques retouches pour être finalisée) et va donc évoluer et donner naissance à des Tags.

Le premier de ces tags s'appelle CTP (pour Community Technology Preview, il aurait pu s'appeler Alpha ou PreAlpha ou autre nom qui indique que ce n'est pas vraiment sec) : /releases/2.5.0/tags/CTP apparait dans l'arborescence. Noter que les codes sources dans /tags n'évoluent pas: ils représentent des clichés, des instantannés, du /releases/2.5.0/trunk qui leur a donné naissance. Le trunk bouge, les tags figent.

Cet espace permet aux développeurs de continuer à travailler dans /trunk et donc de continuer à faire évoluer le noyau, pendant que d’autres travaillent pour la Version /releases/2.5.0/trunk afin de régler les derniers bugs qui pourraient exister. Une fois les derniers bugs corrigés dans le /trunk les développeurs créent le tag /releases/2.5.0/tags/Final (et espèrent très fort éviter le petit frère /releases/2.5.0/tags/SP1).(Voir http://en.wikipedia.org/wiki/Software_release_life_cycle pour des exemples de noms de révision.)

Pendant ce temps les développeurs du /trunk se sont lancés dans la future version 2.5.1 et ont d'ores et déjà produit une release en Alpha ce qui signifie qu’un dossier /releases/2.5.1/trunk a été créé ainsi qu’un tag /releases/2.5.1/tags/Alpha.

Cette petite histoire nous amène au dépôt suivant :

Avec ce système la version d’un projet à une vraie existence : les corrections sont possibles sans (trop) d'interférences ce qui libère le développeur, qui sait clairement s’il peut imaginer de nouveaux bugs (/trunk) ou s’il doit réfréner sa créativité (/releases).

Infrastructure technique du projet CiviKey

Invenietis a mis en place, pour le projet open source CiviKey, une infrastructure complète de gestion de projet technique. Cette infrastructure va continuer d’évoluer dans le temps, mais la base décrite dans ce poste est en place. Un autre post sera dédié à la gestion du repository SVN (dépôt SVN en français) et du serveur d’intégration. Nous avons choisi de mettre en place un certain nombre de services que l’on retrouve dans un grand nombre de projets. Ces services ont été mis en place à l’aide d’autre projet Open Source et à des modules spécifiquement développés pour la problématique de CiviKey.

Gestion du projet

La gestion projet a été déléguée à un outil fonctionnant en Ruby, Redmine (http://www.redmine.org). Il est utilisé par Invenietis en interne pour la gestion de projet ainsi que pour le dialogue avec nos clients. Cet outil permet de gérer l’ensemble des tâches lié à un ou plusieurs projets en proposant une interface web simple et un environnement unique. (Pour ceux qui connaissent, c'est un Track simple, fonctionnel, et multi projet.)

Ce service est accessible via l’URL : http://civikey.invenietis.com/

Repository SVN

La gestion des sources a été déléguée à un serveur SVN sous Windows à l’aide de VisualSvn Server (http://www.visualsvn.com/server/). Ce serveur donne accès à l’ensemble des sources du projet CiviKey.

Compte tenu du refactoring (assez violent) en cours, les dépôts de la version 2.0 et 2.5 n’ont pas la même structure.

Version CiviKey 2.0 :

         Core : https://svn.invenietis.com/svn/CK/branches/Core-Summer09

         Contrib : https://svn.invenietis.com/svn/CK/trunk/Contrib

Version CiviKey 2.5 :

         CK.Core : https://svn.invenietis.com/svn/CK/Core/trunk

         CK.Certified : https://svn.invenietis.com/svn/CK/Certified/trunk

Un premier autre post sera dédié a la gestion du repository par rapport à la gestion des releases et des tags de version.

Service d’intégration continue

Pour garantir la qualité des éléments développée dans ce projet, nous avons mis en place un service d’intégration continue qui permet de faire un ensemble d’opérations à chaque commit de développeur. Ce service a été mis en place grâce à Cruise Control .NET (http://ccnet.thoughtworks.com/). Il permet entre autres de compiler en environnement contrôlé, de faire passer des tests unitaires plus spécialisés que sur les postes de développement ou encore de gérer les releases des projets.

Ce service est accessible via l’URL : http://ci.civikey.invenietis.com/

Cruise Control  génère aussi la documentation type MSDN pour chaque release taguée du projet. Elles sont disponibles ici : http://help.civikey.invenietis.com/.

Un deuxième autre post sera dédié à la gestion de Cruise Control par rapport à la gestion des releases.

 

Les arguments d’un évènement

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…