Création de logiciels : de l'agilité à l'artisanat

La fausse bonne idée des Virtual Extension Methods dans Java 8

Quand on s'intéresse aux évolutions de Java, il faut lire le Touilleur Express et plus particulièrement son compte rendu "Rémi Forax au Paris JUG". Apparemment, les lambda expressions feront leur apparition dans la version 8 du langage, ce dont tout le monde devrait se réjouir.
Je ne suis pas un habitué de Java. Mon premier langage fut le C++ et aujourd'hui celui que je maîtrise le mieux reste C# mais comment résister à l'envie de jeter un coup d'oeil aux évolutions de Java au moment où celui-ci se met à rattraper (un petit peu[1]) son retard sur C# ?
Je vais essayer d'expliquer ce que j'ai compris et pourquoi je pense qu'un des choix effectués ne me semble pas être des plus judicieux[2]

Un des principaux usages des lambdas expressions dans les langages orientés objet, c'est la manipulation de collections à travers quelques méthodes générales (filtrage, projection, aggregats, ...) paramétrées par des lambdas expressions.
C'est bien évidemment un des objectifs de l'introduction des lambdas dans Java comme l'explique Brian Goetz dans ces slides de novembre dernier : http://blogs.oracle.com/briangoetz/resource/devoxx-lang-lib-vm-co-evol.pdf

En pratique, le but est de pouvoir écrire le code suivant en Java :

List<Student> students = ...
double highestScore =
    students.filter(s -> s.getGradYear() == 2011)
                .map(s -> s.getScore())
                .reduce(0.0, Integer::max);

Cela n'est bien sûr que l'équivalent de ce que l'on peut écrire en C# depuis quelques années déjà :

List<Student> students = ...
double highestScore =
    students.Where(s => s.GradYear == 2011)
                .Select(s => s.Score)
                .Aggregate(0.0, Math.Max);

Une question annexe est alors : comment définir les méthodes filter/map/reduce sur une collection ?
C# a fait un choix simple. Where/Select/Aggregate et bon nombre d'autres opérations sont définies comme méthodes d'extension statiques. Ce sont tout simplement des méthodes statiques d'une classe statique qu'un artifice du compilateur permet de faire apparaitre comme des méthodes de toute énumération.
Dans la suite de ce billet je les appelerai SEM (pour Static Extension Method)

Pour le compilateur C#, le code précédent est strictement équivalent à celui-ci :

double highestScore =
  Enumerable.Aggregate (
    Enumerable.Select (
      Enumerable.Where (students, s => s.GradYear == 2011),
      s => s.Score
    ),
    0.0, Math.Max
  );

Visiblement, cette façon de faire ne plait pas aux concepteurs de Java. Brian Goetz détaille les objections suivantes :

Classes don’t know about their extension methods so cannot provide a “better” implementation
Not reflectively discoverable
No covariant overrides
Brittle – if default changes, clients have to be recompiled
Poor interaction with existing instance methods of same name
Not very object-oriented

Voyons un peu ce qu'il en est vraiment.

  • Les classes ne connaissent pas leurs méthodes d'extension. C'est en partie vrai (seule l'interface IQueryable permet de faire varier l'implémentation des SEM, c'est d'ailleurs le fondement de LINQ) mais dans la plupart des cas, on n'en a rien à faire. Les SEM ne sont pas là pour étendre une abstraction, elles sont là pour proposer un concept d'implémentation indépendant du type auquel elles se rapportent.[3]
  • Etant indépendantes des types, les SEM ne sont pas accessibles à travers la réflexion. C'est vrai mais à quoi cela pourrait servir ? Quand on se soucie de réflexion sur les méthodes d'un type, c'est que l'on aurait grand besoin, au choix, de revoir sa conception[4] ou d'aller vers un langage plus dynamique.
  • Je ne m'étendrai pas sur les redéfinitions covariantes qui n'existent pas en C# et auxquelles je préfère une conception à base de types paramétrés
  • La prétendue "fragilité" due au manque de compatibilité binaire est un leurre. La compatibilité source est la seule qui a de l'importance à mes yeux pour définir (il faudra souvent le rappeler) des concepts d'implémentation
  • L'existence de méthodes du même nom que les extensions est sans objet : qui aurait intérêt à définir ce genre de doublons ?
  • Le "not very object-oriented", c'est le "cherry on top". Est-ce que les lambdas expressions sont "very object-oriented" ???

Vous l'aurez compris : je ne partage pas vraiment le point de vue de ces javaistes sur les extension methods de C#.
Mais voyons un peu ce qu'ils proposent pour faire mieux. Ils appelent ça les "Virtual Extension Methods". Cela consiste à fournir une implémentation par défaut à une méthode d'une interface.

Exemple :

interface List<T> {
  // existing methods, plus
  void sort(Comparator<? super T> cmp) default { Collections.sort(this, cmp); };
}

A première vue, je me suis dit "pourquoi pas ?".
Cela fournit un mécanisme élégant pour faire des héritages de comportement et si ça permet de combler les défauts mineurs de l'extension par méthode statique, il n'y a pas de raison de s'en passer.
Mais j'ai quand même un peu réfléchi à la question et il y a quelques raisons qui me font penser que l'approche choisie par C# est bien meilleure.

Raison No 1 : l'extension de types existants

Imaginons que, dans notre exemple de code, nous ayons envie de simplifier l'écriture de la manière suivante :

List<Student> students = ...
double highestScore =
    students.Where(s => s.GradYear == 2011)
                .Select(s => s.Score)
                .Max();

En effet, le "Aggregate(0.0, Math.Max)" n'est pas un modèle de lisibilité et il gagnerait à être raccourci par le rajout d'une méthode qui renvoie le maximum d'une collection.

En C#, cette fonction "Max" existe nativement mais, si elle n'existait pas, elle serait très facile à rajouter avec une SEM car celles-ci sont entièrement indépendantes des types auxquelles elles se rapportent :

static class MyIEnumerableExtension {
  static double Max(this IEnumerable<double> self)
  {
     return self.Aggregate(0.0, Math.Max);
  }
}

Allez donc faire ça avec les VEM de Java sans toucher à l'interface...
C'est d'ailleurs dingue ce que l'on peut faire dès que l'on a la possibilité de rajouter des méthodes à une interface sans toucher à celle-ci.
La future version de java aura de nouvelles méthodes filter/map/reduce dans ses collections mais comment être exhaustif et ne pas oublier toutes les méthodes qui pourraient être utiles ?
J'espère que ceux qui amélioreront les interfaces auront une bonne boule de cristal... Ils ne faudrait pas qu'ils oublient de mettre des méthodes de groupement et de mise en dictionnaire. Elles sont parfois si utiles.

List<Student> students = ...
Dictionary<double,IEnumerable<Student>> studentsByScore =
  students.GroupBy (s => s.Score)
          .ToDictionary (g => g.Key, g => g.AsEnumerable());


Raison No 2 : la maintenabilité au delà de la réutilisabilité

L'aspect le plus élégant des VEM (mixer du code et des interfaces) est aussi le plus dangereux en termes de maintenabilité.
Commençons par rappeler quelques principes énoncés par Uncle Bob :

Common Closure Principle
The classes in a package should be closed together against the same kind of changes.
A change that affects a package affects all the classes in that package.

Stable Abstractions Principle
Packages that are maximally stable should be maximally abstract.
Instable packages should be concrete.
The abstraction of a package should be in proportion to its stability.

Si l'on admet que le respect de ces principes (et de quelques autres) maximise la maintenabilité d'une application, alors on a un petit problème avec les VEM.
Mélanger une abstraction relativement stable avec un détail d'implémentation qui va plus souvent évoluer, fut-il un comportement par défaut, ne va pas dans le sens de regroupements maintenables.

En clair, on est dans un des deux contextes suivants :

  • Soit les VEM ne sont utilisées qu'à la marge et on n'a pas de problème de maintenabilité. On peut alors se demander si les VEM sont une véritable stratégie du langage Java ou une simple réponse tactique au seul problème de la compatibilité des collections Java existantes avec le rajout filter/map/reduce...
  • Soit les VEM sont amplement utilisées et elles sont alors une improbable invitation à mélanger abstraction stables et détails d'implémentation anéantissant ainsi tout effort allant vers plus de maintenabilité d'une application Java

A l'inverse, les SEM de C# induisent une séparation claire entre les abstractions et les implémentations.

Raison No 3 : une fonctionnalité pour les développeurs d'applications

Cette troisième raison est un peu le corollaire des deux raisons précédentes.
Brian Goetz écrit :

The whole point of extension methods is being able to compatibly evolve APIs
The key operation we care about is adding new methods with defaults to existing interfaces without necessarily recompiling the implementation class

Les solutions qu'il apporte vont clairement dans le sens de la facilitation d'écriture de librairie tierces et d'API : les Virtual Extension Methods de Java ne sont qu'une fonctionnalité pour les développeurs de composants et de frameworks
A l'inverse, avec leurs possibilités d'extension de types externes et de séparation abstraction/détail, les Static Extension Methods de C# sont une fonctionnalité pour les développeurs d'applications.

On pourra argumenter que c'est important d'avoir des composant tiers extensibles qui ne cassent pas la compatibilité binaire et qu'à ce titre, les VEM sont un bon choix.
A cela, je suis tenté de répondre :

  • que l'extensibilité grace aux VEM est plus que relative (on ne peut pas rajouter une méthode à une interface sans casser la compatibilité binaire...)
  • que le mécanisme IQueryable de C#/.NET qui est un des fondements de LINQ offre des possibilités bien supérieures si on tient absolument à écrire des composants binairement indépendants avec des fonctionnalités d'un tout autre ordre (récupération dynamique de l'arbre d'expression sous-jacent aux appels extension methods + lambdas)[5]

Ma conclusion est simple : le choix VEM vs SEM est un peu le reflet de l'écosystème dans lequel chacun des langages évolue.
En tant que développeur d'applications, je suis bien content d'utiliser C# et ses extensions statiques qui facilitent ma vie de tous les jours. Je laisse volontiers les Virtual Extension Methods aux amoureux des frameworks en tout genre et aux adeptes des armées de librairies à tout faire et à assembler "comme on peut"...

Notes

[1] troll inside

[2] Et si je fais des erreurs dans ce billet, je ne doute pas qu'un javaiste de passage saura me le faire remarquer...

[3] J'ai écrit récemment une série de billets sur le sujet. Le premier est là.

[4] Il y aurait un billet entier à faire sur "comment se passer de la réflexion sur les méthodes"

[5] Il faudrait là aussi un billet complet pour montrer le polymorphisme introduit en C# par LINQ

Les logiciels petits sont les plus jolis

Cinquième billet et dernier billet de cette série sur la conception de logiciels[1]
Que se passe-t-il lorsqu'un logiciel grossit ?
Est-on en mesure d'introduire indéfiniment de nouveaux concepts et de conserver un ensemble toujours aussi évolutif ?

Je ne crois pas que cela soit possible car

  • cela nécessiterait une équipe de plus en plus grosse pour gérer la base de code
  • et cela demanderait un effort monumental pour garantir la cohérence fonctionnelle de l'ensemble

Je sais que, dans certains endroits, plusieurs équipes travaillent simultanément sur la même base de code, chaque équipe étant en charge de fonctionnalités bien définies. On parle alors d'"équipe feature"[2]. Je n'ai jamais expérimenté cela et je reste dubitatif sur l'efficacité de la chose.
Il m'est arrivé de travailler sur de gros logiciels mais le développement de ceux-ci était toujours organisé en composants, chaque composant ayant sa base de code propre et son équipe dédiée, y compris au niveau fonctionnel : un chef de produit et un backlog produit par composant.

La plus grande qualité que je trouve aux composants logiciels, c'est qu'ils permettent à la base de code d'un logiciel de rester dans une taille raisonnable. Cela se réalise en considérant chaque composant comme un logiciel à part entière avec ses propres exigences et ses propres concepts.
A mon avis, le seul risque de cette approche est de définir des composants trop en amont dans le développement d'un logiciel, ce qui pourrait amener à des choix qui se marient mal avec les concepts du logiciel décomposé.
Mais si on laisse faire les choses et que l'on découpe au moment opportun, le risque disparait.

Exemple :

  • 1- Le code d'un logiciel a été simplifié grace à un concept d'implémentation
  • 2- Après quelques évolution du logiciel, le concept sous-jacent commence à grossir
  • 3- Il grossit tellement qu'il commence lui-même à être organisé avec des abstractions et des détails
  • 4- Il devient alors un produit logiciel à part entière où une partie de ses concepts d'architecture sont l'API qui permet de l'utiliser
  • 5- Notre logiciel est revenu à sa taille initiale (voire à une taille plus petite) car il utilise un produit tiers qui a son propre cycle de vie

5x01.png

Autre exemple :

  • 1- Un gros logiciel est défini par ses concepts d'architecture et ses détails d'implémentation
  • 2- Une partie de ce logiciel est fortement découplée du reste (le lien ne se fait que par quelques abstractions) : elle est naturellement organisée comme si elle était un produit à part entière, les quelques abstraction formant le lien étant l'API du produit distinct
  • 3- On donne sa liberté à ce produit et le logiciel initial retourne une taille raisonnable tout en ayant limité le lien avec le nouveau produit tiers à un seul de ses détails d'implémentation

5x02.png

On voit donc que l'émergence de produits tiers à partir de morceaux d'un logiciel existant peut être intéressante si elle respecte le découpage naturel du logiciel, c'est à dire si elle se base sur des concepts qui ont déjà émergé. Par ailleurs, comme nos jeux de tests se basent sur les concepts, qu'ils soient d'architecture ou d'implémentation, les nouveaux produits débutent leur existence avec des jeux de tests qui expriment clairement leur comportement.

Il existe toutefois des cas où les choses ne sont pas aussi simples.

  • 1- Prenons un logiciel qui, à travers ses concepts d'architecture, propose une API et la possibilité d'être étendu par divers détails présentés comme des "plugins"
  • 2- Si ces plugins venaient à grossir, on ne pourrait pas les considérer comme des produits tiers dont dépend le logiciel car l'API fait partie du logiciel et les plugins en dépendent. Une solution est alors de considérer notre le logiciel initial comme un produit tiers.
  • 3- En faisant cela, les plugins deviennent notre logiciel principal qui va pouvoir grossir en dépendant du produit tiers nouvellement créé. Cette opération est toutefois délicate car l'API qui sert désormais de point d'entrée au produit tiers n'est pas forcément adaptée à cela et, du fait de l'orientation "plugins", elle ne possède peut être pas les jeux de tests adéquats.

5x03.png

Cela reste une exemple. On pourrait avoir un plugin qui grossirait par introduction d'un concept d'implémentation lequel deviendrait à son tour un produit à part entière (cf 1er exemple du billet).
On retiendra que le concept d'architecture de type "API qui permet d'étendre un logiciel", ça peut être sympa sur le moment mais ça peut engendrer des inconvénients à plus long terme en fonction de la façon dont on appréhende le développement des plugins.

Voilà...
Ainsi s'achève cette série sur la conception de logiciels, ou devrais-je plutôt dire sur ma vision, en ce début d'année 2012, de ce qui me semble primordial dans la réalisation d'un logiciel. J'espère que cela aura intéressé quelques personnes. J'espère aussi avoir quelques retours qu'ils soient positifs ou négatifs. En tout cas, cela aura bien occupé mon temps libre de la semaine !

Notes

[1] Les quatre premiers billets sont là : Conception logicielle ; Le logiciel, un organisme multicellulaire ? ; Chacun cherche son TDD ; Le logiciel d'un développeur est son château.

[2] L'article "Feature Team Primer" dont une traduction par Fabrice Aimetti est disponible ici : http://www.fabrice-aimetti.fr/dotclear/index.php?post/2011/06/13/Equipe-feature présente ce genre d'organisation mais, pour moi, les auteurs avancent des arguments qui reposent entièrement sur une définition que je ne partage pas de ce qu'est une "architecture". Il faudrait un billet complet pour détailler ce sujet...

Le logiciel d'un développeur est son château

Quatrième billet de la série sur la conception de logiciels[1]

Je n'ai pas encore abordé la question des éléments tiers dans la réalisation d'un logiciel.
Il y a une bonne raison à cela : je crois fermement que c'est un point de détail qui ne mérite pas une place trop importante.

Quand on discute sur les méthodes agiles et sur la capacité de pouvoir remettre en cause à tout moment le comportement de l'existant pour satisfaire au besoin le plus prioritaire d'un produit logiciel, il y a toujours quelqu'un pour dire "Oui on peut faire des changements mais il y a quand même des choix d'architecture que l'on ne peut pas facilement modifier". Et bien souvent il y a quelqu'un pour rajouter "c'est pourquoi il faut définir l'architecture dans la première itération". Il m'est arrivé plusieurs fois d'entendre cela et je ne partage pas du tout cet avis.

Cette divergence de points de vue est simple à comprendre : tout le monde ne met pas la même chose derrière le mot "architecture".

Choisir un SGBDR X ou Y ou choisir un stockage "NoSQL", ce n'est pas faire un choix d'architecture, c'est faire le choix des détails d'un mécanisme de persistance.
Choisir un framework web Y ou Z, ce n'est pas faire un choix d'architecture, c'est faire le choix des détails d'un mécanisme de diffusion de données.

Comme le dit l'académie française[2], l'architecture, c'est la disposition, l'ordonnance d'un édifice. Les choix d'architecture doivent représenter ce qui est fondamental -et rien d'autre- dans un logiciel, ce qui constitue sa nature, des choses telles que la modélisation du domaine métier ou la formalisation des principales opérations réalisées par le logiciel.
Cette approche permet de garantir un point essentiel de l'architecture : sa stabilité. Sous certaines conditions, cette stabilité ne remet pas en cause la capacité à modifier un comportement et elle offre la possibilité de changer de mécanisme de persistance ou de diffusion à tout moment dans la (longue) vie du logiciel.

Une des conditions est que les concepts d'architecture ne doivent pas dépendre de logiciels tiers. Bien evidemment la suppression de toutes les dépendances est impossible : si les concepts d'architecture sont exprimés dans un langage informatique, ils dépendent, au minimum, de la syntaxe de ce langage et d'un interpréteur/compilateur/runtime associé. Idéalement, il ne devrait y avoir aucune autre dépendance.

En résumé, un logiciel est à son développeur ce que sa maison est à un anglais : son château, c'est à dire un endroit où il n'a pas à subir les invasions de logiciels tiers et de leurs hordes de contraintes.

Si tout cela est acquis, seuls les concepts et les détails d'implémentation dépendent des logiciels tiers (représentés en vert dans le schema qui suit).
La dépendance à un tiers donné se limite alors à un élément précis et ne s'éparpille pas dans tous les détails du logiciel. Les concepts d'implémentation peuvent centraliser ces dépendances le cas échéant.

4x01.png

Voilà réglée de manière un peu radicale la question des tiers.
Il va sans dire que certaines pratiques désormais courantes, telles que prendre un framework tiers pour constituer l'ossature d'un logiciel, me font hurler...

Le prochain billet abordera un sujet connexe : le cas des GROS logiciels et de leur possible décomposition...

Notes

[1] Les trois premiers billets sont là : Conception logicielle ; Le logiciel, un organisme multicellulaire ? et Chacun cherche son TDD.

[2] cf premier billet de la série : Conception logicielle

Chacun cherche son TDD

Ce billet est le troisième d'une série sur la conception de logiciels[1].

Après avoir parlé de l'évolution d'un logiciel et de l'émergence des divers concepts qui le composent, voyons comment un développeur peut maitriser cette évolution. Le meilleur outil que je connaisse aujourd'hui pour cela, c'est le développement piloté par les tests[2].

Beaucoup de choses ont été écrites sur le TDD et ce billet sera donc un énième point de vue (le mien) sur le sujet.
Des reproches lui sont régulièrement faits : "le nombre de tests à écrire est trop coûteux", "le TDD est une mauvaise approche car il est inutile de tester toutes les fonctions/toutes les méthodes de toutes les classes", ...
Ces points de vue me donnent au moins une certitude : tout le monde ne met pas la même chose derrière le mot TDD.

En ce qui me concerne, le TDD me sert à deux choses :

  • 1- faire émerger et peaufiner les concepts d'un logiciel, aussi bien ceux d'architecture que d'implémentation
  • 2- vérifier le comportement des détails d'implémentation au travers des concepts

Le premier point est la conséquence directe de l'évolution naturelle d'un logiciel par conception émergente.
Regardons comment peuvent apparaitre des concepts d'architecture lors d'un pilotage par les tests.

  • 1- On a un test vide ;
  • 2- On commence à écrire le test : un début de concept apparait pour accéder aux détails à venir ;
  • 3- Les détails apparaissent. On passe alors par plusieurs phase red-green-refactor qui vont faire évoluer à la fois l'implémentation et le concept pour arriver à un résultat satisfaisant ;
  • 4- On introduit d'autres scénarios de test. Lors des red-green-refactor, d'autres concepts apparaissent ;
  • 5- Les concepts ont permis de diviser les détails. Une partie peut alors être avantageusement remplacée par une doublure pour favoriser la croissance ultérieure du logiciel.

3x01.png

L'apparition de nouveaux concepts d'architecture est le cas de figure idéal pour découpler les détails d'implémentation mais elle n'est jamais garantie.
Quand des détails deviennent trop complexes, il reste la possibilité d'introduire de nouveaux concepts d'implémentation comme dans l'exemple suivant.

  • 1- On a un test vide ;
  • 2- On commence à écrire le test : un début de concept apparait pour accéder aux détails à venir ;
  • 3- Les détails apparaissent. On passe alors par plusieurs phase red-green-refactor qui vont faire évoluer à la fois l'implémentation et le concept pour arriver à un résultat satisfaisant ;
  • 4- On introduit d'autres scénarios de test. Lors des red-green-refactor, le code se complexifie ;
  • 5- On travaille sur un nouveau concept d'implémentation pour simplifier le code. Pour cela, on rajoute des tests ad-hoc et on fait évoluer en parallèle le nouveau concept et son utilisation ;
  • 6- Le nouveau concept finit par remplacer complètement le code trop complexe.

3x02.png

Le point délicat de ces phases d'émergence est que, bien évidemment, les tests existants portent sur des concepts existants. Toute la difficulté réside dans la création de jeux de tests portant sur les nouveaux concepts. Cela nécessite soit de mener en parallèle l'émergence et la constitution des nouveaux tests, soit de les écrire a posteriori.
En l'occurrence, dans le premier exemple on n'a pas introduit de scenarios de tests utilisant le dernier concept créé pour vérifier les détails qui ont été séparés. Cela doit être fait immédiatement après pour permettre à ces détails de continuer à croitre sereinement.

3x03.png

Une caractéristique commune à tous les tests de ces exemples est qu'ils ne dépendent que des concepts, jamais des détails.
Pour moi, c'est un point important. Les tests sont écrits en utilisant des notions stables. Cela permet à la fois d'écrire des tests qui résisteront mieux à l'épreuve du temps et de les exprimer avec un vocabulaire clairement défini. Un test, ce n'est pas juste du code qui affiche une bande verte ou une bande rouge, c'est surtout un moyen de décrire intelligiblement le comportement du logiciel.

A l'usage, la manière d'écrire les tests diffère sensiblement selon que l'on manipule des concepts d'architecture ou des concepts d'implémentation.
Les concepts d'implémentation sont très proches des détails du code. Je trouve que les outils de type xUnit[3] se prêtent bien à l'écriture des tests qui y sont rattachés. Ils permettent de construire des concepts qui utilisent un maximum de possibilités du langage utilisé (par exemple, pour des fluent interfaces[4])

Les concepts d'architecture constituent le langage commun aux divers éléments d'un logiciel et, par extension, aux personnes qui participent au développement de ce logiciel. C'est en utilisant ce langage que l'on peut écrire des spécifications exécutables. Les concepts peuvent ainsi être paramétrés dans des outils de spécification exécutables de type 'scenario' (cucumber, specflow, jbehave...)
Typiquement, on pourra créer une bibliothèque de doublures dont les étapes d'initialisation seront utilisées dans les pré-conditions d'un scenario. On peut ainsi se constituer un "DSL" de spécifications pour notre logiciel. En se basant uniquement sur les concepts d'architecture, on peut écrire tous les scénarios utiles dans un langage naturel sans avoir à systématiquement détailler les étapes dans du code spécifique.
J'ai trop peu d'expérience avec les outils de spécification de type 'document' (fitnesse, concordion...) mais je suppose que l'on peut les utiliser de la même manière.

Emergence de la conception... Pilotage par les tests... Tout cela marche souvent très bien quand on écrit du code qui fonctionne en autarcie. Les choses peuvent être légèrement différentes quand il s'agit d'interagir avec des logiciels tiers
Dans le prochain billet, j'essaierai donc de parler des relations qu'un logiciel entretient avec son environnement.

Le logiciel, un organisme multicellulaire ?

Ce billet est le deuxième d'une série sur le conception de logiciels[1].

Je m'intéresse aujourd'hui à la manière dont évolue la structure d'un logiciel, notamment en ce qui concerne les concepts dont on a pu parler précédemment.

Il y a fort longtemps, les cours que j'ai pu avoir en relation avec le développement logiciel avaient toujours une approche assez constructiviste : avant d'écrire la moindre ligne de code, on commence par découper un problème donné en problèmes élémentaires, puis on réfléchit aux concepts que l'on va utiliser, ensuite on commence a écrire des bouts de code que l'on va assembler pour parvenir au logiciel final.

En suivant cette démarche, on commence par définir l'architecture du logiciel avant d'en réaliser les détails :

2x01.png

Et même au niveau des détails, on est amené à suivre la même approche. On commence par implémenter quelques concepts de base avant de les assembler :

2x02.png

Cette approche a le mérite d'être analogue à certains domaines de l'ingénierie ("on crée le plan avant de réaliser l'ouvrage") mais, en ce qui me concerne, elle survit difficilement à la réalité du développement logiciel.
Je n'ai jamais vu quelqu'un bâtir intégralement un logiciel comme on construirait une maison. A un moment ou à un autre, le logiciel dévie sensiblement du moindre plan que l'on aurait pu faire. En fait, par certains aspects, un logiciel ressemble à un organisme...

Organisme : entité biologique, unicellulaire ou pluricellulaire, capable de se développer et de se reproduire.

Dans le phénomène probablement le plus connu de la reproduction cellulaire, la mitose[2], une cellule croit et se divise pour au final donner deux cellules génétiquement identiques mais qui peuvent être fonctionnellement différenciées.

Mitose

Il en va un peu de même pour la formation des concepts d'architecture d'un logiciel. Lorsqu'une implémentation grossit, des abstractions apparaissent et, si tout se passe bien, l'implémentation se divise en deux parties indépendantes mais rattachées par ces nouvelles abstractions.

2x03.png

Pour rester dans le même type d'analogie, on pourrait aussi évoquer la réparation d'une plaie cutanée[3] : l'organisme s'auto-répare lorsque survient une blessure légère.

Dans un logiciel, le code blessé est celui qui, devenu trop complexe, ne peut plus évoluer correctement. L'émergence de concepts d'implémentation répare le code et lui permet d'aller de l'avant.

2x04.png

Bref, même si de bons concepts peuvent être pensés a priori, il ne faut pas abuser de la chose. Un logiciel qui évolue harmonieusement à partir de besoins de conceptualisation avérés (une abstraction qui émerge, une implémentation qui se simplifie) a de meilleures chances d'évoluer et croitre qu'une créature de Frankenstein assemblée à partir de morceaux choisis en amont.

Le défi dans l'évolution d'un logiciel, c'est la manière dont un développeur arrive à la contrôler. Cela sera le sujet d'un prochain billet.

Conception logicielle

Une pratique utile à tout développeur de logiciels est l'analyse de son propre travail[1]. C'est dans cet esprit que j'entame aujourd'hui une petite série de billets sur la conception de logiciels, ou, plus précisément, ce qu'est, à cet instant, pour moi, la conception de logiciels.
C'est à la fois une façon de faire un point personnel sur ce que j'ai appris[2], de le partager avec tous ceux qui pourraient être intéressés[3], et enfin de l'exposer à la critique publique de ceux qui prendront le temps de le lire[4].
Dans cette série de billets, je compte donc aborder les points qui me paraissent les plus importants dans la conception, la manière dont ils évoluent au fil du temps, le contrôle qu'exerce un développeur sur cette évolution et les relations qu'entretient le logiciel avec son environnement.

Avant de parler de "conception" logicielle, il me faut définir le terme.
Le dictionnaire de l'académie française[5] propose plusieurs définition pour la conception. Celle-ci me parait adaptée.

Conception : Action de former le concept d'un objet

Allons voir la définition d'un concept. Il y en a, là aussi, plusieurs mais je prends celle qui me convient le mieux :

Concept : Construction de l'esprit explicitant un ensemble stable de caractères communs désigné par un signe verbal.

Ainsi, j'ai envie de dire que concevoir un logiciel, c'est nommer et décrire ce que partagent les divers éléments qui le composent. Ce partage induit une dépendance des divers éléments, les détails, envers ce qu'ils ont en commun. 1x01.png

Ceci étant, tous les concepts n'ont pas la même nature. J'en distingue deux :

  • Certains concepts sont omniprésent sur l'ensemble du logiciel. Ils permettent de décrire, par exemple, le domaine métier que le logiciel manipule[6] ou encore les contrats que respectent les divers éléments quand ils dialoguent les uns avec les autres[7].
  • D'autres concepts n'ont de raison d'être que par rapport à une tactique de réalisation du logiciel. Un élément va, par exemple, utiliser une évaluation paresseuse[8] pour optimiser les performances ; d'autres élements aux algorithmes similaires vont partager un patron de méthode[9] ; etc.

Intéressons-nous au premier cas. Les concepts omniprésents sont la base sans laquelle le logiciel ne pourrait exister. Ils décrivent la manière dont les élements de détail sont agencés les uns par rapport aux autres. Il y a un mot qui me rappelle cette idée de mise en forme globale d'une construction :

Architecture : Disposition, ordonnance d'un édifice.

On pourrait donc parler de concepts d'architecture.

Faisons un zoom sur le schema précédent pour apercevoir les packages qui composent le logiciel. On entend ici par package des bouts de code dont le contenu est lié par un destin commun, tant du point de vue de l'utilisation que de la modification[10]. On a, en bleu, les packages contenant les concepts d'architecture et, en rouge, les détails de réalisation du logiciel. 1x02.png

Faisons un aparté rapide pour préciser que l'association implicite "concept <-> bouts de code" n'a rien d'étonnant. On peut dessiner un concept sur un tableau ; on peut le décrire dans un document mais s'il n'est pas présent explicitement dans le code, il n'existe pas !

Si on fait un nouveau zoom, on découvre les concepts d'implémentation qui constituent les choix de construction des détails. A priori, ces concepts sont internes aux packages de détails car l'utilisation d'un entre eux dans un package n'a aucune implication sur les autres packages. Sur le schéma suivant, les concepts d'implémentation sont représentés en beige. 1x03.png

Toutefois, il arrive que certains de ces concepts soient utilisés dans plusieurs situations indépendantes. Ils intègrent alors, pour des raisons pratiques, des packages partagés. 1x04.png

On se retrouve ainsi avec plusieurs niveaux de définition d'un logiciel sur lesquels on doit pouvoir vérifier les progressions habituelles d'abstraction et de stabilité[11].

Concepts d'architectureConcepts d'implémentationDétails d'implémentation
DépendancesNe dépendent que d'autres concepts d'architecturePeuvent dépendre de concepts d'architecture ou de d'autres concepts d'implémentationDépendent de concepts d'architecture et de concepts d'implémentation
Niveau d'abstractionEntièrement abstraitsMélange d'abstraction et de détailsEntièrement composés de détails
Niveau de stabilitéStabilité maximalePartiellement stableChange au moindre changement dans le logiciel

Dans un prochain billet, on parlera de la formation et de l'évolution de ces divers éléments.

Je n'aime pas les conteneurs d'injection de dépendances

Je n'ai rien contre les DI containers en tant que tels. Je pratique l'injection de dépendances aussi souvent que nécessaire mais j'ai un problème avec les librairies et frameworks en tout genre qui me donnent plus de complications à gérer que d'écrire le code correspondant aux 10% de fonctionnalités dont j'ai réellement besoin.

C'est pour cela que je lis avec grand plaisir des phrases telles que "Dependency Injection doesn’t require a framework; it just requires that you invert your dependencies and then construct and pass your arguments to deeper layers" venant de quelqu'un dont la crédibilité n'est plus à démontrer.

L'injection de dépendances, ce n'est jamais qu'un cas particulier de l'inversion de dépendances, c'est à dire d'une structuration logicielle où les détails d'implémentation ne dépendent pas l'un de l'autre mais dépendent tous d'abstractions.
La spécificité de l'injection de dépendances, c'est, étant donnés un contexte et une abstraction, d'être capable de fournir automatiquement le détail d'implémentation de l'abstraction pour le contexte considéré. Bref, si on est capable d'implémenter l'appel suivant (écrit dans un pseudo langage objet), on est sur la bonne voie :

instance = context.GetDetailsFor(abstraction)

Une abstraction, c'est typiquement ce que, dans les langages orientés objets les plus courants, on appelle une "interface", Mais un contexte ? J'ai un théorème pour répondre à ça.

Théorème de oaz sur l'injection de dépendance

Le contexte d'une injection de dépendance peut se limiter à un choix de package

Démonstration

Une abstraction étant fixée, une injection de dépendance est le choix d'une classe implémentant cette abstraction et des arguments de construction d'une instance de cette classe.
Les arguments de constructions peuvent être rendus optionnels : il suffit pour cela d'utiliser une classe dérivée ad hoc où les arguments en question sont fixés.
Reste à choisir la classe à instancier.
Si une injection de dépendance nécessitait plus qu'un choix de package, cela signifierait que, en plus du package choisi, il faut d'autres informations qui permettent de choisir telle ou telle classe du package implémentant l'abstraction considérée. Cela implique que, parmi les classes du package choisi, au moins deux d'entre elles implémentent la même abstraction injectable et et ces deux ne vont donc pas être utilisées simultanément.
L'existence de ces deux classes dans le même package est une violation du Common Reuse Principle qui dit que "The classes in a package a reused together. If you reuse one of the classes in a package, you reuse them all"


Ce résultat théorique n'est pas toujours pratique à mettre en oeuvre (du fait de la nécessité d'une classe dérivée dans certains cas) mais pour un grand nombre de cas d'injection de dépendances, il suffit amplement.

Et, ce qui n'est pas pour me déplaire, dans un langage pas trop mal fichu, il me donne l'occasion d'écrire un "framework" basique d'injection de dépendances en 5 ou 6 lignes de code :

public static class MyDIContainer
{
  public static ABSTRACTION GetDetailsFor<ABSTRACTION>(this Assembly a)
  {
      var candidates = a.GetTypes().Where(t => typeof(ABSTRACTION).IsAssignableFrom(t));
      if(candidates.Count() != 1)
        throw new ApplicationException(string.Format("Cannot find unique implementation of {0} in {1}", typeof(ABSTRACTION), a));
      return (ABSTRACTION) Activator.CreateInstance(candidates.First());
  }
}

Ce "framework", qui se contente de retrouver dans un package donné l'unique classe qui implémente une interface donnée, s'utilise de la façon suivante :

var context = Assembly.LoadFile("DependenciesToInject.dll");
var instance = context.GetDetailsFor<IAmAnAbstraction>();

Une manière simple d'étendre ce "framework" pour prendre la main sur la phase de construction des instances, c'est de rajouter une interface "IProvideCustomDetails" et de l'implémenter dans les packages qui ont besoin de cette spécificité.

public interface IProvideCustomDetails
{
  ABSTRACTION GetDetailsFor<ABSTRACTION>() where ABSTRACTION : class;
}

public static class MyDIContainer
{
  public static ABSTRACTION GetDetailsFor<ABSTRACTION>(this Assembly a) where ABSTRACTION : class
  {
    return a.GetCustomDetailsFor<ABSTRACTION>() ?? a.GetUniqueImplementationFor<ABSTRACTION>();
  }
	
  public static ABSTRACTION GetUniqueImplementationFor<ABSTRACTION>(this Assembly a) where ABSTRACTION : class
  {
    var candidates = a.GetTypes().Where(t => typeof(ABSTRACTION).IsAssignableFrom(t));
    if(candidates.Count() != 1)
      throw new ApplicationException(string.Format("Cannot find unique implementation of {0} in {1}", typeof(ABSTRACTION), a));
    return (ABSTRACTION) Activator.CreateInstance(candidates.First());
  }
	
  public static ABSTRACTION GetCustomDetailsFor<ABSTRACTION>(this Assembly a) where ABSTRACTION : class
  {
    var provider = a.GetUniqueImplementationFor<IProvideCustomDetails>();
    return ( provider == null ) ? null : provider.GetDetailsFor<ABSTRACTION>();
  }
}

Bien évidemment, ce genre de code ne va pas rendre l'intégralité des services que rendrait un conteneur de DI "prêt à l'emploi" mais, en ce qui me concerne, je n'ai pas besoin de beaucoup plus que ça (typiquement j'aurais tendance à rajouter un passage de données de configuration à l'appel GetDetailsFor). Je n'ai donc aucune incitation pour utiliser des choses bien plus compliquées et qui nécessitent un suivi ne serait-ce que pour intégrer les mises à jour qui ne manquent pas de fleurir.

Peut être que ce choix n'est finalement qu'une question de contexte de développement. Un développeur qui vogue de projet en projet aura tout intérêt à débarquer avec une panoplie de connaissances directement utilisables. La maîtrise d'un ou plusieurs DI container du marché en fait partie. Cela peut permettre de gagner du temps lors d'un démarrage de projet.
Mais un développeur qui passe des années à faire évoluer les mêmes produits, a-t-il le même intérêt ?
Gardons cela pour un autre débat...

Faut-il encore parler de méthodes agiles en 2012 ?

Les années passent et je me dis de plus en plus souvent : mais pourquoi encore parler de méthodes agiles ?
Pourquoi garder ce terme restrictif alors que l'on devrait, à mon avis, tout simplement parler de méthodes professionnelles de développement logiciel ?

Car c'est bien de cela dont il s'agit : des valeurs, des principes et des pratiques qui guident toute activité de développement logiciel et qui caractérisent, sans fausse modestie ni dédain, le professionnalisme nécessaire à la satisfaction des utilisateurs et à la pérennité de l'ouvrage.

En ce début d'année 2012, je ne vais donc prendre qu'une seule résolution pour toutes mes activités qui tournent autour du développement logiciel : parler le moins possible de méthodes agiles et le plus possible de méthodes professionnelles.

Et pour sortir du carcan des "méthodes agiles", quoi de mieux que de commencer l'année avec une rencontre axée sur l'écriture de code, activité majeure de tout développement logiciel ?
Le 5 janvier, c'est la première du software craftsmanship Toulouse (il y aurait aussi beaucoup à dire sur le terme craftsmanship mais chaque chose en son temps...)

Dans mon agenda "agile", j'enchainerai les 2 et 3 février avec une réunion de sensibilisation aux méthodes agiles pour les enseignants des IUT Informatique où j'aurai le plaisir d'intervenir.
Malgré un programme pédagogique national des DUT informatique assez ancien, il y a des choses qu'un diplomé de 2012 qui se lance dans le développement logiciel devrait savoir et toute action de promotion/sensibilisation ne peut aller que dans le bon sens.
J'y animerai probablement un des ateliers ludiques sur l'importance des principes de conception pour garder un logiciel maintenable ou sur la place des artefacts de test dans le TDD. Cela m'évitera de parler de méthodes agiles (ce que d'autres feront bien mieux que moi).

Ce début d'année bien rempli se poursuivra les 16 et 17 mars à Banyuls sur mer pour le premier Agile Open Sud. La rencontre entre une trentaine de passionnés[1] promet d'être enrichissante.
J'y aborderai peut-être le sujet de la place des méthodes agiles. Sommes-nous contraints à vivre éternellement dans cet enclos pour bêtes curieuses du développement logiciel ? Est-ce aller trop loin que de suggérer le caractère non-professionnel de ceux qui resteraient scotchés sur des approches d'un autre siècle ?

Mais mon principal chantier de l'année sera certainement de prendre du recul sur le chemin parcouru avec la même équipe de développement depuis six ans. Aujourd'hui, je n'utilise pas quotidiennement une méthode agile. Je ne fais que développer une famille de produits logiciels en adaptant jour après jour la manière de travailler. Après avoir fait, il y trois ans, un premier bilan sur la mise en place de Scrum, il me faudrait faire une synthèse sur tout ce qui a changé depuis : des itérations au flux, des estimations à la décomposition, des tests manuels aux tests automatisés et à l'exploration, l'architecture centrée sur le métier, l'apprentissage en continu...

Je ne parlerai peut être plus de méthodes agiles mais je vais encore parler de développement logiciel pendant un certain temps.

Notes

[1] il reste encore des places mais ça devrait partir, je pense, assez vite

Boucles à base de générateurs et de transformations : un exemple plus complet

Dans un billet précédent, j'avais évoqué une approche pour l'écriture de boucles permettant d'expliciter la combinaison de diverses exigences.
Le reproche fait dans les commentaires de ce billet est que cette approche serait moins lisible et rendrait inutilement le code plus compliqué.

Pour ma part, je campe sur mes positions. L'approche n'est moins lisible que pour des exemples triviaux que l'on ne rencontre jamais dans un programme réel.
La complexité inhérente à ces programmes fait que, au final, un programme est plus facilement maintenu s'il se base sur une combinaison de générateurs et de transformations indépendantes que s'il se base sur des boucles où tous les ingrédients sont mélangés.

Ce débat me semble par ailleurs être une illustration de la différence entre "simple" et "facile" mise en lumière par Rich Hickey. Les boucles while/for sont certainement plus faciles à écrire au début mais elle ne donnent pas ce qu'il y a de plus simple à maintenir et faire évoluer.

Essayons donc de voir ce que cela donne avec un programme à peine plus riche que des exemples d'une ou deux lignes.



Soit un programme qui :

  • demande à l'utilisateur de choisir une fonction parmi les suivantes :
    • mettre un entier au carré
    • calculer la somme des entiers inférieurs ou égaux à un entier donné
    • calculer le k-ième terme de la suite de Syracuse à partir d'un entier donné, k étant un entier saisi par l'utilisateur
  • demande à l'utilisateur de choisir un filtre parmi les suivants :
    • prendre les k premiers éléments, k étant un entier saisi par l'utilisateur
    • prendre les éléments inférieurs ou égaux à k, k étant un entier saisi par l'utilisateur
    • prendre les éléments impairs inférieurs ou égaux à k, k étant un entier saisi par l'utilisateur
  • Lorsque l'utilisateur a fait ces choix, affiche les images par la fonction choisie précédemment des entiers naturels strictement positifs restreints au filtre lui aussi choisi précédemment

Exemples d'utilisation :

  • L'utilisateur choisit "mise au carré", puis "4 premiers éléments". Le programme affiche : 1, 4, 9, 16
  • L'utilisateur choisit "somme des entiers inférieurs ou égaux", puis "impairs inférieurs à 17". Le programme affiche : 1, 3, 15
  • L'utilisateur choisit "3ème terme de la suite de Syracuse", puis "éléments inférieurs à 6". Le programme affiche : 2, 4, 5, 1

Voici une implémentation à base de générateurs et de transformations. Le code complet est disponible sur github.

J'ai essayé de décrire la démarche étape par étape mais je ne parle pas de ce qui a, dans le cas présent, un intérêt moindre, comme par exemple le découplage au niveau de l'"interface utilisateur" pour faciliter l'écriture des tests.

Allons-y.

Stab : un langage pour découvrir C# quand on ne connait que java

Ce billet est issu de la concomitance de deux évènements :

  • je me pose la question "comment peut-on écrire quelque chose proche de C# mais qui tourne avec une JVM ?" et je découvre le langage "stab"
  • je lis ce billet "Sélection FooBarQix" où un même programme a été implémenté dans 13 langages différents ayant pour seul point commun la JVM.

Il ne m'en a pas fallu plus pour écrire un FooBarQix en stab.
Et je crois que ce programme a un intérêt pour montrer ce que l'on peut écrire quand on rajoute quelques éléments basiques de C# 3 à java 6.
Le code complet est dispo sur github.

J'ai fait au plus simple avec seulement deux fichiers.

Un fichier de tests qui utilise junit :

using java.lang;
using junit.framework;
 
public class TestFooBarQix : TestCase
{
  delegate void Action<T1,T2>(T1 t1, T2 t2);

  public void testFBQ()
  {
    Action<Integer,String> check = (src,dst) => { assertEquals(src+" => "+dst, dst, FooBarQix.from(src)); };
    check(1,"1");
    check(2,"2");
    check(3,"FooFoo");
    check(4,"4");
    check(5,"BarBar");
    check(6,"Foo");
    check(8,"8");
    check(9,"Foo");
    check(10,"Bar");
    check(11,"11");
    check(12,"Foo");
    check(13,"Foo");
    check(15,"FooBarBar");
    check(7,"QixQix");
    check(14,"Qix");
    check(16,"16");
    check(17,"Qix");
    check(18,"Foo");
    check(19,"19");
    check(20,"Bar");
    check(21,"FooQix");
  }
}

Et un fichier de code qui tire profit de quatre éléments majeurs de C# :

  • les lambda expressions
  • les générateurs
  • les types anonymes
  • LINQ (qui n'est presque rien de plus que la résultante des éléments précédents)
using java.lang;
using stab.query;
 
public static class FooBarQix
{
  public static String from(Integer numberToConvert)
  {
    var fbqDigits = sequence(
      new { Value=3, Text="Foo" },
      new { Value=5, Text="Bar" },
      new { Value=7, Text="Qix" }
    );

    Predicate<Integer> isNumberToConvertDivisibleBy = n => (numberToConvert % n == 0);
    var divisors = fbqDigits.where(x => isNumberToConvertDivisibleBy(x.Value));

    var digitsReplacement=digitsLeftToRight(numberToConvert).selectMany(digit => fbqDigits.where(x => x.Value==digit));

    var fbqConversions = divisors.concat(digitsReplacement);
 
    if( fbqConversions.count() == 0 )
      return Integer.toString(numberToConvert);
    else
      return fbqConversions.select(x => x.Text).aggregate((a,b)=>a+b);
  }

  static Iterable<Integer> digitsLeftToRight(Integer number)
  {
    return digitsRightToLeft(number).reverse();
  }

  static Iterable<Integer> digitsRightToLeft(Integer number)
  {
    while(number != 0)
    {
      yield return number % 10;
      number = number / 10;
    }
  }

  delegate Boolean Predicate<T>(T t);

  static Iterable<T> sequence<T>(params T[] items)
  {
    return Query.asIterable(items);
  }
}

Fluctuations d'une équipe agile

L'équipe entame une période un peu particulière. Pour quelques mois, elle va devoir prendre en charge une activité de support produit qui sort de son cadre habituel de développement logiciel. Cette activité est potentiellement chronophage (elle occupera facilement une personne à plein temps sur les quatre développeurs que compte l'équipe) et les attentes des clients sur les sorties à venir ne diminuent pas pour autant. Il y a un budget permettant de financer une personne supplémentaire pendant 9 mois.
Que faire ?

L'option "la personne supplémentaire assurera le support produit et on ne change rien d'autre" n'est pas envisageable : cette activité nécessite des connaissances spécifiques et une autonomie qui ne s'acquièrent pas en moins de 6 mois.
Par ailleurs, les membres de l'équipe ont à coeur de minimiser l'impact sur leur situation actuelle : "Notre métier c'est le développement logiciel. On n'assurera le support que parce qu'il faut bien que quelqu'un s'en charge". Et ils sont les seuls à pouvoir le faire.

L'équipe s'est réunie et a décidé de modifier son organisation pour faire face aux nouvelles contraintes :

  • A tour de rôle, un membre est désigné pour assurer la prise des appels téléphoniques. Chaque appel sera limité en durée afin que la perturbation induite ne soit pas supérieure aux interruptions habituelles internes à l'équipe.
  • Si une demande de support vient à prendre plus de temps, elle devient une tache de l'équipe et rejoint le tableau Kanban avec une classe de service dédiée. Un WIP dédié à cette classe permettra de maitriser le temps consacré au support.
  • Avec le budget fourni, un développeur supplémentaire rejoint l'équipe pendant 9 mois. L'usage généralisé du pair programming devrait permettre d'intégrer rapidement la personne. La présence de ce nouveau développeur garantira par ailleurs la continuité du travail en paires.

Reste une question: comment trouver le développeur supplémentaire ?
En faisant probablement appel à une SSII, mais comment déterminer la mission et les compétences recherchées ?

  • Les développements sont faits en C# mais la maitrise du langage n'est pas primordiale. Un développeur java ou C++ qui applique les principes essentiels et utilise les bonnes pratiques s'intègrerait plus facilement qu'un développeur qui sait avant tout cliquer dans MS Visual Studio.
  • La connaissance de l'environnement .NET ou de tel ou tel framework n'est pas non plus essentielle. L'architecture du produit est centrée sur le besoin à résoudre et est quasiment framework-agnostique.
  • L'équipe essaie dans la mesure du possible de coller aux valeurs et aux principes agiles mais un fraichement certifié CSM serait un peu déboussolé dans un contexte agile sans sprints, sans burdown chart, sans estimations et presque sans Scrum.

Bref, ce qu'il nous faudrait c'est un compagnon de route qui vienne pendant quelques mois vivre notre cheminement, nous aider à ne pas décevoir les attentes de nos clients, partager son réel savoir-faire et peut être apprendre quelques trucs au passage.
C'est mon idée d'un vrai développeur professionnel qui va de mission en mission. Je me demande s'il y a des SSII qui peuvent répondre à ce genre de demandes mais je ne vais probablement pas tarder à le découvrir...

Si quelqu'un qui passe par ici est intéressé, je suis facile à joindre.

Agile Tour Toulouse 2011 : jour J-2

Avec plus de 300 personnes déjà inscrites (les records des années précédentes vont être explosés), l'Agile Tour Toulouse 2011 sera probablement un grand succès.

J'aurai le plaisir d'y animer deux sessions. Je n'ose pas leur donner le nom de "présentations" car je n'y présenterai rien du tout.

La journée débutera par une keynote d'Alexandre Boutin qui donnera une place de choix à l'apprentissage par le jeu. Je crois que, plus ou moins consciemment, la présence de ce thème m'a amené à construire mes sessions comme des jeux.

Loin de moi la prétention d'en imaginer qui pourraient être utilisés de manière courante dans l'enseignement des méthodes agiles mais j'espère quand même que ce format donnera l'occasion à tous les participants d'apprendre quelques trucs et de s'en souvenir grace au format ludique.
Le matin, dans "si t'es pas SOLID, t'es pas agile", les participants se retrouveront sur scène à se faire des passes avec un ballon (ovale, actualité oblige !) pour dérouler l'exécution d'un programme. Le ballon représentera en quelque sorte l'instruction courante et toute la difficulté du jeu résidera dans le choix de la meilleure tactique pour répondre le plus efficacement possible aux attentes toujours changeantes d'un client.
En fin de journée, dans "quand je serai grand, je serai artisan logiciel", on jouera aux cartes. A partir du livre "Apprenticeship Patterns", j'ai fabriqué un jeu de 35 cartes, une par pattern. Aidés par ces cartes, les participants tenteront de répondre à la question que beaucoup de monde se pose et dont la réponse n'est pas toujours simple : comment devient-t-on un bon développeur de logiciels ?

D'autres jeux (des vrais, maintes fois éprouvés) seront aussi présents dans cette journée : le "marshmallow challenge" et le "sky castle game". Je n'y participerai pas mais je ne peux que les conseiller à tous ceux qui voudraient passer une journée animée loin des confortables fauteuils d'un amphithéatre.

Je ne sais pas encore exactement comment j'occuperai le reste de ma journée. L'association SigmaT, organisatrice de l'évènement y aura un stand où j'assurerai probablement un peu de permanence. Il y a quelques présentations que j'aimerais voir comme l'"histoire d'un transformation agile"Laurent Carbonnaux parlera de l'utilisation des méthodes agiles à une échelle rarement rencontrée en France. J'aimerais également parler de Domain Driven Design avec les toujours sympathiques et compétents bordelais d'Arpinum parce que c'est une approche que l'on commence à aborder dans mon équipe.

Quoi qu'il en soit, avec 4 sessions en parallèle sur les 5 créneaux de la journée, il y en aura pour tous les goûts et si vous lisez ce billet avant la date du 19 octobre 2011, il est encore temps de s'inscrire !

Faire des boucles sans "while" ni "for" pour les rendre plus lisibles et maintenables

Le B.A.BA de la programmation, celui que tout le monde, ou presque, commence par apprendre en débutant l'écriture d'algorithmes, ce sont les structures de contrôle en programmation impérative : les tests, les boucles, les sauts...
Mais, avec le temps, cela devient, de moins en moins vrai. Les instructions de sauts ont commencé à disparaitre des langages informatiques quand deux illustres néerlandais et suisse ont évoqué leur nocivité.
En ce qui concerne les structure de tests, il a fallu attendre l'avènement des langages orientés objet pour que le polymorphisme devienne "mainstream". Aujourd'hui, on peut ouvertement se revendiquer anti-if.

La seule structure qui résiste encore bien, ce sont les boucles. Mais la redécouverte de la programmation fonctionnelle à travers sa présence à dose plus ou moins grande dans des langages "en vogue" (python, ruby, javascript, C#, scala...) fait que le développeur lambda[1] peut commencer à entrevoir d'autres manières de décrire des opérations sur des ensembles d'éléments.

Je vais dans ce billet essayer de montrer ce que l'on peut faire en C# sur ce sujet.
Prenons un exemple simple : "afficher les carrés des entiers naturels inférieurs ou égaux à 10"

L'implémentation la plus courante ressemblera à une boucle sur des entiers au sein de laquelle seront mis en oeuvre le calcul et l'affichage adéquats :

for(var i=0; i<=10; i++)
  Console.WriteLine( i*i );

Ca fonctionne, mais ce n'est pas très explicite. L'énoncé du problème comporte 4 exigences distinctes et le code proposé ne permet pas de les expliciter et les implémenter indépendamment les unes des autres :

  1. afficher
  2. les carrés
  3. des entiers naturels
  4. inférieurs ou égaux à 10

Commençons par les points (3) et (4). Pour expliciter les éléments sur lesquels portent l'opération, on pourrait écrire quelque chose dans ce genre :

foreach(var n in Sequence.OfN().TakeWhile( n => n<=10 ))
  Console.WriteLine( n*n );

Sequence.OfN() n'est pas du code standard C#. On va supposer que cela retourne un IEnumerable<int> qui est l'ensemble des entiers naturels. On s'interessera plus tard à son implémentation.
Le TakeWhile( n => n<=10 ) est quelque chose qui devrait être désormais naturel à tous les développeurs C# qui connaissent LINQ et les lambda expressions.

On va maintenant expliciter le point (2) en remontant son calcul dans la définition de l'ensemble de nos éléments :

foreach(var n2 in Sequence.OfN().TakeWhile( n => n<=10 ).Select( n => n*n ))
  Console.WriteLine( n2 );

Pour en finir avec la boucle, il ne reste plus qu'à imaginer un moyen simple pour effectuer un affichage sur un ensemble d'éléments donné :

Sequence.OfN().TakeWhile( n => n<=10 ).Select( n => n*n ).Apply( n2 => { Console.WriteLine(n2); } );

Et voilà... Notre boucle s'est métamorphosée en la concaténation d'opérations indépendantes sur des suites d'éléments :

  1. afficher => Apply( n2 => { Console.WriteLine(n2); } )
  2. les carrés => Select( n => n*n )
  3. des entiers naturels => Sequence.OfN()
  4. inférieurs ou égaux à 10 => TakeWhile( n => n<=10 )

L'avantage d'une telle écriture, c'est que l'on n'est plus condamné à des algorithmes monolithiques où la modification d'une exigence a des impacts sur les autres. On a remplacé cela par des briques de lego que l'on peut facilement recombiner.

Passons maintenant à l'implémentation. Définissons d'abord quelques briques de base sur lesquelles viendront s'appuyer les autres :

  • la définition d'une suite initiale d'éléments IEnumerable<T>. Pour cela, on utilisera 2 méthodes :
    • IEnumerable<T> Of<T>(params T ts) pour une suite définie par la liste explicite de ses éléments
    • IEnumerable<T> Of<T>(Func<T> next) pour une suite définie par une fonction génératrice (chaque appel renvoie l'élément suivant de l'énumération)
  • les opérations de transformation d'un IEnumerable<T> vers un IEnumerable<U> : nul besoin d'écrire quoi que ce soit pour cela, il y a déjà de quoi faire avec le LINQ de base
  • une opération de terminaison void Now<T>(this IEnumerable<T> seq). Toutes les autres opérations étant à évaluation différée, il nous faut une opération qui force l'exécution.

Tout cela fait beaucoup de blabla pour finalement peu de code :

public static class Sequence
{
  public static IEnumerable<T> Of<T>(params T[] ts)
  {
    return ts;
  }

  public static IEnumerable<T> Of<T>(Func<T> next)
  {
    sendNext:
    yield return next();
    goto sendNext; // on aurait pu faire un while(true) mais le goto est plus fun, non ?
    // En tout cas, les codes CLR générés sont identiques.
  }

  public static void Now<T>(this IEnumerable<T> seq)
  {
    seq.Count(); // Demander le compte des éléments est un moyen simple pour forcer l'énumération
  }
}

A partir de là, implémenter notre Sequence.OfN() est une simple formalité. Il suffit d'initier un ensemble d'élément avec une fonction qui renvoie consécutivement les entiers naturels :

public static IEnumerable<int> OfN()
{
  var n=0;
  return Sequence.Of( () => n++ );
}

L'application d'une action sur chaque élément n'est guère plus compliquée :

public static IEnumerable<T> Apply<T>(this IEnumerable<T> seq, Action<T> action)
{
  return seq.Select( item => {action(item);return item;} );
}

Pour être tout à fait précis, le Apply étant défini à évaluation différée, notre exemple initial devrait se terminer par un Now pour forcer l'exécution :

Sequence.OfN().TakeWhile( n => n<=10 ).Select( n => n*n ).Apply( n2 => { Console.WriteLine(n2); } ).Now();

Passons maintenant à quelque chose d'un peu plus évolué. Les carrés des 10 premiers entiers, c'est sympa, mais la réalité est souvent plus complexe.
Prenons, par exemple, l'affichage des 20 premiers termes d'une suite d'entiers définie par récurrence :

var u = 1;
for(var i=0; i<20; i++)
{
  Console.WriteLine( u );
  u = 2*u+1;
}

Notre nouvelle écriture ressemblera à ça :

Sequence.Recurrence(1, x => 2*x+1).Take(20).Apply( n => { Console.WriteLine(n); } ).Now();

La méthode Recurrence est assez facilement définie à partir des autres méthodes d'initialisation :

public static IEnumerable<T> Recurrence<T>(T u0, Func<T,T> f)
{
  var u = u0;
  return Sequence.Of(u0).Concat( Sequence.Of( () => u=f(u) ) );
}

Notre collection ne pourrait pas être complète sans la star des algorithmes de boucles favoris des débutants : la suite de Fibonnacci.
En version "classique" :

var u = 1;
var v = 0;
for(var i=0; i<20; i++)
{
  Console.WriteLine( u );
  var w = u;
  u = u+v;
  v = w;
}

Et en version "moderne" :

Sequence.Fibonnaci().Take(20).Apply( n => { Console.WriteLine(n); } ).Now();

public static IEnumerable<int> Fibonnaci()
{
  return Sequence.Recurrence(new {fib=1,fib2=0}, x => new {fib=x.fib+x.fib2, fib2=x.fib}).Select(x => x.fib);
}

On remarquera au passage l'utilisation de types anonymes statiques.

Et pour finir, un petit exemple avec condition d'arrêt portant sur les élements manipulés avec un autre classique, la division euclidienne :

var a = 34;
var b = 5;

var q = 0;
var r = a;
while(r >= b)
{
  Console.WriteLine( "( q = {0}, r = {1} )", q ,r );
  q++;
  r -= b;
}

var quotientAndRemainder = Sequence
  .Recurrence(new{q=0,r=a}, x => new{ q = x.q+1, r = x.r-b })
  .Apply( x => { Console.WriteLine(x); } )
  .SkipWhile(x => x.r>=b)
  .First();

C'est fini...
J'espère que ce petit aperçu sur la manière d'écrire des boucles "autrement" donnera des envies d'exploration à ceux qui n'avaient jamais envisagé ces techniques d'écriture de code.
Comme d'habitude, les commentaires sont les bienvenus !

Notes

[1] Ce billet n'est pas destiné au développeur qui a SICP comme livre de chevet mais à un hypothétique développeur moyen tout comme on peut parler du "français moyen"

Agile Tour Toulouse 2011 : une journée pour les développeurs ?

L'été vient à peine de commencer mais c'est bientôt l'automne. Dans moins de 2 mois, l'Agile Tour posera ses bagages à Diagora Labège pour son édition toulousaine annuelle.
Les inscriptions sont d'ailleurs déjà ouvertes.

Pour ceux qui souhaiteraient apporter leur contribution au programme, c'est encore possible jusqu'au 15 septembre.
Pour ma part, je viens de déposer 2 propositions de sessions : "Si t'es pas SOLID, t'es pas agile" et "Quand je serai grand, je serai artisan logiciel".

La première est un atelier qui vise à découvrir ou approfondir l'importance des principes de conception dans toute organisation qui produit du logiciel de manière incrémentale et adaptative.
Pour cela, j'ai envie de surfer sur une approche que j'avais bien aimée lors du "Stub et Mock montent sur scène" de l'an dernier : des personnes qui montent sur scène pour jouer le rôle de composants logiciels.
L'idée ici, c'est que l'animateur joue le rôle du client et laisse l'équipe présente sur scène implémenter le logiciel "humain". Par demandes successives du client, il devrait être possible de redécouvrir rapidement l'intérêt de plusieurs principes SOLID et enchainer sur une discussion concernant leur apport à une démarche "agile".

La deuxième proposition serait plutôt dans le genre "débat". Les conférences sur les méthodes agiles proposent souvent des retours d'expérience orienté "projet" et, à mon goût, trop peu de retours d'expérience orientés "carrière dans le développement logiciel". L'idée serait donc d'amener les membres de l'assistance qui le souhaitent à évoquer des points essentiels de leur parcours.
Pour cela, les "Apprenticeship Patterns" formalisés par D.Hoover et A.Oshineye me semblent être un support intéressant. Malgré l'existence d'une version en ligne, ce sont des travaux, à mon avis, trop peu connus dans la communauté agile francophone.

Voilà... Ce deux propositions sont encore un peu des "work in progress" comme on dit et toutes les suggestions, voire les collaborations, sont les bienvenues.

Mon sentiment général, après avoir vécu plusieurs journées "agile tour" les années passées, est que ce genre de conférences s'adresse encore trop à des profils "chef de projets" -présents ou en devenir- et autres wannabe guru de l'organisation. De manière générale, je n'y vois pas un contenu suffisamment intéressant pour des développeurs.
Pour autant, je ne crois pas qu'un format "on amène nos laptops et on code" soit plus adapté. Une telle journée me semble plus intéressante pour ses opportunités de communication et d'échange. Pour écrire du code hors du contexte de travail habituel, il existe d'autre formats.

Mon espoir pour cette année est que la journée agile tour toulouse soit aussi pour les développeurs.

Tranches d'agilité

L'agilité, ça ne se vend toujours pas en boite de 12 mais ça peut se raconter en tranches.

C'est l'exercice auquel je vais me prêter cette semaine sur Slice of IT :

Slices of IT est une expérience : raconter nos tranches vie au sein d'un projet pour prendre du recul et tenter de s'améliorer, à la manière de G. Weinberg. Pour que ce soit facile, l'écriture d'une tranche en timebox de 15 minutes. Raconter les événements marquant et l'impression générale de la journée suffit à obtenir l'effet attendu.

Avant de se mettre à écrire, il faut apprendre à lire

read_and_write.pngConnaissez-vous des gens qui écrivent sans jamais avoir appris à lire ? L'idée est assez saugrenue.

Et pourtant...

Il ne faut pas chercher plus loin que dans l'industrie du logiciel.
On ne compte plus les professionnels qui écrivent du code à longueur de journée mais n'ont souvent jamais appris, ou simplement pris le temps de lire.

A ceux qui m'opposeront que écrire du code n'a rien à voir avec écrire de la littérature, je suggèrerai d'aller lire le dernier billet d'Antoine[1].

A moins d'être un génie qui jongle avec les mots comme d'autres jonglent avec un ballon ou avec des notes de musique dès leur plus jeune age, il y a un passage obligé pour arriver à écrire des choses de manière intelligible : la lecture.


Ma fille ainée a eu 10 ans hier. Il y a trois jours, elle m'a montré avec fierté une rédaction qui vient de lui valoir les félicitations de sa maitresse. Une histoire sur le cirque. Ce qui m'a le plus impressionné, c'est son application à dépeindre les personnages tant du point de vue physique qu'à travers leurs traits de caractère. Je lui ai demandé comment lui était venue l'idée d'écrire de cette manière là. Le plus simplement du monde, elle m'a répondu "c'est comme ça qu'ils font dans les livres".

Je ne connais pas de parents ayant pour préoccupation majeure la capacité de leur enfant de 10 ans à inventer et rédiger une histoire. Moi le premier. Si jamais cela arrive, on le reçoit comme un cadeau. Par contre, la plupart des parents que je connais seraient vraiment inquiets si, au même age, leur progéniture était incapable de déchiffrer une phrase.

Il faut croire que, dans le monde du logiciel, tout marche à l'envers. On s'intéresse beaucoup à la capacité qu'a un développeur d'écrire un programme. On s'intéresse plus rarement à sa capacité à le lire.
Toutes les Beaucoup de[2] formations sont axées sur l'écriture de code après quelques rudiments d'orthographe et de syntaxe, voire quelques notions de grammaire pour les plus chanceux.
L'employeur d'un développeur n'en demande pas plus. Il semble se complaire dans une sorte de "On s'en fout qu'il sache lire. On s'en fout que ce soit bien écrit. Tout ce qui compte c'est que le résultat plaise au client".

C'est en tout cas ce que j'ai souvent ressenti et le je m'enfoutisme vient parfois des développeurs eux-mêmes. Pour moi qui ai découvert ce qu'était un logiciel en recopiant inlassablement des pages entières de listing à la "grande époque" d'Hebdogiciel, c'est tout simplement inconcevable.
On ne peut bien écrire du code que lorsque l'on a passé du temps à lire du code, bon ou mauvais, à s'en imprégner, à en découvrir les forces et les faiblesses.

Le code, c'est une production humaine. C'est l'oeuvre d'un humain qui se parle à lui même et aux autres avant même de parler à la machine. Lorsqu'il écrit du code, un développeur devrait se demander comment celui-ci va être lu avant de se demander comment il va être compris par la machine.

Je laisserai le mot de la fin à un tweet de Jason Gorman.
Ecrire du code qu'un ordinateur comprend, c'est de la science. Ecrire du code que les autres programmeurs comprennent, c'est de l'art.

Notes

[1] sans lequel ce billet-ci n'existerait pas

[2] cf commentaire de Tok'

Pour en finir avec les Stubs et les Mocks

stubmockgear.pngLes termes de "Stub" et "Mock" sont aujourd'hui utilisés de manière assez courante[1] quand il s'agit de parler de tests unitaires, mais, faute de véritable référence, tout le monde n'utilise pas ces mots avec la même signification.

Une affirmation assez courante, consiste à dire "les stubs c'est pour faire des vérifications d'état, les mocks c'est pour faire des vérifications de comportement". Cette affirmation ne me convient pas car elle simplifie à outrance la diversité des techniques d'écriture de test.

Je préfère, de loin, les définitions proposées par Gerard Meszaros dans ses xUnit Patterns :

  • Un stub, c'est une doublure qui remplace un objet réel en fournissant des entrées indirectes à l'objet à tester
  • Un mock, c'est une doublure qui remplace un objet réel en vérifiant les sorties indirectes de l'objet à tester

Et la liste ne serait pas complète si on ne rajoutait pas l'espion :

  • Un spy, c'est une doublure qui remplace un objet réel en enregistrant les sorties indirectes de l'objet à tester pour vérification ultérieure par le test

C'est cette nomenclature que j'ai utilisée l'an dernier pour mon atelier "Stub et Mock montent sur scène" car elle me parait apporter la précision nécessaire à la compréhension des mécanismes de test.
Quand on parle des doublures, ces objets qui remplacent les objets réels lors des tests, les caractéristiques à identifier (celles qui auront le plus d'impact sur la stratégie d'implémentaiton du test) sont la mise en oeuvre des entrées indirectes et des sorties indirectes.

Voyons un exemple avec l'implémentation en Ruby[2] d'une partie de "Stub et Mock montent sur scène". On a un thermomètre qui sait calculer la température maximale observée à partir de relevés effectués sur un capteur et afficher cette température maximale sur l'écran qui lui est rattaché :

class Thermometre

  def initialize(capteur,ecran)
    @ecran = ecran
    @capteur = capteur
    @temperature_maximale = -273.15
  end

  def affiche_temperature_maximale
    temperature_courante = @capteur.lis_temperature_courante
    if temperature_courante > @temperature_maximale
      @temperature_maximale = temperature_courante
    end
    @ecran.ecris(@temperature_maximale)
  end

end

Une stratégie possible de test est la vérification de l'état de notre thermomètre. Puisqu'il mémorise la température maximale observée après chaque mesure, il suffit de vérifier la valeur mémorisée. Pour cela, on prend un stub pour doubler le capteur, un dummy pour doubler l'écran et on rend accessible l'état de notre thermomètre :

def test_avec_un_stub
  capteur = flexmock("capteur")
  capteur.should_receive(:lis_temperature_courante).and_return(21, 18, 25)

  ecran = Object.new
  def ecran.ecris(texte)
  end

  thermometre = Thermometre.new(capteur, ecran)
  def thermometre.temperature_maximale
    @temperature_maximale
  end

  thermometre.affiche_temperature_maximale
  assert_equal( 21, thermometre.temperature_maximale )
  thermometre.affiche_temperature_maximale
  assert_equal( 21, thermometre.temperature_maximale )
  thermometre.affiche_temperature_maximale
  assert_equal( 25, thermometre.temperature_maximale )
end

Une autre stratégie possible est de vérifier le comportement de notre thermomètre. Pour cela, on va toujours utiliser un stub pour les entrées indirectes fournies par le capteur mais on va prendre un mock pour vérifier ce que le thermomètre envoie à l'écran :

def test_avec_un_stub_et_un_mock
  capteur = flexmock("capteur")
  capteur.should_receive(:lis_temperature_courante).and_return(21, 18, 25)

  ecran = flexmock("ecran")
  ecran.should_receive(:ecris).with(21).times(2).should_receive(:ecris).with(25).once.ordered

  thermometre = Thermometre.new(capteur, ecran)
  3.times { thermometre.affiche_temperature_maximale }
end

Enfin, une troisième stratégie pourrait consister à vérifier les conséquences du comportment de notre thermomètre. On garde notre stub pour les entrées indirectes du capteur et on prend un spy pour enregistrer les conséquences des envois de notre thermomètre à l'écran :

def test_avec_un_stub_et_un_spy
  capteur = flexmock("capteur")
  capteur.should_receive(:lis_temperature_courante).and_return(21, 18, 25)

  ecran = Object.new
  def ecran.ecris(texte)
    @texte = texte
  end
  def ecran.affiche
    @texte
  end

  thermometre = Thermometre.new(capteur, ecran)
  thermometre.affiche_temperature_maximale
  assert_equal( 21, ecran.affiche )
  thermometre.affiche_temperature_maximale
  assert_equal( 21, ecran.affiche )
  thermometre.affiche_temperature_maximale
  assert_equal( 25, ecran.affiche )
end

Quelle est, de manière générale, la meilleure stratégie ?
Il n'y a pas de réponse unique à cette question. Cela dépend fortement de ce que l'élément que l'on est en train de tester.

Pour ce qui est de cet exemple précis, on peut toutefois avancer quelques arguments.

La simple vérification de l'état du thermomètre n'est peut être pas le meilleur choix. Imaginons que, lors d'une modification de code, un développeur supprime la ligne @ecran.ecris(@temperature_maximale) de notre thermomètre. On aurait donc ce code là :

def affiche_temperature_maximale
  temperature_courante = @capteur.lis_temperature_courante
  if temperature_courante > @temperature_maximale
    @temperature_maximale = temperature_courante
  end
end

Et le test de vérification d'état serait toujours vert malgré l'absence totale d'affichage à l'écran...

La vérification de comportement nous aurait signalé le problème :

Failure:
test_avec_un_stub_et_un_mock:
in mock 'ecran': method 'ecris(21)' called incorrect number of times. <2> expected but was <0>.

La vérification des conséquences du comportement en aurait fait de même :

Failure:
test_avec_un_stub_et_un_spy:
<21> expected but was .

Dans un exemple aussi simple, le cas est peu probable diront certains[3] mais dans du code un peu plus compliqué, ça arrive plus souvent qu'on ne le croit. D'autres diront[4] que la pratique du TDD n'aurait jamais pu conduire à cette situation : le code d'envoi à l'écran n'aurait jamais été écrit sans test qui ne passe pas.
Quoi qu'il en soit, un critère de qualité d'un test qu'il faut toujours avoir à l'esprit : un bon test passe au rouge si le code devient mauvais.

La vérification du comportement de notre thermomètre pose d'autres questions : si on vérifie le comportement de manière trop stricte, n'arrive-t-on pas à de la sur-spécification inutile ? Par exemple, un développeur qui passe sur notre code pourrait envisager une optimisation "Pas besoin d'écrire sur l'écran à chaque fois : il suffit de le faire lorsque le maximum change" et faire la modification suivante :

def affiche_temperature_maximale
  temperature_courante = @capteur.lis_temperature_courante
  if temperature_courante > @temperature_maximale
    @temperature_maximale = temperature_courante
    @ecran.ecris(@temperature_maximale)
  end
end

Et là, il se retrouve avec un test rouge pour une raison inutile qui l'oblige à aller modifier le test.

Failure:
test_avec_un_stub_et_un_mock:
in mock 'ecran': method 'ecris(21)' called incorrect number of times. <2> expected but was <1>.

Les deux autres stratégies de test n'ont pas ce problème là.

Donc, dans cet exemple précis[5], le couple stub+spy est, à mon avis, le bon choix. Le stub permet de simuler la lecture sur un capteur et le spy permet de vérifier les conséquences du comportement de l'élément testé.

Notes

[1] Google est d'accord

[2] Si un spécialiste Ruby passe par ici, qu'il n'hésite pas à pointer mes erreurs de débutant. J'ai choisi d'utiliser ce langage dans mes exemples pour son aspect dynamique qui simplifie grandement l'écriture des doublures

[3] et ils n'auront pas forcément raison

[4] et ils auront tout à fait raison

[5] N'en faisons pas une généralité

Bye Bye Scrum

R.I.P. ScrumJ'arrête Scrum.

Ce n'est pas un billet de premier avril qui aurait un peu de retard : j'arrête volontairement et définitivement l'utilisation de Scrum.

Il y a 4 ans, j'annonçais que, après quelques années de bricolages méthodologiques agiles, j'avais pris une approche relativement formelle pour passer à Scrum. Compte-tenu de mon contexte d'alors, je pense que c'était la bonne chose à faire.

Au fil du temps, l'équipe a gagné en compétence sur les pratiques agiles. De début 2007 à mi 2010, cinq releases de notre principal produit se sont succédées et, dans les derniers temps, les sprints se suivaient sans le moindre souci à gérer. En juin 2010, nous avons démarré une nouvelle ligne de produit avec un changement technologique assez conséquent.
D'un point de vue gestion de projet, le premier impact visible (et attendu) fut le changement de vélocité. Sur le produit précédent, le graphe de vélocité ressemblait à une mer d'huile. Sur les premiers mois du nouveau produit, il donnait plutôt dans la coupe d'une étape pyrénéenne du tour de France. Cela n'avait rien d'affolant : le contexte avait changé et il fallait quelques mois pour stabiliser la pertinence des estimations.
Ce n'est devenu plus problématique qu'en novembre lors de l'approche de la première release. La dernière itération avant la date butoir fut vécue comme une sorte de fin de projet où il fallait absolument livrer toutes les fonctionnalités prévues.

Je ne sais si j'ai été influencé par mes lectures d'alors[1] ou par quelques présentations intéressantes sur Kanban lors de l'agile tour mais j'ai pris -sans en parler à quiconque- une décision.
Puisque les fins d'itérations sont si mal vécues, c'est qu'elles n'ont pas que du bon. J'allais donc désormais m'abstenir d'envoyer des invitations pour la moindre réunion relative à une itération. Si un backlog ou une revue de sprint venait à manquer à quelqu'un, il ou elle le ferait probablement savoir...

Depuis quatre mois, nous n'avons plus formalisé la moindre itération.

A-t-on des problèmes pour définir le travail à réaliser ? Non. Il y a en permanence 2 ou 3 éléments du backlog de produit qui sont en cours de réalisation. Dès qu'un est terminé, celui qui suit par ordre de priorité prend sa place et est immédiatement découpé en taches.
A-t-on des problèmes pour rendre visible l'avancement du travail ? Non. Dès qu'un élément du backlog est terminé, il est présent dans le build du produit, lequel est immédiatement disponible pour qui voudrait l'essayer.

Progressivement, notre mode de fonctionnement passe d'un cadencement par les itérations à un flux qui s'écoule de manière plus régulière. La métaphore du "sprint" était en fait très bien choisie. On avait juste oublié qu'il est plus facile de gérer son effort dans une course de fond que dans une succession de sprints.

Bien sûr, tout cela n'est pas aussi simple. La disparition des itérations entraine des contraintes plus importantes sur certains aspects du processus.
L'intégration continue est un de ces aspects : la nécessité d'un build à jour et en état livrable est quasi-permanente. Heureusement, les efforts consentis sur la mise en oeuvre sans concession d'un développement piloté par les tests commencent à porter leurs fruits.
La planification des estimations doit, elle aussi, s'adapter au flux. Il n'y a plus de "début de sprint" où on peut estimer tout ce qui aurait pu entrer dans le backlog de produit pendant le déroulement du sprint précédent.

La définition de la vélocité et, plus généralement, du plan de release est probablement le changement le plus significatif.
Pour la vélocité, on a pour l'instant fait très simple. Quand on a des itérations de 4 semaines, la vélocité, mesurée en fin d'itération, c'est la somme des points des éléments du backlog terminés au cours des 4 dernières semaines. Et quand on n'a plus d'itérations ? Et bien, rien ne nous empêche de continuer à prendre la même mesure !!! On a une vélocité calculée sur une fenêtre glissante qui change chaque jour. Si le flux des éléments de backlog s'écoule régulièrement à travers l'équipe de développement, cela ne change rien par rapport à une mesure de vélocité par itérations.
D'ailleurs, il faut bien comprendre que la vélocité ne sert, au final, qu'à une seule chose : proposer une date de release la plus réaliste possible. Quand on a des itérations, la taille du backlog de release divisée par la vélocité nous donne le nombre d'itérations restant à effectuer pour terminer la release. Quand on n'a plus d'itérations, on divise toujours la taille du backlog de release par la vélocité, puis on multiplie par la taille, en jours, de la fenêtre de calcul de la vélocité et on obtient une approximation guère moins exacte du nombre de jours restant avant la release...

Finalement, la suppression des itérations et le passage à un processus de flux n'a rien de bien compliqué. Le point à surveiller le plus crucial est le maintien du flux. On doit en permanence garder à l'esprit qu'il vaut toujours mieux achever ce qui est déjà commencé que de commencer autre chose. Concrètement, cela veut dire que si un développeur termine quelque chose, il devrait, avant de prendre une nouvelle tache, toujours se demander s'il ne peut pas aider un collègue à terminer une tache en cours.
Ce n'est pas grand chose mais je crois que c'est une notion fondamentale. Quand on fonctionne par sprints, on a tendance à remplir un backlog de sprint avec des taches et à ne plus trop se préoccuper de la valeur de chacune de ces taches. Pourtant, à un instant donné, une tache qui permet de terminer la réalisation d'une histoire utilisateur a énormément plus de valeur qu'une autre tache qui ne fait que démarrer la réalisation d'une autre histoire.
Le but d'une méthode agile n'est-il pas de fournir, dans un délai le plus court possible, de la valeur pour les utilisateurs tout en conservant un rythme viable sur une longue durée ?

Il y a là, je pense, matière à réflexion pour toute équipe pratiquant le développement itératif.
En ce qui me concerne, le pas est franchi. Scrum, c'est fini. Désormais, tout se passera dans le flux.

Notes

[1] Je ne lis pas souvent mai, en novembre dernier, j'ai notamment apprécié "Lean Management" de Pierre Pezziardi et "Who Moved My Cheese ?" de Spencer Johnson

Le référentiel des pratiques agiles : vers la formalisation du "framework agile" ?

Il y a quelques jours semaines[1], l'institut agile a publié la 1ère version du référentiel des concepts, pratiques et compétences agiles.

Par les temps qui courent, de plus en plus de monde s'intéresse aux méthodes agiles et le nombre d'experts de la chose s'adapte bien évidemment à la demande qui en résulte. Dans un tel contexte, un référentiel a un intérêt immédiat : clarifier les définitions du vocabulaire agile pour que tout le monde parle de la même chose avec les mêmes mots.

Et même en ayant une bonne expérience des méthodes agiles, je crois que l'on sous-estime le besoin de définitions partagées et non ambigües.
Un exemple. Récemment, lors d'une discussion sur la liste "xp-france", j'ai posé une question et j'ai eu la réponse suivante :

Les équipes que j'ai vu faire de l'agile utilisent le concept d'itération pour répondre à cette question. A intervalles réguliers, ils vérifient avec les clients (parfois nommés aussi "utilisateurs") que le produit correspond à ce qu'ils sont prêts à payés.

La réponse ne correspondait pas à ce que j'attendais[2].
En temps normal, je n'aurais pas creusé plus loin mais, là, je suis allé voir la définition d'itération dans le référentiel :

Une itération au sens Agile est une "boite de temps" ou "timebox" dont la durée:
*varie d'un projet à l'autre, de 1 semaine à 4 semaines, rarement plus
*est en principe fixe sur la durée du projet

Une boîte de temps ou "timebox" est une période fixe pendant laquelle on cherche à exécuter le plus efficacement possible une ou plusieurs tâches.

Et il se trouve que, depuis quelques mois, mon équipe n'utilise plus de boites de temps[3] ce qui ne nous empêche pas de demander très souvent à notre chef de produit si ce que l'on fait correspond bien à ce qu'il a en tête.
Cette possibilité de vérification n'est donc pas issue des boites de temps.
En fait, ce qui permet à un client de vérifier très souvent que le produit correspond à ses attentes, c'est le développement incrémental

Pour s'en convaincre, il suffit d'aller regarder la définition dans le référentiel :

Le développement incrémental consiste à réaliser successivement des éléments fonctionnels utilisables, plutôt que des composants techniques.

J'apprécie énormément cette précision sur les termes utilisés.

Le seul reproche que je pourrais faire au référentiel dans sa version actuelle, c'est la frontière qui me parait encore un peu ambigüe entre les "concepts" et les "pratiques".
Pour ma part, j'aurais plutôt vu "développement itératif" et "développement incrémental" dans les pratiques et non pas dans les concepts.

Ceci étant dit, le potentiel de ce référentiel me parait important.
Je ne sais pas si c'est un objectif souhaitable pour ce référentiel mais j'y verrais bien une plus grande articulation des pratiques entre elles, façon pattern language.

Le canevas de description des pratiques est ce que l'on attendrait classiquement d'une fiche de pattern[4]
Par exemple, chaque pratique contient une section "bénéfices attendus" qui la met en perspective en tant que solution possible à un problème que l'on pourrait rencontrer.

Un exemple assez représentatif : intégration continue

* le principal intérêt de l'intégration continue est de réduire la durée, l'effort et la douleur provoquée par chaque intégration, l'expérience suggérant qu'il existe un "cercle vicieux" dans le sens inverse: plus les intégrations sont espacées, plus elles sont difficiles, et plus (en réaction à la douleur provoquée) on a tendance à les espacer
* l'intégration continue démultiplie le bénéfice d'une batterie étendue de tests unitaires: elle permet de détecter au plus tôt les défauts n'apparaissant qu'à l'intégration et par conséquent de minimiser leurs conséquences et les risques associés aux défauts d'intégration
* l'intégration continue permet de tirer le meilleur parti du développement incrémental: des questions comme l'installation et le déploiment du produit ne sont pas laissées de côté jusqu'à la fin du projet mais résolues dès le départ dans le cadre de la pratique

Le petit plus qui permettrait d'atteindre le niveau langage de patterns, c'est une formalisation des liens entre les différentes pratiques. Dans l'exemple précédent quelques filiations apparaissent : pour qui implémente déjà la pratique "tests unitaires automatisés" ou la pratique "développement incrémental", l'intégration continue est une suite logique.

Ce langage de pratiques agiles, que l'on peut également qualifier de "framework agile" me semble inévitable.
L'agilité en est arrivée à un point où tout le monde essaie d'adapter les méthodes existantes à des contextes très variés (avec plus ou moins de savoir faire).
Une bonne boite à outils serait certainement très utiles à tous les apprentis sorciers qui conçoivent des schemas d'organisation agile après deux jours de formation Scrum et la lecture d'un ou deux livres.
Et aux autres, aussi, bien sûr.

Notes

[1] le temps passe trop vite

[2] mais c'est une autre histoire : j'aurais dû formuler différemment ma question

[3] on est passé à un mode de fonctionnement "en flux" mais ça aussi c'est une autre histoire

[4] je n'arrive pas à me faire à la moindre francisation de ce mot : patron, motif, modèle...

Gloubi-Boulga Agile

De temps à autre, j'aime bien me balader en ligne dans les endroits où les gens parlent de méthodes agiles. Ca permet de garder un certain contact avec les préoccupations actuelles de ces personnes.
Ce soir, sur le groupe linkedin consacré au French Scrum User Group, je suis tombé sur une discussion qui m'a donné l'impression de débarquer sur une autre planète. Si c'est ça les méthodes agiles aujourd'hui, il serait grand temps pour moi d'aller voir ce qui se passe ailleurs. Un ailleurs où on serait plus attaché à faire de bons logiciels qu'aux organisations d'entreprises où on fait entrer de "l'agilité" au marteau piqueur (voire au bulldozer ?)

Ou devrais-je me consoler en me disant que ces gens là ne parlent pas vraiment de méthodes agiles car, en fin de compte, ils ne parlent que de Scrum ?

Pour ceux qui n'auraient pas l'immense privilège d'être inscrits sur linkedin et d'avoir accès à ce groupe, voici la discussion en question:


Intervenant A
Dans les grosses organisations, des Responsables qualité et PMO interviennent en soutien des Chefs de projet. Dans un monde agile, que deviennent-ils ?

- page 1 de 5