Planet Valtech

January 23, 2012

7P… puissant pour préparer efficacement vos workshops!

Après les 4 P du marketing (revisités ou non par l’ère digitale), voici aujourd’hui un outil de facilitation et surtout de préparation de workshops à la fois ultra simple, visuel et terriblement efficace. Créé par James Macanufo (l’un des 3 auteurs de Gamestorming), les 7P vous permettent d’aller à l’essentiel, de bien cadrer votre workshop, bref [...]

by jc-Qualitystreetjc-Qualitystreet atJanuary 23, 2012 06:15 PM

January 22, 2012

Truc de coach: un Backlog enrichi et visuel pour ne pas perdre le fil… de vos estimations

Vous savez que je suis un fervent défenseur de l’utilisation d’un backlog de produit (liste de tous les éléments sources de valeur et nécessaires à l’équipe pour réaliser un produit) “PHYSIQUE” c’est à dire affiché au mur au sein du radiateur d’informations… du coup aujourd’hui, je vous propose un nouveau format “enrichi” pour ce backlog… Ce [...]

by jc-Qualitystreetjc-Qualitystreet atJanuary 22, 2012 11:21 PM

January 19, 2012

Centre Agile, Entreprise Agile: 6 Pistes vers l’infini et au delà…

L’agilité se joue désormais au niveau organisationnel. Selon les cas, on parle donc de Culture Agile, de Centres Agiles voire même, but ultime, d’Entreprise Agile, et cette nouvelle capacité qu’a l’Entreprise de : créer de la valeur et de ravir son client, tout en favorisant et en s’adaptant -à temps- aux changements de son environnement (Grosjean, 2011) Pourtant, [...]

by jc-Qualitystreetjc-Qualitystreet atJanuary 19, 2012 10:48 AM

January 18, 2012

Petit déjeuner sur le “Cloud Computing” le 16 février à Paris

Après le mainframe, le réseau, l’ordinateur personnel ou Internet, le cloud computing s’impose depuis plusieurs années comme la nouvelle avancée technologique dans le domaine de l’informatique d’entreprise. Comme toute évolution majeure, cela entraine une mutation des habitudes, autant pour l’exploitant que pour le développeur. A l’occasion d’un petit déjeuner à Paris le 16 février, Valtech vous [...]

by Olivier RodriguesOlivier Rodrigues atJanuary 18, 2012 02:40 PM

January 16, 2012

Tab9

I – Introduction

La plupart des clients veulent à juste titre, savoir quel sera leur ROI s’il décidait d’investir dans le déploiement des pratiques agiles sur leur activité projet, de maintenance ou de réalisation de nouvelles applications.

Afin de pouvoir apporter des éléments concrets à cette problématique incontournable, je propose de m’appuyer sur un cas concret décrit principalement par :

  • Un contexte client;
  • Des budgets;
  • Des natures de projet diverses;
  • Une organisation spécifique;
  • Une demande plutôt légitime.

II - Problématique du client

Contexte client :

Un client possède un département informatique représentant 100 projets et 700 personnes dont environ 400 intervenants extérieur.

Le middle management est réfractaire à l’agilité et doit donc être convaincu par la Direction qui souhaite déployer les pratiques agiles et qui compte sur un fournisseur pour l’aider via une présentation officielle mettant en avant le ROI d’une telle démarche.

Budget du client :

Le budget annuel des projets (maintenance comprise) représente environ 20 M€.

Ce client peut dépenser 1 M€ pour déployer l’agilité sur son périmètre projet.

Natures de projet :

Les projets réalisés dans le département informatique du client se répartissent aujourd’hui de la façon suivante :

  • Projets de maintenance : 40% du budget;
  • Projet de réalisation :       60% du budget.

Organisation spécifique :

En fin d’année d’Octobre à Décembre, le client travaille sur les budgets de l’année suivante en identifiant ses nouveaux projets.

En début d’année suivante, le client déstaffe en masse les équipes projet de Janvier à Février (2 mois) durant lesquels ses équipes spécifient les nouveaux projets.

Début Mars, le client démarre les nouveaux projets et staffe à nouveau ses équipes avec de nouveaux intervenants extérieurs.

Demande du client :

Le client  souhaite connaître l’ordre de grandeur du ROI lié à l’adoption de pratiques projets agiles.

Ce ROI est évalué par le client en nombre de projets qu’il peut réaliser avec un même budget annuel. Il souhaite par conséquent augmenter le nombre de projets réalisés en une année grâce à l’agilité et en parallèle, diminuer la part prise par les projets de maintenance.

III – Analyse des données client

Aucune information détaillée sur la taille actuelle des 100 projets réalisés annuellement n’est connue.

Aucune information détaillée sur la nature des activités de maintenance (corrective ou évolutive) n’est connue, ni sur la taille des “defect backlogs”.

Aucune information détaillée concernant les frais de structure et d’encadrement n’est connue.

IV – Hypothèses retenues

Les hypothèses retenues proviennent de statistiques ou du contexte client devant être discutées avec le client à l’occasion d’interviews ou de réunions supplémentaires.

IV.I – Statistiques

Taux de succès des projets agiles / projets traditionnels (www.agilemodeling.com de Scott W. Ambler)

  • Taux de succès des projets traditionnels : 50%
  • Taux de succès des projets agiles : 65%

 Les tailles moyennes des équipes agiles se répartissent de la manière suivante :

78% des équipes agiles ont une taille d’équipe de 5 à 15 personnes. Cette taille de projet constitue la taille optimale pour l’agilité, évitant des organisations de management plus lourde et donc moins agiles pour des équipes importantes, et des équipes trop petites (moins de 5 personnes) pour conserver des pratiques agiles efficaces.

Dans 65% des cas environ, la satisfaction des clients a été améliorée par la mise en œuvre de l’agilité sur les projets. Il faut dire que statistiquement, il est courant de constater à posteriori que plus de 40% des fonctions livrées n’ont quasiment jamais été utilisées par les opérationnels. Enfin 56% des problèmes rencontrés en cours de projet proviennent des besoins exprimés.

   « The state of agile development » Juin 2010, FrenchSUG : Analyse des méthodes agiles – 2009

                                                                     

Dans plus de 74% des cas, la productivité des équipes est améliorée par la mise en œuvre de pratiques agiles.

« The state of agile development » Juin 2010, FrenchSUG : Analyse des méthodes agiles – 2009

Dans plus de 68% des cas, la qualité des logiciels produits est améliorée. 

 

« The state of agile development » Juin 2010, FrenchSUG : Analyse des méthodes agiles – 2009

IV.II – Hypothèses client

  • Taille actuelle des projets : nous pouvons par exemple imaginer que les petits projets (moins de 5 personnes) sont en plus grand nombre que les gros projets (supérieure à 15  personnes) et que 60% des projets ont une taille comprise entre 5 et 15 personnes. Soit une hypothèse de distribution théorique de 15% de gros projets, 60% de projet moyens et 25% de petits projets.
  • Les 100 projets réalisés sont considérés comme étant réalisés hors maintenance, c’est à dire qu’il n’inclut pas les projets de maintenance.
  • La nature des activités de maintenance est inconnue pour le moment. S’agit-il de maintenance corrective uniquement (correction d’anomalies) ou de maintenance corrective et évolutive avec des nouvelles fonctions à réaliser en plus des anomalies à corriger ? Soit une hypothèse de distribution théorique entre correction et évolution de 30% de correction et 70% d’évolution.
  • Chaque année, 400 extérieurs interviennent mais sur 10 mois au lieu de 12, une fois les spécifications rédigées. Le changement de ressources lié au déstaffing et au restaffing des intervenants extérieurs induit une montée en compétence systématique et coûteuse dont le budget peut être estimé au alentour de 5%
  • Pour un même budget annuel, si les intervenants extérieurs travaillaient 12 mois de l’année au lieu de 10, le nombre d’intervenants diminueraient de 15% environ. Cela induirait une diminution des frais d’infrastructure et d’encadrement en proportion.
  • Les coûts d’infrastructure sont estimés à 200 € par personne et par mois pour l’estimation de l’économie engendrée par une baisse du nombre moyen d’intervenants extérieurs sur l’année.

V – Axes de réflexion sur le ROI

1)  Concernant la taille des équipes

L’agilité étant optimale sur des projets de taille moyenne (entre 5 et 15 personnes), un axe d’optimisation du ROI consisterait à :

  • Diminuer la taille des gros projets en créant des équipes indépendantes de taille plus raisonnable;
  • Mutualiser les petits projets en créant des équipes multi projets par technologie ou par métier.
  • La réorganisation des projets via la réduction de taille des gros projets et la mutualisation des petits projets conduirait à une meilleure utilisation des pratiques agiles et donc à une meilleure productivité et qualité sur l’ensemble des projets. Le nombre global de projets resterait quasiment inchangé.

2) Concernant la nature de la maintenance

Sur les activités de correction, un axe d’optimisation du ROI consisterait à :

  • Augmenter la qualité des produits en maintenance corrective en corrigeant prioritairement les anomalies jusqu’à réduire à 10% la charge nécessaire sur le long terme à la correction des anomalies par rapport à la charge globale de production (en 2012). L’effet positif se fera sentir en 2013 avec une augmentation de la capacité de production et donc une diminution de la part de maintenance corrective de 30% à 10% et une augmentation de la part de maintenance évolutive de 70% à 90%.
  • Assurer la qualité de la maintenance évolutive permettant à la part consacrée aux corrections d’anomalie de ne jamais dépasser 10%.

Sur la base des hypothèses prises (30% de maintenance corrective à T0), on peut raisonnablement planifier une baisse de la maintenance corrective de 10% par an sur deux ans, à l’avantage de la maintenance évolutive. Cela se traduirait par une augmentation de la maintenance évolutive de 10% par an sur deux ans.

3) Concernant le succès des projets

Les statistiques démontrent que le taux de succès des projets agiles est au minimum de 15% par rapport aux projets traditionnels. Ce gain est obtenu par la diminution des coûts de non qualité et par l’augmentation de la productivité des équipes.

L’augmentation du taux de succès des projets d’environ 15% une fois les pratiques agiles déployées et maîtrisées se traduirait donc par une économie substantielle de budget. Ce budget peut être évalué par exemple en considérant un dépassement moyen en effort de 20% pour les projets en échec. Si les projets représentent 60% du budget, que les projets qui étaient en échec représentent 15% et que cet échec représente en moyenne 20% de dépassement, une économie de 1,8% serait possible (60% x 15% x 20%).

4) Amélioration de la satisfaction client

L’amélioration de la satisfaction client se traduira par la diminution du taux des fonctionnalités livrées mais jamais utilisées car remplacées par des fonctionnalités à plus haute valeur ajoutée. Cela survient grâce aux feedbacks réguliers et fréquents du métier lors des démonstrations de fin d’itération, mais également grâce à la capacité de l’équipe à être force de proposition car plus et mieux impliquée sur le produit. Il paraît donc raisonnable que le taux de non utilisation passe de 40% à 20% environ. Le budget récupéré correspondant devrait permettre soit d’améliorer la valeur du produit livré (15%), soit de le réinvestir sur de nouveaux projets (5%) et inciter le métier à confier plus de demandes au département informatique. Une économie de budget d’environ 5% peut probablement être atteinte.

5) Amélioration de la qualité logiciel et augmentation de la productivité des équipes

L’amélioration de la qualité des logiciels réalisés en agile se traduit par une baisse de la capacité de production consacrée à la correction des anomalies en cours de projet, renforçant encore l’augmentation de productivité des équipes agiles obtenues par la dynamique et le rythme projet.

Cette augmentation de productivité se traduit par une diminution des budgets nécessaires à la réalisation d’un certain périmètre de fonctionnalités ou par une augmentation de la taille logicielle livrée pour un certain budget.

Une augmentation de productivité de 10% à 20% par rapport à la productivité sur un projet traditionnel est courante et peut varier selon les cas. Elle est liée principalement à :

  • Une meilleure motivation et responsabilisation des équipes ;
  • Un outillage compatible des pratiques agiles qui vise à automatiser bon nombre d’activités (Intégration, vérifications, tests, déploiement…) ;
  • Des estimations et une planification plus précises et fréquentes ;
  • Un suivi plus rigoureux basé sur les fonctionnalités réalisées et livrées pas à pas ;
  • Une amélioration continue via les rétrospectives ;
  • Un meilleur travail en équipe et avec les différentes parties prenantes (principalement le« Product owner », et les « end users ») ;
  • Une qualité logicielle évitant de dépenser beaucoup d’effort à corriger des anomalies.

6) Réorganisation du travail du département informatique :

Le déstaffing des intervenants extérieurs deux mois de l’année pourrait être évité par la priorisation des projets et le lissage des activités de spécifications tout au long de l’année. Cela peut être réalisé avec la constitution de roadmap produit et de « product backlog » au fil de l’eau chaque fois que de nouveaux besoins émergent notamment à l’occasion des démonstrations et des livraisons de releases.

Il paraît raisonnable de considérer une économie de montée en compétence de 5% sur les 400/700 ième du budget, à condition de considérer que le coût des intervenants internes est identique à celui des intervenants externes (ce qui est forcément faux mais simplifie le calcul). Cela correspond à une économie de budget de 2,5%.

Il faut ajouter à cela, une réduction des coûts globaux d’infrastructure d’environ 15% (400p sur 10 mois = 4000 p.m => 4000 p.m./12 = 333 p => Ecart de 67p/400 = -16%).

Si on considère que les coûts d’infrastructure et d’encadrement représentent 200€ par mois et par personne, la réduction des coûts d’infrastructure d’environ 15% représenterait 15% de 200€ * 12 * 400p = 144 000 € / ans. Cette économie représenterait 0,7% du budget global qui est de 20 M d’Euros.

VI – Conclusion

Activités de maintenance :

L’impact de la diminution progressive de la part de maintenance corrective (-10% par an sur deux ans) liée à l’application des pratiques agiles se traduirait sur le budget constant du département informatique par :

  • Une diminution de la part de maintenance globale de 40% à 32% ;
  • Une augmentation du nombre de projets réalisés de 13,3%.

Activités projet :

L’impact de l’agilité sur les projets est positif et peut être quantifié dans le temps (entre 2012 et 2014) de la façon suivante :

  • Augmentation du taux de succès des projets de 15%;
  • Diminution du taux de fonctionnalités livrées inutilisées de 20%;
  • Augmentation de la productivité des projets de 10% ;
  • Diminution du nombre d’intervenants extérieurs nécessaires de 15%.

En termes d’économies budgétaire, le tableau suivant décrit entre 2012 et 2014 les économies correspondantes :

Economie globale pressentie :

Les économies pressenties globalement se répartissent de la façon suivante :

Indicateurs projet :

Ces économies se concrétiser par une augmentation du nombre de projets pouvant être réalisée de la façon suivante :

Economies et indicateurs estimés en mode dégradée :

Si le périmètre de la maintenance n’était pas revu à la baisse et si l’organisation du travail restait inchangée (10 mois travaillés pour les intervenants externes), les économies réalisables par le seul jeu de l’agilité sur le périmètre projet seraient décrites de la façon suivante :

Les économies pressenties globalement se répartiraient de la façon suivante :

L’augmentation du nombre de projets se réaliserait de la façon suivante :

Retour sur investissement (ROI) :

Si nous partons de l’hypothèse que le client investit 1 M Euros en 2012 et en 2013, puis 500 K Euros en 2014 pour passer intégralement sa structure à l’agilité, le calcul de ROI nous donnerait :

L’investissement serait donc rentabilisé au bout de la seconde année, c’est-à-dire fin 2013.

En mode dégradé, le calcul de ROI nous donnerait :

L’investissement serait donc rentabilisé au bout de la troisième année, c’est-à-dire en 2014.


by hubertgillonhubertgillon atJanuary 16, 2012 05:21 PM

January 15, 2012

2ème épisode du podcast Fréquence Valtech : iOS

Dans ce 2ème épisode, Sylvain Rousseau est interviewé par Grégory Paul sur iOS, le système d’exploitation mobile d’Apple. Lors de cette discussion, nous évoquons le système iOS, Objective-C, quelques nouveautés d’iOS 5, les contraintes du développement mobile, les tests unitaires, l’intégration continue ainsi que le déploiement des applications. Votre navigateur ne gère pas les balises [...]

by Grégory PaulGrégory Paul atJanuary 15, 2012 04:18 PM

January 14, 2012

Capture d’écran 2011-12-30 à 23.29.32

Incanter logo

Quand on ne sait plus trop où trouver des formules magiques, il reste Harry Potter, Alohomora l’incantation qui ouvre les portes, les portes de la voie des statistiques.

Allez au boulot, on a du rangement à faire.

Cet article fait partie d’une suite qui commence avec cet article épisode 1.

Recharger les données

First things first, charger nos librairies et nos données et on reprend où on en était dans l’épisode 3.

user=> (use '(incanter core io datasets stats))
nil
user=> (def data (read-dataset
"/Users/cfalguiere/Workspaces/Diapason/report-data/20111217-ETALON_WSW_TPS2011.csv" :header true))

#'user/data

Il est grandement temps de faire une fonction qui charge les résultats. On peut être sorcier et fainéant.

user=> (defn read-results []
  (read-dataset "/Users/cfalguiere/Documents/2012-01/RD-Clojure/Workspace/incanter/resources/sample-results.csv" :header true))

#'user/read-results

N’oubliez pas les parenthèses, sinon au lieu du prince charmant vous auriez une jolie mangouste, jolie mais bon ça n’est pas ce que vous vouliez. En l’occurrence, vous ne voulez pas que data représente la fonction mais son résultat.

user=>(def data read-results)
#'user/data
user=> data
#<user$read_results user$read_results@6bd46c20>

Donc voilà, le tour exécuté correctement et quelques donnnées présentes.

user=>(def data (read-results))
#'user/data

Pour le moment j’expérimente interactivement, donc peu importe l’IDE. Lançons un éditeur de texte pour copier ce code et chargeons le dans le REPL.

(use '(incanter core io datasets stats))

(def result-file-name "/Users/cfalguiere/Documents/2012-01/RD-Clojure/Workspace/incanter/resources/sample-results.csv")

(defn read-results []
  (read-dataset result-file-name :header true))

(def data (read-results))

J’aimerais bien indiquer le répertoire courant pour éviter tout ces chemins mais à la différence de R on ne peut pas changer le répertoire courant, parce que la JVM ne permet pas de le faire. Mais au fait quel serait mon répertoire courant ? Une rapide recherche sur Google indique ça :

user=> (. (java.io.File. ".") getCanonicalPath)
"/Applications"

Un peu ésotérique. C’est plus clair avec un exemple plus évident comme la méthode toUpperCase de la classe String. Le . applique la méthode à l’objet.

user=>(. "aaa" toUpperCase)
"AAA"

Il ne reste plus qu’à charger le programme et data est disponibles pour d’autres tours.

user=>(load-file "/Users/cfalguiere/Documents/2012-01/RD-Clojure/workspace/incanter/tuto1.clj")
#'user/data
user=> (nrow data)
209

Agrégeons, agrégeons, ils en restera toujours quelque chose

En général, j’aime bien avoir une vue d’ensemble de mes données : temps max, temps moyen, taux d’erreur …

Qu’est ce que j’ai là dedans ?

Le dataset est une map qui contient un vecteur de labels et une liste de lignes de données. La fonction col-names retourne les labels des colonnes (c’est équivalent (:column-names data))

user=> (col-names data)
[:lb :t :lt :ts :s :rc :rm :by :na]
user=> (:column-names data)
[:lb :t :lt :ts :s :rc :rm :by :na]

Les labels JMeter ne sont pas des plus parlants pour des raisons de compacité du résultat.

Pour chaque relevé nous disposons des informations suivantes :

  • :t le temps de réponse,
  • :lt le ‘latency time’ c’est à dire le temps qui s’est écoulé jusqu’à la réception du premier octet de la réponse,
  • :by le nombre d’octets de la réponse,
  • :na : le nombre d’utilisateurs simulés au moment de cette requête,
  • :rc le code retour de la requête HTTP, habituellement 200 ou 302,
  • :s le résultat des assertions sur les contenus reçus (true ou false),
  • :rm : un message,
  • :lb le label soit le nom donné au sample dans le script JMeter soit une url,
  • :ts  le timestamp du relevé.

Les 4 premières lignes sont des séries numériques. Les autres sont des catégories (l’équivalent des factors pour R). Elles vont servir à filtrer (pour les statuts) ou à regrouper les données.

Le module stats fourni les statistiques habituelles. Pour faire quelques expériementations, on va générer une série de nombres dont la moyenne est connue. sample-normal construit une liste de 1000 nombres distribués selon une loi normale centrée sur 10 avec un écart-type de 1. Sans surprise, la moyenne est autour de 10 et l’écart type est autour de 1.

user=> (def mysample (sample-normal 1000 :mean 10 :std 1))
(10.099828740537856 9.903470850168006 9.52228847634955 9.003518490739022 9.171579274811812 ...
user=> (mean mysample)
9.941436842660105
user=> (sd mysample)
1.0007997747452984

Et min et max ?

présentation des 4 quartiles

Un magicien ne voit pas les choses aussi simplement. Le min est la valeur qui est inférieure à tous les autres relevés. Et symétriquement le max est la valeur qui est supérieure à tous les autres relevés.

Mais les magiciens aiment bien découper les données en tranches. Donc ils veulent aussi savoir quelle est la valeur qui sépare les relevés en deux groupes, inférieurs et supérieurs à une valeur qui s’appelle la médiane.

Pour améliorer le spectacle, les données sont découpées en quatre tranches par pas de 25% comme le montre le schéma de droite, ce sont des quartiles. On voit sur le schéma que la surface rouge qui représente les 25% les plus bas se termine vers 9, la surface bleu qui représente les 25% suivants se termine vers 10 etc.

Les valeurs de 25% sont arbitraires. La forme générale de cet agrégat est le quantile. La fonction quantile retourne le min, la valeur maximale pour 25% des relevés, 50% (la médiane) et 75%, et le max (le quantile 100%).

user=> (quantile mysample)
(6.078738192126153 9.284660278736457 9.9269193454763 10.609659921191085 12.899443382831647)

Le monde étant ce qu’il est les sorciers qui font du test de charge ne s’intéressent pas aux quantiles des magiciens. En fait la seule chose qui les intéressent, c’est l’impact des 5% ou 10% de temps les plus longs. Donc ils calculent la valeurs maximale pour 90% ou 95% des relevés.

user=> (quantile mysample :probs 0.95)
11.591767882673627
user=> (quantile mysample :probs [0.9 0.95])
(11.278966306153395 11.591767882673627)

Qu’est ce que ça donne sur nos données de test ?

user=> (mean ($ :t data))
1429.579831932773
user=> (sd ($ :t data))
2237.25502018713

On peut faire toutes ces opérations sur la même série de données en utilisant la fonction associée au dataset with-data. Elle  factorise le dataset et permet de définir l’expression à utiliser, ici construire un tableau contenant les différentes métriques. $data représente la série à l’intérieur du with-data.

user=> (with-data ($ :t data)
  [(mean $data)(sd $data)])

[1429.579831932773 2237.25502018713]

Si on rajoute le quantile ça nous donne ce qui suit. Un bon début, mais on a un tableau des différents indicateurs plus une liste avec les quantiles.

user=> (with-data ($ :t data)
  [(count $data)(mean $data)(sd $data)(quantile $data :probs[0 0.5 0.9 0.95 1])])

[119 1429.579831932773 2237.25502018713 (10.0 708.0 3393.4 4130.999999999982 13007.0)]

Mmm … un coup de baguette magique flatten et les données se retrouvent bien alignées dans le tableau.

user=> (with-data ($ :t data)
  (flatten [(count $data)(mean $data)(sd $data)(quantile $data :probs[0 0.5 0.9 0.95 1])]) )

(119 1429.579831932773 2237.25502018713 10.0 708.0 3393.4 4130.999999999982 13007.0)

Une variable pour stocker le résultat et on sauve

user=> (def stats
  (with-data ($ :t data)
    (flatten [(count $data)(mean $data)(sd $data)(quantile $data :probs[0 0.5 0.9 0.95 1])]) ) )

#'user/stats
user=> stats
(119 1429.579831932773 2237.25502018713 10.0 708.0 3393.4 4130.999999999982 13007.0)

Quoique, pas tout de suite apparemment.

user=> (save 'stats "stats.csv")
java.lang.IllegalArgumentException: No method in multimethod 'save' for dispatch value: class clojure.lang.Symbol (NO_SOURCE_FILE:0)

En quête d’une multiméthode …

Que peut bien être une multiméthode ?

Vous trouverez une réponse complète dans cet article sur le polymorphisme en Clojure. En quelques mots, c’est le mécanisme qui permet à une fonction de se comporter différement en fonction des paremètres qu’on lui passe (des nombres ou des chaînes par exemple).

Vous avez probablement déjà noté que les fonctions en clojure sont souvent définies pour plusieurs nombres d’arguments (l’arité de la fonction si vous voulez briller la nuit au prochain ParisJUG). Clojure permet qu’une fonction soit surchargée (overloaded en anglais) et elle peut ainsi avoir une définition différente selon le nombre d’arguments.

Ci-après un exemple très simple de surcharge qui crée une map rectangle avec 1 ou 2 paramètres. La forme a un seul paramètre utilise la forme a 2 paramètres pour construire un carré.

user=> (defn make-rectangle
  ([width] (make-rectangle width width))
  ([width height] {:width width :height height}))

#'user/make-rectangle
user=> (make-rectangle 20)
{:width 20, :height 20}
user=> (make-rectangle 10 20)
{:width 10, :height 20}

Autre exemple plus compliqué, la multiplication implémentée de manière récursive en utilisant 4 formes :

(def mult
  (fn this
    ([] 1)
    ([x] x)
    ([x y] (* x y))
    ([x y & more]
      (apply this (this x y) more))))

La fonction est définie pour 0, 1, 2 arguments et un nombre quelconque d’arguments. Le & indique que ce qui se trouve à droite contient une liste d’arguments, et c’est par ce moyen qu’on définie une fonction variadique (pour quand vous ne brillerez plus assez avec arité).

Décryptons un peu cette multiplication. Tout d’abord, cet exemple n’utilise pas la macro defn, on a donc deux les étapes, décrire la fonction (fn) et la nommer (def). La multiplication utilise 4 variantes. Dans le cas général (4) je prend les 2 paramètres les plus à gauche, je leur applique “moi-même” (this x y) et j’applique ensuite “moi-même” en utilisant ce résultat et tous les paramètres qui restent à droite (more). Si je n’ai que 2 paramètres (3), je les multiple. Si je n’en ai plus qu’un (2), je le renvoie. Si je n’en ai plus (1) je renvoie 1.

Et pourquoi apply ? apply est utilisé dans les contextes où le nombre d’élements n’est pas connu à la compilation et donc typiquement lorsque l’on déconstruit une séquence. Par exemple

user=> (+ 1 2 3 4 5)
15
user=> (+ [1 2 3 4 5])
java.lang.ClassCastException (NO_SOURCE_FILE:0)
user=> (apply + [1 2 3 4 5])
15

L’addition du contenu de la séquence n’est pas possible directement. L’utilisation d’apply permet de déconstruire le tableau pour en lister les éléments et d’effectuer l’opération. Dans le cas de la fonction mult, apply permet de traiter more qui est une séquence.

Et ma multiméthode ?

La multiméthode est un mécanisme plus puissant qui permet de gérer le polymorphisme. La syntaxe est un peu différente et basée sur les verbes defmulti et defmethod.

L’exemple suivant montre une multiméthode “rencontre” entre différentes espèces. Si le paramètre 1 est un lapin et le paramètre 2 un lion, le comportement de “rencontre” est équivalent à la fonction “s’enfuit”. Dans le cas inverse, elle est équivalente à “mange”

(defmulti encounter (fn [x y] [(:Species x) (:Species y)]))
(defmethod encounter [:Bunny :Lion] [b l] :run-away)
(defmethod encounter [:Lion :Bunny] [l b] :eat)

L’exemple complet se trouve dans cet article sur le polymorphisme en Clojure.

Les multimethodes peuvent dispatcher en fonction des types des paramètres mais également sur des valeurs des arguments, leur nombre ou des méta-données selon la syntaxe.

Bref,  “No method in multimethod 'save' for dispatch value: class clojure.lang.Symbol (NO_SOURCE_FILE:0)" signifie seulement “je n’ai pas d’implémentation de save pour le type que tu m’envoie”. Oui, je sais …, mais les magiciens ne vont pas dévoiler tous leurs tours si facilement.

Enfin sauvé

stats est une liste et  save est une fonction Incanter qui fonctionnera que sur une matrice ou un dataset.
Qu’à celà ne tienne, on va construire un dataset avec ces données. Il faut lui indiquer les noms de colonnes et les valeurs.

user=> stats
(119 1429.579831932773 2237.25502018713 10.0 708.0 3393.4
4130.999999999982 13007.0)
user=> (def statsds
 (dataset ["count", "mean", "sd", "min", "median", "q90", "q95", "max"] stats) )

#'user/statsds
user=> statsds
#:incanter.core.Dataset{:column-names ["count" "mean" "sd" "min" "median" "q90" "q95" "max"],
:rows ({"count" 119} {"count" 1429.579831932773} {"count" 2237.25502018713} {"count" 10.0} {"count" 708.0} {"count" 3393.4} {"count" 4130.99999
9999982} {"count" 13007.0})}
user=> (view statsds)

Mmm, est ce bien ce que l’on veut ? Non, en fait le dataset n’est pas dans le bon sens. Il devrait avoir 1 ligne avec une colonne par indicateur.

Si vous allez sur la fonction dataset vous pouvez voir le source sous github. La fonction dataset attend une séquence de séquences ou une séquence de maps. Avec une simple liste, dataset a considéré que l’on voulait une liste de plusieurs lignes de 1 colonne.

La façon la plus simple de résoudre le problème est de transformer la liste en dataset en la transposant pour qu’elle constitue 1 ligne de plusieurs colonnes. Ensuite on lui ajoute des noms de colonnes.

user=> (def statsds (col-names
  (to-dataset stats :transpose true) ["count", "mean", "sd", "min", "median", "q90", "q95", "max"]) )

#'user/statsds
user=> statsds
#:incanter.core.Dataset{:column-names ["count" "mean" "sd" "min" "median" "q90" "q95" "max"],
:rows ({"max" 13007.0, "q95" 4130.999999999982, "q90" 3393.4, "median" 708.0, "min" 10.0, "sd" 2237.25502018713, "mean" 1429.579831932773, "count" 119})}
user=> (view statsds)
Le dataset est bien dans le bon sens et on peut le sauver
user=> (save statsds "/Users/cfalguiere/Documents/stats.csv")
nil

Il manque juste une dernière info utile le taux d’erreurs et le top n.
Pour le taux d”erreurs, on verra plus tard (surtout qu’il n’y en a pas dans le jeu de données) mais pour le top 5 des pires temps voici l’incantation trier-les-lignes-renverser-l-ordre-et-prendre-les-5-premiers

user=> (take 5 (reverse (sort-by :t (:rows data))))
({:na 1, :by 172777, :rm "OK", :rc 200, :s "true", :ts 1.324109405557E12, :lt 1398, :t 13007, :lb "/CategoryDisplay"}
{:na 1, :by 33235, :rm "OK", :rc 200, :s "true", :ts 1.324109282891E12, :lt 8967, :t 8981, :lb "/--product--.html"} ...

Et voilà d’autres tours de sorciers inscrits dans le grimoire. Dans l’épisode 5 on regroupera ces données pour avoir des résultats plus détaillés, par label par exemple et on fera des graphes de ces données.


by cfalguierecfalguiere atJanuary 14, 2012 08:01 PM

January 13, 2012

Une journée de séminaire à Toulouse sur le Web et la Mobilité

Valtech organise 1 séminaire gratuit sur le HTML5, CSS3, Windows Phone le 7 février 2012 à Toulouse. Inscription via le site web   Que vous soyez décideur, chef de projets, architecte logiciel ou développeur, cette conférence permettra d’aborder les thématiques suivantes : HTML5 est-il la solution pour des développements mobiles multiplateformes? Windows Mango, Windows 8 : [...]

by Olivier RodriguesOlivier Rodrigues atJanuary 13, 2012 11:28 AM

January 09, 2012

Eye tracking pour Windows 8

La société suédoise Tobii Technology vient de mettre au point leur technologie eye tracking (Gaze Interface), pour l’interface Metro de Windows 8. Le eye tracking couplé au touchpad des notebooks offre des perspectives très séduisantes pour rendre encore plus naturelle l’expérience utilisateur. Cette nouveauté sera présenté au  CES à Las Vegas cette semaine.

 

by IonutIonut atJanuary 09, 2012 06:07 PM

January 08, 2012

cfalguiere

Voici les slides de la présentation que j’ai faite aux JUGs de Toulouse et Bordeaux le 7 et 8 décembre.

 

Je voudrais remercier ces JUGs ainsi que les deux genigraph.fr qui a publié un compte rendu sur son blog. Le JUG de Toulouse a également fait un compte-rendu de la soirée.

Une session similaire a été enregistrée par le JUG de Lausanne.


by cfalguierecfalguiere atJanuary 08, 2012 04:40 PM

January 07, 2012

January 06, 2012

Meilleurs voeux pour 2012

  A travers ce message interactif, toute l’équipe de Valtech vous souhaite une Bonne Année 2012 !!! Elaborée en HTML5, cette e-card est compatible sur Tablettes & Smartphones, même en mode déconnectée. N’oubliez pas de mettre à jour votre navigateur web .

by Olivier RodriguesOlivier Rodrigues atJanuary 06, 2012 10:22 AM

January 05, 2012

Assurer la fiabilité des échanges avec MSMQ et WCF

Ca faisait longtemps que je n’avais plus eu recours à cette technologie. La semaine dernière un client me pose un problème intéressant : “Comment dans une architecture distribuée je peux garantir qu’un message envoyé par un client sera délivré au serveur même si celui-ci est inaccessible momentanément du à des problèmes réseau ou autre ? “

Et la les vieux souvenirs remontent et MSMQ couplé à WCF parait la solution la plus appropriée. Et me voila lancé dans le POC que je vous présente dans cet article.

Mais avant un petit rappel sur technologie MSMQ.

 

Microsoft Message Queuing

C’est le nom (pour le coup pas très original) de la technologie de Message Queuing présente chez Microsoft sur les serveurs depuis Windows NT. Cette technologie a été généralisé aussi sur les plateformes workstation (Windows XP, Windows 7) et aussi sur Windows embeded.

Le but de MSMQ est d’assurer la fiabilité des échanges entre des applications qui s’exécutent à  des moments différents et/ou en utilisant des environnement réseau hétérogènes.

En gros j’ai une application A qui doit envoyer des données à une application X. En utilisant MSMQ  si l’application X n’est pas accessible au moment ou l’application A envoie les données, les données en question seront stockées dans la file d’attente locale de la machine qui exécute l’application A. Lorsque la file d’attente distante sera accessible les message lui seront remis depuis la file d’attente locale.MSMQ file distante indisponible

 

MSMQ_Distribution_Success

Pour pouvoir mettre en œuvre ce type de scénario il faut que MSMQ soit actif sur les machines exécutant les applications.

MSMQ peut être utilisé pour des scenarios synchrones et asynchrones pour des applications critiques comme :

  • - Applications e-Commerce : prise en compte des commandes client,
  • - Applications de logistique : triage automatisé (ex. tri des bagages à l’aéroport),
  • - Pattern d’architecture : casser le couplage entre différentes applications.
  • etc…

 

Le POC

Dans cet exemple nous allons démontrer ce scénario dans une architecture client/serveur en utilisant WCF et MSMQ. Cet exemple à été réalisé et testé avec Windows 7 x64, Visual Studio 2010.

Le code est disponible sur github https://github.com/imihalcea/MSMQ-WCF-Post.

Le starter (la branche master)

Pour démarrer j’ai préparé une solution Visual Studio avec un client et un serveur qui communiquent en netTcp. La structure est la suivante :

- Orders.Client – client WPF qui permet la saisie d’une commande et l’envoie au serveur.La classe “OrderFacade” se charge d’assurer l’envoi d’une commande via le proxy généré coté client.

   1:      public class OrdersFacade
   2:      {
   3:          public void SendOrder(Order o)
   4:          {
   5:              using (var client = new OrdersServiceClient())
   6:              {
   7:                  client.Open();
   8:                  client.PrepareOrder(o);
   9:                  client.Close();
  10:              }
  11:          }
  12:      }

La configuration du service coté client dans App.config est la suivante :

   1:    <system.serviceModel>
   2:      <bindings>
   3:        <netNamedPipeBinding>
   4:          <binding name="NetNamedPipeBinding_IOrdersService" />
   5:        </netNamedPipeBinding>
   6:      </bindings>
   7:      <client>
   8:        <endpoint address="net.pipe://[server host name or ip]/" binding="netNamedPipeBinding"
   9:            bindingConfiguration="NetNamedPipeBinding_IOrdersService"
  10:            contract="IOrdersService" name="NetNamedPipeBinding_IOrdersService">
  11:          <identity>
  12:            <userPrincipalName value="Ionut-PC\Ionut" />
  13:          </identity>
  14:        </endpoint>
  15:      </client>
  16:    </system.serviceModel>

- Orders.Common – librairie définissant le contrat de données, la classe Order, qui est échangée entre le client et le serveur. Cette librairie est référencée par  “Orders.Service” et “Orders.Client”. Le proxy du service utilisé coté client utilise la classe Order définie dans ce projet.

   1:      public class Order
   2:      {
   3:          public string ProductName { get; set; }
   4:          public int Quantity { get; set; }
   5:      }

- Orders.Server – application console hôte du service.

   1:  static void Main(string[] args){
   2:              _resetEvent = new AutoResetEvent(false);
   3:              
   4:              host = new ServiceHost(typeof(Orders.Services.OrdersService));
   5:              host.Open();
   6:  ....

La configuration du service est la suivante :

   1:  <system.serviceModel>
   2:      <services>
   3:        <service name="Orders.Services.OrdersService" behaviorConfiguration="serviceBehavior">
   4:          <endpoint contract="Orders.Services.IOrdersService" binding="netNamedPipeBinding" />
   5:          <endpoint address="mex" contract="IMetadataExchange" binding="mexHttpBinding" />
   6:          <host>
   7:            <baseAddresses>
   8:              <add baseAddress="net.pipe://[server host name or ip]"/>
   9:              <add baseAddress="http://[server host name or ip]:8000"/>
  10:            </baseAddresses>
  11:          </host>
  12:        </service>
  13:      </services>
  14:      <behaviors>
  15:        <serviceBehaviors>
  16:          <behavior name="serviceBehavior">
  17:            <serviceMetadata/>
  18:          </behavior>
  19:        </serviceBehaviors>
  20:      </behaviors>
  21:    </system.serviceModel>

- Orders.Services – librairie définissant et implémentant  le contrat de service “OrdersService”.

   1:      [ServiceContract]
   2:      public interface IOrdersService
   3:      {
   4:          [OperationContract]
   5:          void PrepareOrder(Order order);
   6:      }
   7:   
   8:      public class OrdersService:IOrdersService
   9:      {
  10:   
  11:          public void PrepareOrder(Order order)
  12:          {
  13:              Console.WriteLine("Prepare order : {0}, Qty:{1}", order.ProductName,order.Quantity);
  14:          }
  15:      }

 

Le problème

Si le serveur n’est pas disponible au moment ou le client va exécuter la méthode “SendOrder” de la classe “OrderFacade” j’aurai une jolie “EndPointNotFoundException” et le client ne pourra pas envoyer le message au serveur. J’aurai un problème d’indisponibilité du service.

La solution (la branche msmq_support)

Vous l’aurez compris il faut qu’on fasse rentrer MSMQ dans la boucle, pour résoudre notre problème.

Etape 1: Au niveau de Windows installez MSMQ (sur la machine cliente et serveur).

image

Après l’activation de MSMQ vous disposez d’une console de management et monitoring des files d’attente.

image

 

Etape 2: Ajouter le support de MSMQ ou niveau du serveur.

a) MSMQ est supporté uniquement pour les contrat de service à sens unique. Il faut donc ajouter le paramètre IsOneWay=true.

   1:  [ServiceContract]
   2:      public interface IOrdersService
   3:      {
   4:          [OperationContract(IsOneWay = true)]
   5:          void PrepareOrder(Order order);
   6:      }
   7:   
   8:      public class OrdersService:IOrdersService
   9:      {
  10:   
  11:          public void PrepareOrder(Order order)
  12:          {
  13:              Console.WriteLine("Prepare order : {0}, Qty:{1}", order.ProductName,order.Quantity);
  14:          }
  15:      }

b) Au niveau de l’application console “OrdersServer” qui héberge le service il faut ajouter la référence vers l’assembly “System.Messaging”. Cette assembly du framework .NET fournit une API permettant le travail avec des files d’attente. Au démarrage du serveur il faut rajouter le code qui va créer automatiquement la file d’attente distante si elle n’existe pas :

   1:  public const string NomFileAttente = @".\private$\OrdersQueue";
   2:   
   3:  private static void CreateQueueIfNotExist()
   4:   {
   5:          if (!MessageQueue.Exists(NomFileAttente))
   6:          {
   7:             MessageQueue.Create(NomFileAttente, true);
   8:           }
   9:   }
.csharpcode, .csharpcode pre { font-size:small;color:black;font-family:consolas, courier, monospace;background-color:#ffffff;} .csharpcode pre {margin:0em;} .csharpcode .rem {color:#008000;} .csharpcode .kwrd {color:#0000ff;} .csharpcode .str {color:#006080;} .csharpcode .op {color:#0000c0;} .csharpcode .preproc {color:#cc6633;} .csharpcode .asp {background-color:#ffff00;} .csharpcode .html {color:#800000;} .csharpcode .attr {color:#ff0000;} .csharpcode .alt { background-color:#f4f4f4;width:100%;margin:0em;} .csharpcode .lnum {color:#606060;}

c) Modifier le binding du service dans le projet “OrdersServer”.

   1:  <configuration>
   2:    <system.serviceModel>
   3:      <services>
   4:        <service name="Orders.Services.OrdersService" behaviorConfiguration="serviceBehavior">
   5:          <endpoint contract="Orders.Services.IOrdersService" binding="netNamedPipeBinding" />
   6:          <endpoint address="mex" contract="IMetadataExchange" binding="mexHttpBinding" />
   7:          <endpoint address="net.msmq://[server host name or ip]/private/OrdersQueue" contract="Orders.Services.IOrdersService" binding="netMsmqBinding" bindingConfiguration="netMsmq"/>
   8:          <host>
   9:            <baseAddresses>
  10:              <add baseAddress="net.pipe://[server host name or ip]"/>
  11:              <add baseAddress="http://[server host name or ip]:8000"/>
  12:            </baseAddresses>
  13:          </host>
  14:        </service>
  15:      </services>
  16:      <bindings>
  17:        <netMsmqBinding>
  18:          <binding name="netMsmq">
  19:            <security mode="None"/>
  20:          </binding>
  21:        </netMsmqBinding>
  22:      </bindings>
  23:      <behaviors>
  24:        <serviceBehaviors>
  25:          <behavior name="serviceBehavior">
  26:            <serviceMetadata/>
  27:          </behavior>
  28:        </serviceBehaviors>
  29:      </behaviors>
  30:    </system.serviceModel>
  31:  </configuration>

d) Compiler la solution et lancez l’application console “OrdersServer”. Attention il faut l’exécuter en mode administrateur.

e) Régénérez le proxy  coté client.

Etape 3 : Lancez le service et le client testez la solution.

a) Vous pouvez arrêter l’application console “OrdersServer”, lui envoyer des messages depuis le client puis avec la console de management regardez comment les messages sont stockes dans la file d’attente distante.

b) Vous pouvez arrêter physiquement  le serveur ou le débrancher du réseau, puis luis envoyer des messages depuis le client. Sur la machine client vous pouvez constater (avec l’aide de la console de management) que les messages sont stockés dans la file d’attente locale en attendant que la file d’attente distante soit disponible.

image 

 

c) Rebranchez le serveur et vous allez constater que les messages vont arriver dans la file distante dans un délais de 2 à 3 min.

Conclusions

Grâce à la puissance de WCF, .NET, MSMQ nous arrivons a obtenir une solution client serveur fiable avec un minimum d’effort. Je vous fais remarquer qu’une fois le support de MSMQ activé, nous avons ajouté 3 lignes de code et modifié un fichier de config.

Ressources

1) Queues in WCF: http://msdn.microsoft.com/en-us/library/ms731089.aspx

2) Introduction à WCF : http://www.amazon.fr/dp/284177449X

3) Programming WCF Services : http://www.amazon.fr/dp/B0068GHGTI

by IonutIonut atJanuary 05, 2012 03:19 PM

Feuilles de Dialogue : Un nouvel outil pour les rétrospectives


Traduction de l'article "Dialogue Sheets : a new tool for retrospectives" d'Allan Kelly parru le 3 janv sur  InfoQ:


Publié avec l'autorisation de copie/traduction d'InfoQ.


_________________________________________________________________
Feuilles de Dialogue : Un nouvel outil pour les rétrospectives


Une Feuille de Dialogue est une feuille de papier,huit fois plus grande qu'un format A4 ou format Lettre. L’année dernière deséquipes agiles ou traditionnelles ont pris à l’initiative d’utiliser ces fichespour les rétrospectives. Les équipes qui utilisent les feuilles déclarentsystématiquement un intérêt renouvelé pour les rétrospectives, des niveaux plusélevés d'énergie pendant la session et un style plus inclusif engageant plus demembres de l'équipe.

Les rétrospectives avec Feuilles de Dialoguesdivergent de plusieurs façons des rétrospectives traditionnelles : Ellessont sans facilitateur. Les instructions sur la feuille entrainent l’équipe àréfléchir sur le passé et à en venir à leurs propres conclusions. Sansfacilitateur, l’équipe doit réellement s’auto-organiser.


Mécanique de la Feuille



La feuille au format ISO A1 ou Poster US estpré-imprimée avec les instructions, citations et graphiques conçus pour piloterl’équipe et stimuler la conversation. Certaines des feuilles de rétrospectivesprésentent une ligne de temps que l’équipe doit remplir ; Sinon le centrede la feuille est disponible pour l’équipe. Les membres de l’équipe sont libresd’écrire des commentaires n’importe où sur la feuille et quelques équipes ontdemandé d’avoir encore plus d’espace pour la prise de notes.
Entre 8 et 15 questions et orientations sont répartiesautour de la feuille. Les deux ou trois premières instructions décriventcomment l’équipe peut travailler avec la feuille. Les questions suivantes sontconçues pour aider l’équipe à réfléchir. Enfin, en marge de la feuille,plusieurs citations sélectionnées stimulent la réflexion et rappellent auxmembres de l’équipe les idées Agiles ou de développement logiciel en général.
Les membres de l’équipe se positionnent eux mêmeautour de la feuille, lisent les questions à tour de rôle, facilitent la discussionet collectent les réponses. Chaque membre de l’équipe est responsable à tour derôle d’une question au moins. Cela assure une voix à chacun et rend plusdifficile la possibilité à une personne de dominer la discussion.
En règle générale, le nombre maximum de participantsest huit, avec un minimum de deux. Au-delà, l’équipe est invitée à se diviseren groupes plus petits, chaque groupe remplissant sa feuille, après quoi leséquipes comparent et confrontent leurs conclusions.
Le temps pour remplir une feuille dépend du nombre depersonnes dans le groupe. Typiquement, une heure est suffisante, mais uneéquipe de huit, ou une équipe ayant beaucoup de choses à dire prendra plus detemps.
Des versions électroniques des feuilles sontdisponibles gratuitement au téléchargement. La nécessité d’imprimer en grandformat ces feuilles peut s’avérer compliquée. Quelques équipes n’ayant pasd’imprimante grand format les ont imprimées en A4 ou A3 puis recolléesensemble, bien que cela prenne du temps. L’impression par un imprimeur n’estsans doute pas coûteuse, mais peut l’être. Le site fourni des liens vers desservices d’impression en ligne, mais vous pouvez trouver cela moins cher de lefaire chez un imprimeur local, évitant les frais de port.


Résultats


Les équipes utilisant les Feuilles de Dialogue pourles rétrospectives rapportent constamment que les gens les trouvent amusanteset que le niveau d’énergie est plus haut. En partie, cela provient simplementdu fait de la nouveauté mais aussi à la nature réellement collaborative desfeuilles. Non seulement tout le monde peut parler mais chacun devientmodérateur pendant quelques minutes.
Il est évident que la feuille donne une voix à chacun.Il est étonnant de constater aussi que les feuilles peuvent donner une voix àun groupe minoritaire au sein d’une plus grande équipe.
Une fois, une équipe a dû se séparer en deux pourremplir la feuille. Comme par hasard, un groupe s’est constitué principalementde développeurs et l’autre d’analystes métier. Après quoi, les analystesmétier ont dit avoir pu discuter de ce qu’ils voulaient, dans les termes qu’ilsvoulaient. En comparaison, du fait du nombre plus élevé de développeurs qued’AF, l’accent, lors des rétrospectives régulières, portait sur les problèmesau niveau du code.
Pour les équipes pratiquant des rétrospectives depuislongtemps, les feuilles offrent une nouvelle façon d’aborder les sujets familiers.Ils ne résolvent pas tous les problèmes, un esprit d’ouverture et de confiancereste toujours nécessaire, et même, plus de confiance peut être nécessaire audébut.
Jusqu’ici toutes les feuilles de rétrospectives respectentla Première Directive pour rétrospectives de Kerth.[1] Il peut être surprenant devoir comment une équipe agonise devant quelque chose dont la plupart desfacilitateurs prennent non seulement comme du bon sens mais aussi quelque chosede sacro-saint. En effet, il a été rapporté qu’une équipe s’est amusée de ladirective.
Plusieurs équipes rapportent qu’elless ont propagé lesfeuilles dans leur organisation, autant pour les équipes agiles que les équipesde développement plus traditionnel. Une fois, c’est le responsable des testsqui a présenté des feuilles remplies aux autres équipes pour les encourager àles essayer.


Sans facilitateur

La suppression du facilitateur lors d’unerétrospective avec Feuille de Dialogue change la dynamique de façon trèsintéressante, et fructifiante. Supprimer le facilitateur force les équipes à s’auto-organiserdans ce cadre. Ce qui en soit en bon, et qui enlève aussi la possibilité aufacilitateur d’orienter la rétrospective en son sens ou d’empêcher ladiscussion sur certains sujets.
Plusieurs groupes rapportent que les discussions sonten fait plus accès sur le travail en cours de discussions. Plutôt que lefaciliter soit au centre de l’action, c’est la feuille qui l’est. Plutôt que lefacilitateur tienne le stylo pour saisir les résultats, c’est l’équipe qui letien. Tout le monde est responsable de maintenir la conversation dans le sujet.
Je n’ai pas eu jusqu’à présent de retour derétrospective à feuille de dialogue en échec du fait du manque de facilitateur.Par contre, j’ai eu plusieurs retours de personnes, ayant vu les feuilles dedialogues, ne voulant pas utiliser ce type de rétrospective, considérant lefacilitateur comme essentiel.
Ce que je pense être une erreur, une opportunitémanquée. Dans l’esprit d’expérimentation Agile, je proposerais à tous d’essayerau moins une fois les rétrospectives sans facilitateur et de réfléchir auxbénéfices.
Bien sûr il y a des choses qu’un facilitateur peutfaire qu’une Feuille de Dialogue ne peut : Il peut diriger la recherchesur un sujet particulier, aider un individu à s’ouvrir, ajouter un exercicespécifique pour un problème spécifique ou plus. Je ne suggère pas lasuppression permanente du facilitateur. Au contraire, les Feuilles de Dialoguessont de nouveaux outils dans la boite à outil de l’équipe. Ils ne sont pas obligésde les utiliser à chaque rétrospective, ils peuvent mélanger les styles etexercices.
Il y a aussi de la place pour des styles hybrides.J’ai vu plusieurs équipes entreprendre des rétrospectives avec Feuilles deDialogue lors de mes recherches. Il n’y a pas de raison qu’un facilitateur nepuisse observer et si nécessaire intervenir. (Bien que cela véhicule un messageerroné sur l’auto-organisation).
D’une autre façon, la Feuille de Dialogue peut êtreutilisée en première partie d’une rétrospective pour les problèmes en surface.En deuxième partie, un facilitateur peut intervenir et aider l’équipe àdévelopper une ou plusieures conclusions particulières.


Le Futur

Il existe actuellement plusieurs Feuilles de Dialoguedifférentes disponibles pour les rétrospectives. L’intention était de fournirde la variété. J’explore aussi d’autres utilisations pour cette technologie.Par exemple, j’ai développé une feuille de démarrage de projet pour leséquipes. En essayant les feuilles, d’autres ont aussi créé les leurs.
Le style et les exercices des feuilles sontactuellement largement inspirés des exercices de rétrospectives standardes, telque ligne de temps, arrête de poisson, j’aime/j’aime pas, etc. Dans le temps,j’espère voir émerger de nouveaux exercices pour les Feuilles de Dialogues.
Actuellement, les feuilles ne sont disponibles qu’enAnglais. Cela n’a pas arrêté des équipes Argentines, Allemandes, Finlandaise etautres à utiliser ces feuilles. Plusieurs personnes se sont proposées pourtraduire ces feuilles et j’espère cela rapidement afin de pouvoir les ajouterau site.


La fin du commencement


L’année passée a vu de nombreuses équipes à travers lemonde s’essayer aux rétrospectives avec Feuilles de Dialogue et ont trouvé a)qu’elles fonctionnent, b) que les équipes les aimes. Elles ne serviront pas àtoutes les équipes mais peuvent être une option que chaque équipe peutregarder.
Les PDF des feuilles disponibles sont gratuitement accessibleici.


A propos de l'auteur


Allan Kelly a presque eu tous les emplois du monde dulogiciel, depuis administrateur système jusqu’à responsable du développement.Aujourd’hui il aide les équipes à adopter et approfondir les pratiques Lean etAgile. Il travaille spécialement pour des éditeurs de logiciels et aligne lesproduits et les processus à la stratégie de l’entreprise. Son nouveau livre« Business Patterns for Software Developper » est attendu pour début2012.

by Laurent Carbonnaux atJanuary 05, 2012 08:19 AM

January 04, 2012

Les Dialog Sheets de retrospectives

Dans la série rétrospectives:

Initiative très intéressante d'Allan Kelly pour les rétrospectives.
Une sorte de jeu de l'oie sauce agile.



L'objectif étant de se passer du facilitateur, ceci augmentant l'auto organisation de l'équipe selon l'auteur.
L'idée simple est de mettre en couleur et sur papier le mode de fonctionnement d'une rétrospective type de Scrum.
Plusieurs modèles existent, du plus simple au début au plus évolué après expérimentations par l'équipe.

Ça tombe bien, fin de sprint demain, j'essaye!

Le site Dialog Sheets : www.dialoguesheets.com
L'article InfoQ sur le sujet par l'auteur : http://www.infoq.com/articles/dialogue-sheets-retrospectives


PS : Au fait, bonne année 2012 à toutes et à tous! Bientôt la fin du monde il parait, rétrospectives en perspective ;-)


by Laurent Carbonnaux atJanuary 04, 2012 10:20 PM

December 28, 2011

Capture d’écran view Incanter

Incanter logo

Continuons notre voyage dans les sortilèges d’Incanter avec l’incantation de Mary Poppins,

supercalifragilisticexpialidocious

Le balai arrive. En route pour le monde où nos tableaux de nombres vont se ranger tout seuls !

Cet article fait partie d’une suite. L”épisode 1 présentait quelques bases de Clojure et Incanter.  L’épisode 2 présentait les types.

Où trouver des grimoires ?

Nous allons manipuler un peu plus d’API et quelques documents vont être bien utiles.

Clojure ainsi que la librairie présentent l’API complète ainsi que des cheat sheets pour regrouper les commandes les plus fréquentes.

To use or not to use ?

Les librairies sont organisées en modules. Pour utiliser une des formules magiques vous devez d’abord charger les librairies correspondantes.

user=> (use 'incanter.core)
nil
user=> (use 'incanter.io)
nil
user=> (use 'incanter.datasets)
nil

Un peu laborieux au bout d’un moment, non ?

use ou require sont des fonctions du package clojure.core qui chargent des librairies, c’est à dire un ensemble de ressources qui se trouvent dans un package Java. Un fichier Clojure à la racine de cette librairie sert de descripteur.

On a souvent à charger différentes librairies avec le même préfixe (par exemple incanter.core incanter.io …)
On peut réécrire ces lignes en plus compact en utilisant la forme suivante (prefix list).

user=> (use '(incanter core io datasets))
nil

Vous noterez que les librairies sont maintenant passées dans une liste. Le premier élément est le préfixe, les autres sont les librairies sans le prefixe. Attention, les noms passés dans la liste ne doivent pas contenir de . .

Et bien sûr une  ’ pour empêcher l’évaluation de la liste.

Convoquer les données

Pour commencer, il vous faut quelques tableaux de nombres.

Incanter propose un moyen rapide de créer un dataset à partir d’un fichier CSV. Si vous n’avez pas de données sous la main, Incanter est livré avec quelques jeux de données (http://liebke.github.com/incanter/datasets-api.html) que l’on peur charger en utilisant la fonction get-dataset.

Commençons avec un résultat de test JMeter transformé en CSV. Un tir étalon pour ne pas avoir trop de données pour le moment.

user=> (def data (read-dataset "/Users/cfalguiere/Workspaces/Diapason/report-data/20111217-ETALON_WSW_TPS2011.csv" :header true))
#'user/data

Petite vérification, avons nous bien des données ? capture d'écran du résultat de view

user=> (view data)
#<JFrame javax.swing.JFrame[frame1,0,22,400x600,invalid,layout=java.awt.BorderLayout,title=Incanter Dataset,resizable,normal,defaultCloseOperation=HIDE_ON_CLOSE,rootPane=javax.swing.JRootPane[,0,22,400x578,invalid,layout=javax.swing.JRootPane$RootLayout,alignmentX=0.0,alignmentY=0.0,border=,flags=16777673,maximumSize=,minimumSize=,preferredSize=],rootPaneCheckingEnabled=true]>
user=>

La fonction view permet de visualiser ce dataset.

Combien êtes vous ?

La fonction de dénombrement Clojure est count.

user=> (count [1 2 3])
3
user=> (count "Incanter")
8

Donc voilà

user=> (count data)
2

Mmmmm … 2 … ???

Regardons de plus près à quoi ressemble ce dataset.

user=> data
#:incanter.core.Dataset{
  :column-names [:lb :t :lt :ts :s :rc :rm :by :na],
  :rows (
    {:na 1, :by 27985, :rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"}
    {:na 1, :by 28383, :rm "OK", :rc 200, :s "false", :ts 1.324109266396E12, :lt 8511, :t 8523, :lb "/CategoryDisplay"}
    ;...
    {:na 1, :by 861, :rm "OK", :rc 200, :s "true", :ts 1.324109908066E12, :lt 21, :t 21, :lb "/AjaxLastViewedDisplay"}
  )}

Le dataset est une map composée de 2 éléments :column-names qui contient tous les labels de colonnes dans un vector et :rows qui est une liste comportant une map par ligne de données.

On obtient un résultat plus pertinent en comptant les entrées de la liste. Incanter fournit aussi une fonction nrow qui compte correctement les lignes d’un dataset (n’oubliez pas que le dataset n’est pas une structure de données Clojure)

user=> (count (:rows data))
119
user=> (nrow data)
119

Le count de l’élément :rows de data nous donne bien le même compte que le nrow du dataset.

Découper en tranches

Quelques incantations Clojure pour se faire la main. Affectons les lignes du dataset a une variable pour les manipuler plus facilement.

user=> (def datarows (:rows data))
#'user/datarows
La Clojure way

Donne moi les deux premières lignes !

user=> (take 2 datarows)
({:na 1, :by 27985, :rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"} {:na 1, :by 28383, :rm "OK", :rc 200, :s "false", :ts 1.324109266396E12, :lt 8511, :t 8523, :lb "/CategoryDisplay"})

Donne moi tous les labels !

On va utiliser la fonction map pour appliquer à chaque ligne une fonction qui renvoie le label.

user=> (map :lb datarows)
("/" "/CategoryDisplay" "/--product--.html" ...

Et tout à la fois !

user=> (take 2 (map :lb datarows))
("/" "/CategoryDisplay")
L’Incanter way

Incanter propose des fonctions de manipulation de dataset. La fonction utilisée pour extraire des données est sel plus souvent utiilisée via son alias $.

L’alias $ a plusieurs formes. Il peut être utilisé avec la colonne seule (identifié par sa position, son nom, ou une collection de ces indications), ou bien la ligne et la colonne. Le dernier paramètre est le dataset.

La fonction sel place le dataset en premier argument et spécifie l’axe par des keywords.

Donne moi tous les labels !

user=> ($ :lb data)
("/" "/CategoryDisplay" "/--product--.html" ...
user=> (sel data :cols 0)
("/" "/CategoryDisplay" "/--product--.html" ...

Un usage typique est celui-ci, extraire une série, ici le temps de réponse et calculer un agrégat tel que la moyenne :

user=> (mean ($ :t data) )
1429.579831932773

Vous avez testé et ça ne marche pas, jeune sorcier ? C’est normal, il faut le package incanter.stats qui n’a pas été chargé pour le moment.

Donne moi les deux premières lignes !

Pour la fonction sel le paramètre qui suit :rows indique soit un numéro particulier de ligne, soit une liste de numéros de lignes, soit un range qui va générer cette liste de numéros de lignes.

Lorqu’on utilise $ le premier paramètre représente la ligne (ou les lignes) et le second la colonne.

user=> (sel data :rows (range 2) )
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm :by :na], :rows ({:na 1, :by 27985, :rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"} {:na 1, :by 28383, :rm "OK", :rc 200, :s "false", :ts 1.324109266396E12, :lt 8511, :t 8523, :lb "/CategoryDisplay"})}
user=> (sel data :rows '(0 1))

Ce qui est équivalent à :

user=> ($ (range 2) :all data)
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm :by :na], :rows ({:na 1, :by 27985, :rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"} {:na 1, :by 28383, :rm "OK", :rc 200, :s "false", :ts 1.324109266396E12, :lt 8511, :t 8523, :lb "/CategoryDisplay"})}

Vous noterez le :all. La syntaxe oblige à spécifier deux paramètres si l’on veut indiquer des lignes, les lignes puis les colonnes. :all indique que toutes les colonnes doivent être conservées.

Donne moi les labels des cinq premières lignes !

user=> user=> ($ (range 5) :lb data)
("/" "/CategoryDisplay" "/--product--.html" ...
user=> (sel data :rows (range 5) :cols :lb)
("/" "/CategoryDisplay" "/--product--.html" ...

Toutes sortes de combinaisons sont possibles. L’expression suivante retourne un dataset ne comportant plus que les colonnes sélectionnées et les deux premières lignes.

user=> ($ (range 2) [:lb :t] data)
#:incanter.core.Dataset{:column-names [:lb :t], :rows ({:t 808, :lb "/"} {:t 8523, :lb "/CategoryDisplay"})}

S’il y a plusieurs colonnes, le résultat est un dataset, mais s’il n’y a qu’une ligne la structure de données est également différente. Le résultat est une liste et non un map. Les éléments sont ordonnés conformément aux colonnes du dataset.

user=> (sel data :rows 0)
("/" 808 794 1.324109256665E12 "false" 200 "OK" 27985 1)
user=> (:column-names data)
[:lb :t :lt :ts :s :rc :rm :by :na]

Pour finir, il existe un mot clé :not qui permet d’exprimer des listes par exclusion. Dans l’exemple suivant, l’expression conserve toutes les colonnes sauf :by et :na pour les deux premières lignes.

user=> ($ (range 2) [:not :by :na] data)
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm], :rows ({:rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"} {:rm "OK", :rc 200, :s "false", :ts 1.324109266396E12, :lt 8511, :t 8523, :lb "/CategoryDisplay"})}

Trouver l’élu

Attention jeune sorcier ça se complique.

Pour analyser les résultats on aura souvent besoin de retrouver des sous ensemble sur des critères particuliers, tous les relevés pour une url données (:lb), les relevés avec des temps très élevés (:t), ou les relevés en erreur (statut :s, return code :rc, message :rm).

La Clojure way

Les collections Clojure ont une fonction filter filter. La ligne suivante donne tous les nombres pairs du vecteur [1 2 3 4 5 6].

user=> (filter even? [1 2 3 4 5 6])
(2 4 6)

Dans le cas plus général on ne pourra pas utiliser une fonction existante et il faudra écrire sa propre fonction filtre.

user=> (defn filterAlice [name] (= name "Alice"))
#'user/filterAlice
user=> (filter filterAlice ["Alice", "Bob", "Charles"])
("Alice")

D’une manière générale on utilisera plutôt une fonction anonyme. Ce qui nous donne :

user=> (filter (fn[name](= name "Alice")) ["Alice", "Bob", "Charles"])
("Alice")

C’est un peu verbeux et vous trouverez plus souvent une forme abrégée utilisant #.

user=> (filter #(= % "Alice" ) ["Alice", "Bob", "Charles"])
("Alice")

# est appelé la macro dispatch et a plusieurs effets décrits ici. Celui qui nous intéresse ici est #(...) qui est un équivalent de (fn [args] (...)).

user=> ( #(+ 1 %) 2 )
3
user=> ( #(+ 1 %1 %2) 2 3 )
6

% représente  l’argument, ou les arguments, de la fonction anonyme, ici le paramètre :name implicite.

Cette fonction anonyme appliquée sur la liste de prénom donne bien le même résultat.

user=> (filter #(= % "Alice") ["Alice", "Bob", "Charles"])
("Alice")

Et sur mes données maintenant ?
Donne moi toutes les lignes dont le label est “/” !

user=> (filter #(= % "/") datarows)
()

C’est vide ? Normal, chaque ligne de la liste est une map. Il faut donc récupérer le :lb.

user=> (filter #(= (:lb %) "/") datarows)
({:na 1, :by 27985, :rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"} {:na 1, :by 27974, :rm "OK", :rc 200, :s "false", :ts 1.324109395646E12, :lt 209, :t 213, :lb "/"} {:na 1, :by 27994, :rm "OK", :rc 200, :s "false", :ts 1.324109537182E12, :lt 247, :t 252, :lb "/"} {:na 1, :by 27983, :rm "OK", :rc 200, :s "false", :ts 1.324109682199E12, :lt 344, :t 349, :lb "/"} {:na 1, :by 27980, :rm "OK", :rc 200, :s "false", :ts 1.32410980267E12, :lt 212, :t 217, :lb "/"})

Autre critère, les temps supérieurs à 10s ? Changeons simplement de fonction pour  #(> (:t %) 10000).

user=> (filter #(> (:t %) 10000) datarows)
({:na 1, :by 172777, :rm "OK", :rc 200, :s "true", :ts 1.324109405557E12, :lt 1398, :t 13007, :lb "/CategoryDisplay-REF"} {:na 0, :by 33268, :rm "OK", :rc 200, :s "true", :ts 1.324109406957E12, :lt 11602, :t 11607, :lb "http://www.witre.se/spannare_85915M.html?leafcode=85919&fromSearch=true"})

Et pour finir comment exprimer la négation ? Par exemple, trouver tous les relevés dont le statut n’est pas OK.

user=> (count (filter #(= (:rm %) "OK") datarows))
97
user=> (count (filter #(not= (:rm %) "OK") datarows))
22
user=> (count (remove #(= (:rm %) "OK") datarows))
22

Le total fait bien 119. Deux solutions sont possibles, remove ou filter avec l’expression complémentaire. La fonction remove n’altère pas la liste initiale.

L’incanter Way

Incanter propose la fonction query-dataset. Cette fonction a elle aussi un alias $where.

Les prédicats de query-dataset peuvent être exprimés dans un langage voisin du langage de requête de MongoDB.

Donne moi tous les relevés de la home page !

Ces relevés sont ceux dont la colonne :lb vaut “/”.

user=> (query-dataset data {:lb "/"} )
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm :by :na], :rows ({:na 1, :by 27985, :rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"} {:na 1, :by 27974, :rm "OK", :rc 200, :s "false", :ts 1.324109395646E12, :lt 209, :t 213, :lb "/"} {:na 1, :by 27994, :rm "OK", :rc 200, :s "false", :ts 1.324109537182E12, :lt 247, :t 252, :lb "/"} {:na 1, :by 27983, :rm "OK", :rc 200, :s "false", :ts 1.324109682199E12, :lt 344, :t 349, :lb "/"} {:na 1, :by 27980, :rm "OK", :rc 200, :s "false", :ts 1.32410980267E12, :lt 212, :t 217, :lb "/"})}

La même chose exprimée avec l’alias $where

user=> ($where {:lb "/"} data)
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm :by :na], :rows ({:na 1, :by 27985, :rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"} {:na 1, :by 27974, :rm "OK", :rc 200, :s "false", :ts 1.324109395646E12, :lt 209, :t 213, :lb "/"} {:na 1, :by 27994, :rm "OK", :rc 200, :s "false", :ts 1.324109537182E12, :lt 247, :t 252, :lb "/"} {:na 1, :by 27983, :rm "OK", :rc 200, :s "false", :ts 1.324109682199E12, :lt 344, :t 349, :lb "/"} {:na 1, :by 27980, :rm "OK", :rc 200, :s "false", :ts 1.32410980267E12, :lt 212, :t 217, :lb "/"})}

On peut aussi exprimer plusieurs critères. Ci-dessous, les temps entre 8s et 10s.

user=> (query-dataset data {:t {:$gt 8000 :$lt 10000} } )
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm :by :na], :rows ({:na 1, :by 28383, :rm "OK", :rc 200, :s "false", :ts 1.324109266396E12, :lt 8511, :t 8523, :lb "/CategoryDisplay"} {:na 1, :by 33235, :rm "OK", :rc 200, :s "true", :ts 1.324109282891E12, :lt 8967, :t 8981, :lb "/--product--.html"} {:na 1, :by 30959, :rm "OK", :rc 200, :s "false", :ts 1.324109547672E12, :lt 8787, :t 8792, :lb "/CategoryDisplay"})}

Ou bien les code retours qui ne font pas partie de la liste acceptée, 200 (OK) ni 302 (redirect).

user=> (query-dataset data {:rc {:$nin #{200 302} } } )
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm :by :na], :rows ()}
user=> (query-dataset data {:rm {:$nin #{"OK" "Found"} } } )
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm :by :na], :rows ()}

Les opérateurs disponibles dans les requêtes sont :$gt, :$lt, :$gte, :$lte, :$eq, :$ne, :$in, :$nin, $fn.

Les prédicats peuvent également être définis de manière classique par une fonction anonyme. La fonction s’applique à la ligne.

user=> (query-dataset data #(= (:lb %) "/" ) )
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm :by :na], :rows ({:na 1, :by 27985, :rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"} {:na 1, :by 27974, :rm "OK", :rc 200, :s "false", :ts 1.324109395646E12, :lt 209, :t 213, :lb "/"} {:na 1, :by 27994, :rm "OK", :rc 200, :s "false", :ts 1.324109537182E12, :lt 247, :t 252, :lb "/"} {:na 1, :by 27983, :rm "OK", :rc 200, :s "false", :ts 1.324109682199E12, :lt 344, :t 349, :lb "/"} {:na 1, :by 27980, :rm "OK", :rc 200, :s "false", :ts 1.32410980267E12, :lt 212, :t 217, :lb "/"})}

Si vous avez déjà des fonctions dans vos grimoires, vous pouvez bien sûr les utiliser directement.

user=> (defn isHomePage [row] (= (:lb row) "/"))
#'user/isHomePage
user=> (query-dataset data isHomePage )
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm :by :na], :rows ({:na 1, :by 27985, :rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"} {:na 1, :by 27974, :rm "OK", :rc 200, :s "false", :ts 1.324109395646E12, :lt 209, :t 213, :lb "/"} {:na 1, :by 27994, :rm "OK", :rc 200, :s "false", :ts 1.324109537182E12, :lt 247, :t 252, :lb "/"} {:na 1, :by 27983, :rm "OK", :rc 200, :s "false", :ts 1.324109682199E12, :lt 344, :t 349, :lb "/"} {:na 1, :by 27980, :rm "OK", :rc 200, :s "false", :ts 1.32410980267E12, :lt 212, :t 217, :lb "/"})}

Voilà, beaucoup de sortilèges a expérimenter en attendant l’épisode 4. Attention à ne pas faire trop de dégâts !


by cfalguierecfalguiere atDecember 28, 2011 02:14 PM