Le kata du « Bowling Game »

Une technique d'entraînement au développement

Salut !

Aujourd’hui je voudrais vous montrer une technique d’entraînement au développement que j’ai récemment adoptée. Il s’agit du code kata.

Cet exercice qui prend moins de 30 minutes par jour peut vous permettre de progresser dans de nombreux aspects du développement. On peut utiliser les katas pour :

  • S’entraîner au Test-Driven Development (TDD) et au Simple Design,
  • Apprendre à utiliser efficacement ses outils de travail,
  • Mieux expliquer aux autres la façon dont on raisonne, par exemple en parlant à une peluche sur notre bureau,
  • Apprendre à connaître quelqu’un au travers de sa façon de penser, et donc à mieux coopérer avec cette personne, en réalisant un kata "en multi-joueur", c’est-à-dire en une courte séance pair programming.

Son principe est de prendre un problème, peu importe si dont on en connait déjà la solution, et s’entraîner à le résoudre en réalisant chaque geste délibérément de bout en bout. En essayant d’optimiser toutes les micro-décisions et les gestes que nous réalisons pour converger vers cette solution. Pour ce faire, je vais vous montrer un kata très populaire de Robert C. Martin, qui consiste à calculer le score d’une partie de bowling américain.

Je vais vous montrer comment je réalise ce kata en Go en vous détaillant toutes les questions que je me pose au fur et à mesure, mais on s’en fiche du langage et des outils: vous n’avez pas du tout besoin de connaître Go pour suivre, juste de savoir déjà programmer dans un langage quelconque. Vous pouvez réaliser cet exercice dans n’importe quelles conditions, de préférence les conditions dans lesquelles vous désirez vous améliorer, en Javascript sous iOS, en C# sous Windows, en Rust sous Linux…

En l’occurrence, ce kata est aussi pour moi l’occasion de vous montrer comment on raisonne en TDD et pourquoi c’est avantageux.

Énoncé du problème

L’objectif de cet exercice est d’implémenter un composant qui calcule le score d’une partie de bowling américain.

Une partie se déroule en 10 tours. À chaque tour, le joueur dispose de deux essais pour faire tomber 10 quilles disposées en triangle. Le score de base est le nombre de quilles que le joueur a réussi à faire tomber pendant le tour.

Lorsque le joueur arrive à faire tomber les dix quilles en deux lancers, cela s’appelle un spare. Le score d’un spare est de 10, auquel on ajoute le nombre de quilles tombées au lancer suivant.

Lorsque le joueur arrive à faire tomber les dix quilles en un seul lancer, cela s’appelle un strike. Le score d’un strike est de 10, auquel on ajoute le total de quilles tombées aux deux lancers suivants.

Lors du dernier tour :

  • si le joueur réalise un spare, il dispose d’un lancer bonus pour permettre de calculer son score ;
  • si le joueur réalise un strike, il dispose de deux lancers bonus pour permettre de calculer son score.

Cela signifie qu’une partie de bowling se termine en 21 lancers maximum (10 spare d’affilée).

Le composant que l’on cherche à implémenter n’est pas responsable de valider le nombre de coups joués dans une partie ni que les coups sont "légaux". On part de l’hypothèse que cette validation est réalisée avant d’interagir avec ce composant, et donc qu’il sera systématiquement appelé avec une entrée bien formée.

Le véritable but de cet exercice est de minimiser le nombre d’efforts à concéder et de questions que nous avons besoin de nous poser pour parvenir au résultat le plus simple possible qui satisfasse cet énoncé.

C’est pour cette raison que l’on se moque de connaître la solution et qu’il est bon de répéter ce kata quotidiennement (mais pas plus d’une fois par jour) : le plus important est d’être capable d’y parvenir en réalisant délibérément chaque geste, de manière à mémoriser les questions que l’on se pose plutôt que les réponses que nous leur apportons.

Écrire le premier test

Le principe du TDD est de boucler rapidement sur les trois étapes suivantes :

  1. RED : On écrit le minimum de tests possible pour que les tests échouent,
  2. GREEN : On ajoute le code le plus simple possible pour faire passer tous les tests,
  3. REFACTOR : C’est ici que l’on prend soin du code et que l’on peut factoriser ou renommer les choses, les tests garantissent que nous ne casserons pas le travail que nous avons déjà fait.

Mais la toute première chose à faire est de créer un environnement (ou "projet") pour travailler.

Dans mon cas, je vais réaliser ce kata en Go, et je vais donc taper les lignes suivantes :

$ rm -rf ./bowling && mkdir bowling && cd bowling
$ go mod init bowling

Puis je vais ouvrir mon éditeur (vim) dans lequel je vais créer un fichier dans lequel je vais écrire mon premier test.

Le rôle des tous premiers tests est hyper important. Ils servent à réfléchir à l’interface publique du code que nous allons écrire.

Le premier élément d’interface auquel nous ayons à faire est le constructeur. C’est pourquoi j’ai écrit le test suivant dans un fichier bowling_test.go.

package bowling

import "testing"

func TestNewGame(t *testing.T) {
    game := NewGame()
    if game == nil {
        t.Fatal("NewGame() returned nil")
    }
}

Évidemment, ce test ne compile pas puisqu’il fait appel à la fonction NewGame() qui n’est pas définie. Nous pouvons d’ailleurs le vérifier dans la console (dans la réalité j’utilise la commande :GoTest dans Vim) :

$ go test
# bowling [bowling.test]
./bowling_test.go:6:10: undefined: NewGame
FAIL    bowling [build failed]

Nous sommes donc à l’état RED. Notre mission est d’écrire juste ce qu’il faut de code pour que ce test passe au vert. Je vais écrire ce code dans le fichier bowling.go que voici :

package bowling

type Game struct{}

func NewGame() *Game {
    return new(Game)
}

Je relance les tests. Ils passent avec succès : nous sommes GREEN. C’est l’heure de "refactoriser", sauf qu’il n’y a rien à refactoriser pour l’instant, donc notre but est d’écrire un nouveau test qui échoue.

Dans notre cas, nous voulons calculer un score de bowling. Nous savons qu’une partie de bowling se compose d’un nombre variable de lancer et nous savons que nous ne calculerons le score que de parties finies. Nous savons aussi que nous ne passerons aucune donnée invalide à notre composant.

La partie la plus simple possible au bowling est une partie nulle (on envoie la boule dans la gouttière à chaque coup). C’est pourquoi j’ai rajouté le test suivant dans bowling_test.go pour la simuler.

package bowling

import "testing"

func TestNewGame(t *testing.T) {
    game := NewGame()
    if game == nil {
        t.Fatal("NewGame() returned nil")
    }
}

func TestGutterGame(t *testing.T) {
    game := NewGame()
    if game == nil {
        t.Fatal("NewGame() returned nil")
    }
    for i := 0; i < 20; i++ {
        game.Roll(0)
    }
    score := game.Score()
    if score != 0 {
        t.Fatalf("game.Score() should be 0, got %d.", score)
    }
}

Évidemment, ce test ne compile pas, car il utilise 2 méthodes qui ne sont pas définies :

  • game.Roll(int) qui sert à enregistrer un lancer,
  • game.Score() int qui sert à récupérer le score final.

Bien qu’il ne compile pas, ce test m’a poussé à réfléchir aux fonctions que je vais écrire. C’est l’API la plus simple possible.

Nous voici donc à nouveau à l’état RED. Il suffit de définir ces méthodes pour que le test passe :

package bowling

type Game struct{}

func NewGame() *Game {
    return new(Game)
}

func (g *Game) Roll(pins int) {}

func (g *Game) Score() int {
    return 0
}

Je relance les tests. Ils passent : nous sommes GREEN.

Regardons nos tests : le test TestNewGame est entièrement contenu dans le test TestGutterGame. Autrement dit, si ce dernier échoue, TestNewGame ne nous donnera aucune information supplémentaire. Ce test est donc redondant : il faut le supprimer. Ce que l’on peut faire, par contre, c’est factoriser ce code dans une fonction newGame afin de ne pas avoir à récrire cette vérification à chaque test que nous écrirons :

func TestGutterGame(t *testing.T) {
    game := newGame(t)
    for i := 0; i < 20; i++ {
        game.Roll(0)
    }
    score := game.Score()
    if score != 0 {
        t.Fatalf("game.Score() should be 0, got %d.", score)
    }
}

func newGame(t *testing.T) *Game {
    game := NewGame()
    if game == nil {
        t.Fatal("NewGame() returned nil")
    }
    return game
}

Du reste, il n’y a pas de code à bouger, mais il est idiomatique en Go de documenter toutes les fonctions et types publics (dont le nom commence par une majuscule) que nous créons, en écrivant pour cela des commentaires spéciaux, sans quoi golint nous fera les gros yeux. Faisons ça dès maintenant pour ne pas avoir à y repenser plus tard.

package bowling

// A Game models a single bowling game and allows to compute its end score.
type Game struct{}

// NewGame creates a new Game.
func NewGame() *Game {
    return new(Game)
}

// Roll records the number of pins that went down during a single roll.
func (g *Game) Roll(pins int) {}

// Score computes the score of the game.
func (g *Game) Score() int {
    return 0
}

Et voilà, le projet est démarré.

Calculer le score d'une partie "normale"

Il est temps d’écrire un nouveau test.

Jusqu’à présent, nous avons réalisé une partie où on ne tombait aucune quille à chaque coup. Essayons maintenant une partie où nous tombons une seule quille à chaque fois. Cette partie devrait être composée de 20 lancers, et son score final devrait être 20.

Pour ce faire, j’ai copié-collé notre premier test puis je l’ai modifié pour donner ceci :

package bowling

import "testing"

func TestGutterGame(t *testing.T) {
    game := newGame(t)
    for i := 0; i < 20; i++ {
        game.Roll(0)
    }
    score := game.Score()
    if score != 0 {
        t.Fatalf("game.Score() should be 0, got %d.", score)
    }
}

// XXX copy-pasta
func TestAllOnes(t *testing.T) {
    game := newGame(t)
    for i := 0; i < 20; i++ {
        game.Roll(1)
    }
    score := game.Score()
    if score != 20 {
        t.Fatalf("game.Score() should be 20, got %d.", score)
    }
}

Vous connaissez l’adage : « si tu copies-colles quelque chose, c’est sûrement que tu as quelque chose à refactoriser ».

Sauf que l’on n’en est pas à l’étape où on refactorise les choses. C’est pour cette raison que j’ai laissé ce commentaire // XXX copy-pasta. Mon éditeur va faire ressortir ce XXX avec un fond coloré pour attirer mon attention et ne pas l’oublier quand il sera temps de refactoriser.

En attendant, les tests échouent, évidemment :

bowling_test.go|30| game.Score() should be 20, got 0.

Il va donc falloir faire passer ce test. La solution la plus simple est d’accumuler le score dans un attribut de la structure Game :

package bowling

// A Game models a single bowling game and allows to compute its end score.
type Game struct {
    score int
}

// NewGame creates a new Game.
func NewGame() *Game {
    return new(Game)
}

// Roll records the number of pins that went down during a single roll.
func (g *Game) Roll(pins int) {
    g.score += pins
}

// Score computes the score of the game.
func (g *Game) Score() int {
    return g.score
}

Les tests passent, nous sommes GREEN. C’est maintenant que l’on refactorise nos tests.

Le but d’une refactorisation des tests n’est pas de rendre le code le plus court possible, mais de rendre chaque test le plus lisible possible.

Pour cette raison, factorisons les trois étapes de chaque test :

  • initialisation (newGame(t) fait déjà le travail),
  • exercice du code (lancer game.Roll() dans une boucle),
  • vérification (comparaison de game.Score() avec la valeur attendue).

Voici ce que cela donne chez moi :

package bowling

import "testing"

func TestGutterGame(t *testing.T) {
    game := newGame(t)
    rollMany(game, 0, 20)
    checkScore(t, game, 0)
}

func TestAllOnes(t *testing.T) {
    game := newGame(t)
    rollMany(game, 1, 20)
    checkScore(t, game, 20)
}

func newGame(t *testing.T) *Game {
    game := NewGame()
    if game == nil {
        t.Fatal("NewGame() returned nil")
    }
    return game
}

func rollMany(game *Game, pins, count int) {
    for i := 0; i < count; i++ {
        game.Roll(pins)
    }
}

func checkScore(t *testing.T, game *Game, expected int) {
    score := game.Score()
    if score != expected {
        t.Fatalf("Score should be %d, got %d.", expected, score)
    }
}

Gérer les "spares"

Ça y est, vous êtes bien échauffés ? :)

Il faut maintenant que notre code soit capable de compter des spares. Pour cela, écrivons un test qui réalise un spare (6, puis 4 quilles) et un lancer non-nul (3 quilles), puis une partie nulle le reste du temps. Le score du spare devra être (6 + 4) + 3 = 13, et le core total 13 + 3 = 16.

func TestOneSpare(t *testing.T) {
    game := newGame(t)
    game.Roll(6)
    game.Roll(4)
    game.Roll(3)
    rollMany(game, 0, 17)
    checkScore(t, game, 16)
}

Et bien sûr ce test échoue avec le message : Score should be 16, got 13.

C’est maintenant que nous allons devoir nous gratter la tête. Commençons par jeter un oeil un peu plus critique à nos méthodes :

// Roll records the number of pins that went down during a single roll.
func (g *Game) Roll(pins int) {
    g.score += pins
}

// Score computes the score of the game.
func (g *Game) Score() int {
    return g.score
}

Nous avons :

  • une méthode dont le nom et la documentation indiquent qu’elle enregistrent les coups, alors qu’elle calcule le score,
  • une méthode dont le nom et la documentation indiquent qu’elle calcule le score, alors qu’elle ne fait rien si ce n’est retourner le score déjà calculé.

Ceci est indicatif d’un problème de design. Nous allons donc avoir besoin de faire des changements relativement profonds dans notre code, alors commençons par commenter le test TestOneSpare, afin de retourner à l’état GREEN précédent, et pouvoir changer de design tout en nous assurant que notre code fonctionne au moins aussi bien qu’avant.

Au lieu de ne garder que le score final en mémoire, nous devrions garder chaque coup, puis calculer le score final dans la méthode. En Go, on peut faire cela avec une slice d’entiers, dont la capacité maximale, 21 éléments, est connue dès le départ.

En modifiant mon code ainsi, le code passe à nouveau tous les tests (sauf TestOneSpare, oui) :

package bowling

// A Game models a single bowling game and allows to compute its end score.
type Game struct {
    rolls []int
}

// NewGame creates a new Game.
func NewGame() *Game {
    return &Game{make([]int, 0, 21)}
}

// Roll records the number of pins that went down during a single roll.
func (g *Game) Roll(pins int) {
    g.rolls = append(g.rolls, pins)
}

// Score computes the score of the game.
func (g *Game) Score() (score int) {
    for _, roll := range g.rolls {
        score += roll
    }
    return
}

Je peux maintenant décommenter TestOneSpare et revenir à l’état RED.

Pour gérer les spares, maintenant, nous avons besoin de compter le score de la même façon que nous comptons manuellement les points au bowling, en considérant la partie comme 10 tours ayant chacun un nombre variable de lancers.

Voici comment je passe GREEN :

// Score computes the score of the game.
func (g *Game) Score() (score int) {
    var i int
    for turn := 0; turn < 10; turn++ {
        if g.rolls[i]+g.rolls[i+1] == 10 { // spare
            score += g.rolls[i] + g.rolls[i+1] + g.rolls[i+2]
        } else { // regular
            score += g.rolls[i] + g.rolls[i+1]
        }
        i += 2
    }
    return
}

Autrement dit, à chaque tour, je regarde si le tour est un spare ou non. S’il l’est, il compte comme la somme des deux lancers du tour courant et du prochain lancer, sinon, il compte normalement comme la somme des quilles tombées en deux coups.

Admettons que ce code n’est pas très lisible. Ça tombe bien, parce que c’est le moment de le refactoriser. D’abord, cette variable i n’est pas super bien nommée parce qu’on ne sait pas vraiment à quoi elle fait référence. Elle est utilisée pour être un indice dans le tableau de lancers, je me propose donc de la renommer rollIndex.

Ensuite, ces calculs polluent un peu la compréhension du code. Plutôt que d’expliquer que nous comptons des spares ou des coups réguliers dans des commentaires, faisons du code qui se documente tout seul en extrayant ces calculs dans des méthodes dont le nom est explicite. Voici ce que ça donne.

// Score computes the score of the game.
func (g *Game) Score() (score int) {
    var rollIndex int
    for turn := 0; turn < 10; turn++ {
        if g.isSpare(rollIndex) {
            score += g.countSpare(rollIndex)
        } else {
            score += g.countRegular(rollIndex)
        }
        rollIndex += 2
    }
    return
}

func (g *Game) isSpare(rollIndex int) bool {
    return g.rolls[rollIndex]+g.rolls[rollIndex+1] == 10
}

func (g *Game) countSpare(rollIndex int) int {
    return g.rolls[rollIndex] + g.rolls[rollIndex+1] + g.rolls[rollIndex+2]
}

func (g *Game) countRegular(rollIndex int) int {
    return g.rolls[rollIndex] + g.rolls[rollIndex+1]
}

C’est tout de même plus facile à suivre, vous ne trouvez pas ?

Essayons un autre test avec les spares, maintenant, en faisant tomber 5 quilles à chaque lancer. Chaque tour va être un spare, et le dernier tour sera composé de 3 lancers. Le score final devrait être de 10 * (10 + 5), soit 150.

func TestAllFives(t *testing.T) {
    game := newGame(t)
    rollMany(game, 5, 21)
    checkScore(t, game, 150)
}

Et… ce test passe tout seul, donc on en a fini avec les spares ! :)

Gérer les "strikes"

Nous entamons la dernière partie du kata. Il faut maintenant que nous soyons capables de prendre en compte les strikes. Et pour cela nous allons… ?

Écrire un nouveau test, bien sûr ! :D

Nous allons réaliser un strike, puis tomber successivement 4 et 3 quilles au prochain tour, puis complètement rater les 8 tours (16 lancers) suivants. Le score du strike devrait être 17, le score total de la partie devrait donner 24.

func TestOneStrike(t *testing.T) {
    game := newGame(t)
    game.Roll(10)
    game.Roll(4)
    game.Roll(3)
    rollMany(game, 0, 16)
    checkScore(t, game, 24)
}

Et ce test… échoue avec un débordement d’indice : panic: runtime error: index out of range [19] with length 19.

Ce n’est pas une grande surprise : un strike est un tour qui se joue en un lancer. Notre code ne sait pas gérer ce genre de tours. Apprenons-lui :

// Score computes the score of the game.
func (g *Game) Score() (score int) {
    var rollIndex int
    for turn := 0; turn < 10; turn++ {
        if g.rolls[rollIndex] == 10 {
            score += g.rolls[rollIndex] + g.rolls[rollIndex+1] + g.rolls[rollIndex+2]
            rollIndex++
        } else if g.isSpare(rollIndex) {
            score += g.countSpare(rollIndex)
            rollIndex += 2
        } else {
            score += g.countRegular(rollIndex)
            rollIndex += 2
        }
    }
    return
}

Nous sommes GREEN à nouveau. Procédons à la refacto :

  • Comme pour les spares, isolons les calculs dans des méthodes aux noms explicites,
  • En Go spécifiquement, cette suite de if {} else if {} else peut se récrire avec un switch qui sera plus lisible.
// Score computes the score of the game.
func (g *Game) Score() (score int) {
    var rollIndex int
    for turn := 0; turn < 10; turn++ {
        switch {
        case g.isStrike(rollIndex):
            score += g.countStrike(rollIndex)
            rollIndex++
        case g.isSpare(rollIndex):
            score += g.countSpare(rollIndex)
            rollIndex += 2
        default:
            score += g.countRegular(rollIndex)
            rollIndex += 2
        }
    }
    return
}

func (g *Game) isStrike(rollIndex int) bool {
    return g.rolls[rollIndex] == 10
}

func (g *Game) countStrike(rollIndex int) int {
    return g.rolls[rollIndex] + g.rolls[rollIndex+1] + g.rolls[rollIndex+2]
}

Voilà, un beau code bien propre. Nous pouvons maintenant écrire un dernier test, qui compte ce qui se passe lorsque l’on réalise une partie parfaite, c’est-à-dire, 10 strikes de suite, puis deux lancers "bonus" parfaits. Le score final d’une partie parfaite est 10 * (10 + 10 + 10) = 300.

func TestPerfectGame(t *testing.T) {
    game := newGame(t)
    rollMany(game, 10, 12)
    checkScore(t, game, 300)
}

Et… ce test passe avec succès, donc nous n’avons plus rien à faire. Mission accomplie.


Et voilà comment on aboutit à un code, simple, lisible et couvert de tests à 100%.

Ce que je vous recommande grandement de faire, c’est d’apprendre à réaliser ce kata dans votre langage de prédilection. Lorsque vous répéterez ce kata, ne vous contentez pas de faire du "par coeur" : réfléchissez à chaque étape, essayez peut-être de prendre une autre décision et de dérouler le reste du kata pour voir où cela vous mène.

Il est recommandé de travailler régulièrement sur un même kata avant d’en apprendre et d’en pratiquer un autre, car ce n’est pas la réalisation de cet exercice qui nous sert à progresser, mais toutes les questions que l’on se pose entre deux exécutions du même kata.

30 commentaires

Évidemment, ce test ne compile pas, car il utilise 2 méthodes qui ne sont pas définies :

  • game.Roll(int) qui sert à enregistrer un lancer,
  • game.Score() int qui sert à récupérer le score final.

Bien qu’il ne compile pas, ce test m’a poussé à réfléchir aux fonctions que je vais écrire. C’est l’API la plus simple possible.

Je suis un peu dubitatif, qu’est-ce qui t’a poussé à choisir cette API avec une struct et une méthode Roll ? Vu les prérequis de ne gérer que des jeux entiers et bien formés, je ne comprends pas le besoin d’abstraire l’enregistrement de chaque roll isolé.

Je me serais contenté d’implémenter un truc du genre pub fn score(rolls: &[u32]) -> u32 en Rust, soit func Score(rolls []int) int en Go si je me plante pas dans la notation des types. C’est l’API la plus simple que je puisse imaginer (et une transcription directe de l’énoncé, en fait). Même dans un contexte réaliste, ce serait un choix pas mauvais : suivant les besoins de l’application, on peut caler cette fonction dans l’implémentation d’une struct, dans un trait, la garder libre, modifier les types d’entrée et de sortie, etc.

+3 -0

C’est pas faux. On pourrait aussi utiliser une fonction. Cela dit ce composant à le mérite de garder toute son implémentation privée. On ne force pas l’utilisateur à accumuler les scores dans une slice pour nous les passer en masse, on abstrait cela pour lui. C’est un choix. C’est celui que fait 'Uncle Bob' Martin dans sa démonstration de ce kata (en Java), et je t’avoue franchement que j’en étais pas encore arrivé à me poser cette question-là. Par contre j’ai acquis le réflexe de me poser cette question à ce moment-là dans la vraie vie.

Il faudrait que je déroule à nouveau ce kata en faisant le choix d’une fonction Score unique, et voir ce que ça donne.

J’essaye et je te dis. :)

+0 -0

En fait @adri1 tu as carrément raison. Le code qui en résulte est beaucoup plus simple:

Le code:

package bowling

// Score returns the score of a bowling game.
// The input is assumed to be the succession of rolls for a valid game of
// american bowling.
func Score(rolls []int) (score int) {
    var rollIndex int
    for turn := 0; turn < 10; turn++ {
        if isStrike(rolls, rollIndex) {
            score += countStrike(rolls, rollIndex)
            rollIndex++
        } else if isSpare(rolls, rollIndex) {
            score += countSpare(rolls, rollIndex)
            rollIndex += 2
        } else {
            score += countRegular(rolls, rollIndex)
            rollIndex += 2
        }
    }
    return
}

func isStrike(rolls []int, i int) bool {
    return rolls[i] == 10
}

func isSpare(rolls []int, i int) bool {
    return rolls[i]+rolls[i+1] == 10
}

func countStrike(rolls []int, i int) int {
    return rolls[i] + rolls[i+1] + rolls[i+2]
}

func countSpare(rolls []int, i int) int {
    return rolls[i] + rolls[i+1] + rolls[i+2]
}

func countRegular(rolls []int, i int) int {
    return rolls[i] + rolls[i+1]
}

Les tests :

package bowling

import "testing"

func TestScore(t *testing.T) {
    cases := []struct {
        name     string
        input    []int
        expected int
    }{
        {
            name:     "gutter game",
            input:    rollMany(0, 20),
            expected: 0,
        },
        {
            name:     "all ones",
            input:    rollMany(1, 20),
            expected: 20,
        },
        {
            name:     "one spare",
            input:    append([]int{6, 4, 3}, rollMany(0, 17)...),
            expected: 16,
        },
        {
            name:     "one strike",
            input:    append([]int{10, 4, 3}, rollMany(0, 16)...),
            expected: 24,
        },
        {
            name:     "all spares",
            input:    rollMany(5, 21),
            expected: 150,
        },
        {
            name:     "perfect game",
            input:    rollMany(10, 12),
            expected: 300,
        },
    }

    for _, c := range cases {
        score := Score(c.input)
        if score != c.expected {
            t.Fatalf(
                "%s: expected score %d, got %d.",
                c.name, c.expected, score,
            )
        }
    }
}

func rollMany(value int, count int) []int {
    result := make([]int, count)
    for i := range result {
        result[i] = value
    }
    return result
}

J’en tire que ça m’a fait converger très rapidement vers un style de tests plus idiomatique en Go. Je remarque que j’ai quand même eu besoin d’écrire des fonctions TestGutterGame et TestAllOnes séparées avant de décider de factoriser les tests. Je trouve le résultat plus agréable à lire.

Je pense que je vais opter pour ce scénario pendant les prochains jours.

+0 -0

Je viens de finir de lire ton billet, et je me pose des questions. Tout d’abord, je ne suis pas du tout dévelopeur informatique, mes esxcuses si je rate des choses evidentes. Je les mets sous forme de liste, pour faciliter une réponse :

  1. L’un des buts du kata est d’optimiser les micro-décision. Mais le choix des décisions, de part le choix des tests, n’est pas optimisé par la méthode, seulement par l’expérience du dev, non ? Est-ce que la méthode a pour but de limiter les effets de bord non couverts ?
  2. En première impression, il me semble que si la méthode peut aider et même faire gagner du temps quand le problème gagne en complexité, elle fait perdre beaucoup de temps dans les premières étapes, quand tout est encore simple. Je me trompe ? Il est tout à fait possible aussi que ce ne soit pas le but de la méthode et que ma question passe à coté.
  3. J’ai l’impression que l’un des bénéfices principaux est d’avoir un code qui est testé, ce qui limite la dette technique (cf ton billet précédent). Mais ce n’est pas directement dans ta liste de 4 points du début (peut-être indirectement dans le premier point). N’est-ce pas un des points utiles ?

Merci !

L’un des buts du kata est d’optimiser les micro-décision. Mais le choix des décisions, de part le choix des tests, n’est pas optimisé par la méthode, seulement par l’expérience du dev, non ? Est-ce que la méthode a pour but de limiter les effets de bord non couverts ?

Alors le but du kata, c’est juste de nous fournir un cadre contrôlé dans lequel on peut expérimenter les décisions justement. Comme je viens de le faire en répondant à @adri1 : "tiens et si tu avais fait ça ? — OK, on essaye et on compare". C’est un exercice qui se code rapidement, et on peut mesurer l’impact de chaque décision sur la suite du raisonnement.

En première impression, il me semble que si la méthode peut aider et même faire gagner du temps quand le problème gagne en complexité, elle fait perdre beaucoup de temps dans les premières étapes, quand tout est encore simple. Je me trompe ? Il est tout à fait possible aussi que ce ne soit pas le but de la méthode et que ma question passe à coté.

TDD, spécifiquement, fait gagner du temps tout le temps. Les gens qui pratiquent TDD sont capables d’exécuter ce genre de katas en convergeant plus rapidement avec que sans les tests.

Tout ce code de test que l’on a l’impression de perdre du temps à écrire au début nous permet d’avoir dès le début l’assurance que l’on a construit quelque chose de solide. Si on veut changer le code, les tests nous permettent de le faire sans risquer de casser le travail que l’on a déjà fait. C’est un confort qui permet d’être plus courageux en essayant plus de trucs.

J’ai l’impression que l’un des bénéfices principaux est d’avoir un code qui est testé, ce qui limite la dette technique (cf ton billet précédent). Mais ce n’est pas directement dans ta liste de 4 points du début (peut-être indirectement dans le premier point). N’est-ce pas un des points utiles ?

Le TDD permet d’avoir un code testé, oui. Le kata, pas forcément. Le kata, c’est juste l’exercice que l’on répète une fois de temps en temps, ou que l’on peut utiliser pour comparer les idées, les méthodes, les façons de penser, etc.

+1 -0

TDD, spécifiquement, fait gagner du temps tout le temps. Les gens qui pratiquent TDD sont capables d’exécuter ce genre de katas en convergeant plus rapidement avec que sans les tests.

Oui et non, à cause de mon métier je ne suis pas très fan du TDD qui me semble potentiellement adapté à un contexte mais pas à tous. Le test automatique c’est important et doit être fait, mais le TDD ne laisse à priori aucune marge de manoeuvre pour doser la pertinence de chaque test.

Par exemple quand tu développes pour un matériel particulier, utiliser le TDD devient vite pénible. Tu peux mocker tout ce que tu veux, mais c’est difficile et vite pénible. Puis techniquement tu dois écrire le test, puis l’implémentation, cela signifie que le mock doit être écrit avant le reste, mais comment s’assurer que le mock correspond au matériel avant de le tester ? Tu peux faire des allers retours entre mock et matériels très vite. Cela peut vite devenir assez laborieux, surtout à la fin si tu réalises que le périphérique a un comportement un peu différent que celui qu’on avait compris à la base.

Alors qu de manière classique, tu joues avec le périphérique avec du code un peu prototype / jetable, puis quand tu as une bonne vue de la chose tu fais une implémentation propre que tu peux valider sur le matériel et avec un mock (si le mock n’est pas trop chiant à écrire, sinon ça se discute au cas par cas).

Le TDD permet d’avoir un code testé, oui. Le kata, pas forcément. Le kata, c’est juste l’exercice que l’on répète une fois de temps en temps, ou que l’on peut utiliser pour comparer les idées, les méthodes, les façons de penser, etc.

Ce qui est bizarre c’est que ton article je le vois plus comme une promotion du TDD que du kata, car tu fais les deux à la fois et tu fais la promotion du TDD.

Même si je comprends la différence, j’ai un peu peur que quelqu’un qui n’a jamais vu de TDD de sa vie confonde les deux aspects de ton article. Par ailleurs la logique de l’article mise grandement sur l’aspect TDD.

Sur Discord tu avais parlé de notions qui me semblaient sympa que tu n’as pas abordé ici. Par exemple en répétant l’exercice tu as pu mettre le doigt sur une opération courante à effectuer qui nécessitait d’en apprendre plus sur son éditeur de texte ou IDE pour savoir le refaire plus vite ou plus simplement. Je pensais d’ailleurs que le but du kata était plutôt de ce côté là, se confronter à des problématiques régulières pour que face à elles on ait des automatismes pour aller plus vite.

Bien sûr ça fonctionne pour la conception ou la méthode de développement (ce que tu as mis en avant ici) mais je pensais naïvement que tu l’avais fait pour une approche plus terre à terre comme mieux maitriser son environnement, son éditeur de code, etc.

+3 -0

Ce qui est bizarre c’est que ton article je le vois plus comme une promotion du TDD que du kata, car tu fais les deux à la fois et tu fais la promotion du TDD.

[…]

Je pensais d’ailleurs que le but du kata était plutôt de ce côté là, se confronter à des problématiques régulières pour que face à elles on ait des automatismes pour aller plus vite.

Évidemment que c’est le but de l’exercice. C’est ce que je dis juste au-dessus : le kata nous fournit un cadre contrôlé pour penser à toutes ces choses-là. Par contre, à un moment, je peux faire la promotion (ou pas) de tout ce que je veux, pour comprendre ce que nous apporte un kata dans le détail, il faut prendre 30 minutes maximum et le faire.

J’en tire beaucoup plus que ce que je n’en dis dans le billet, mais c’est parce que je tâche de montrer le kata ici. J’aurais pu détailler à l’écrit chaque combinaison de touches que je fais délibérément dans Vim, mais le billet serait 5 fois plus long et illisible, et le texte passerait quand même à côté de toutes les questions que je me suis posé pour en arriver à ces mouvements. Ça serait comme écrire une nouvelle complète qui détaille chaque micro-étape de "je me suis levé et préparé pour partir au travail".

Par contre ce que tu dis de TDD m’incite à penser que tu parles dans un contexte où TDD n’a aucun sens. Si tu n’as pas une boucle de feedback immédiate à chaque modification entre test et code de producion, alors ça n’a pas de sens de faire du TDD. Ce que j’affirme c’est que les cas de domaines métiers qui ont de telles contraintes sont plutôt rares dans la pratique. C’est fait pour acquérir un confort : si ça n’est pas confortable dans la pratique alors ça ne sert à rien.

Typiquement là tu parles de mocker un composant matériel dont le design n’est pas figé. Ce sont deux tâches distinctes. D’ailleurs, ce que tu décris ne semble déjà plus être du test unitaire (ce qui est concerné par TDD).

Je pourrais aussi te trouver des tas de cas dans mon projet au boulot où TDD ne s’applique pas/n’a pas de sens. Par exemple les fonctions main de chacun de mes binaires, le chargement de la config, ou encore écrire du test pour du code autogénéré… Le fait est que ce genre de code est fonctionnellement une toute petite portion de mon projet. Tout le reste est testable unitairement à condition d’être designé pour l’être. Et c’est plutôt cette question qui m’intéresse ("comment on écrit du code testable") que les considérations générales sur les contextes où le TDD ne s’applique pas car pas conçu pour s’y appliquer.

Bien sûr ça fonctionne pour la conception ou la méthode de développement (ce que tu as mis en avant ici) mais je pensais naïvement que tu l’avais fait pour une approche plus terre à terre comme mieux maitriser son environnement, son éditeur de code, etc.

On s’en fout un peu de savoir pourquoi je m’entraîne sur un kata, non ? Ce qui compte c’est qu’ils permettent de travailler tout ça. Ce qui compte c’est qu’ils attirent l’attention sur ce qui ne s’acquiert dans aucun bouquin, sur la pratique quotidienne en fournissant un cadre que des exercices dont on considère qu’on les a résolus "une bonne fois pour toutes" sont incapables de reproduire.

+0 -0

Évidemment que c’est le but de l’exercice. C’est ce que je dis juste au-dessus : le kata nous fournit un cadre contrôlé pour penser à toutes ces choses-là. Par contre, à un moment, je peux faire la promotion (ou pas) de tout ce que je veux, pour comprendre ce que nous apporte un kata dans le détail, il faut prendre 30 minutes maximum et le faire.

Note, je ne te reproche pas de parler du TDD. Tu fais en effet ce que tu veux. Je disais juste que si quelqu’un n’avait pas déjà vu ce qu’était un TDD par le passé, en particulier un débutant, il pouvait facilement confondre les deux aspects entre TDD et kata.

Ça serait comme écrire une nouvelle complète qui détaille chaque micro-étape de "je me suis levé et préparé pour partir au travail".

Je comprends tout à fait, mon idée était plus de le dire en guise de complément ou d’ouverture. Pour montrer concrètement la portée de ce que tu as présenté de manière concrète.

On s’en fout un peu de savoir pourquoi je m’entraîne sur un kata, non ? Ce qui compte c’est qu’ils permettent de travailler tout ça. Ce qui compte c’est qu’ils attirent l’attention sur ce qui ne s’acquiert dans aucun bouquin, sur la pratique quotidienne en fournissant un cadre que des exercices dont on considère qu’on les a résolus "une bonne fois pour toutes" sont incapables de reproduire.

Je ne pense pas que cela soit inintéressant que tu donnes un aspect personnel dans ce genre de billets. Dans un tutos ce serait en effet différent.

Je veux dire, il y a une raison pour laquelle tu t’es mis dedans, et il y a des éléments que tu retires que tu juges bénéfique. Quand tu expliques ton avis sur le kata, savoir pourquoi tu le fais et ce que tu en retires c’est une information cool. Tu es un développeur expérimenté, avec un avis et des motivations qui ont du sens, c’est inspirant pour un débutant. Et perso j’aime bien. :)

La démarche est intéressante, après perso je déteste faire des actions répétitives comme ça donc je ne suis ps sûr que la méthode me convienne, mais je note cela dans un coin de ma tête pour plus tard au cas où.

+0 -0

Si j’ai bien compris, le concept de kata c’est de refaire tous les jours le même exercice ?

En lisant le billet, je comprends assez mal l’intérêt de la chose. Quelles sont les compétences que l’on développe avec un exercice de ce style ? Je comprends l’intérêt de répéter des exercices identiques en boucles dans le cas des arts martiaux (parce que la mémoire musculaire et les réflexes, principalement), mais dans le cas du développement informatique j’avoue que la pertinence m’échappe un peu.

Naïvement je penserais en première approche que l’exercice serait intéressant en se forçant à implémenter une solution différente à chaque fois, ou en variant les exercices (en restant dans les cas qui peuvent s’implémenter complètement en une demi-heure). Avec ce genre de fonctionnement, on développe sa logique de résolution de problèmes et on augmente la quantité de « cas déjà connus » qui peuvent nous aider au quotidien. D’autre part, de mon point de vue la difficulté habituelle du développement informatique, c’est pas tellement l’utilisation des outils (qui est ce qu’il me semble que l’on développe avec les katas tels que présentés ici), mais la conception d’une solution au problème posé (qui nécessite de faire varier les problèmes ou au moins les contraintes pour s’entrainer à un éventail de solutions différentes).

Mais j’ai l’impression que ça n’est pas le fonctionnement des katas, et donc que quelque chose m’échappe.

Je dirais que cela sert beaucoup au débutant, pour apprendre la syntaxe de son langage, apprendre les rudiments pour avoir certains réflexes initiaux.

Pour un développeur plus expérimenté, en cas de découverte d’un autre langage (pour les mêmes raisons) ou pour mieux évaluer l’efficacité de son environnement de travail.

Après je ne dirais pas qu’il faut faire cela pendant des mois non plus.

+0 -0

Il n’est pas question de refaire le même déroulé à l’identique. J’ai eu l’occasion de montrer dans les tous premiers commentaires qu’on pouvait tout à fait refaire un même kata en faisant bouger une décision de base, typiquement…

En lisant le billet, je comprends assez mal l’intérêt de la chose. Quelles sont les compétences que l’on développe avec un exercice de ce style ?

Eh bien ça sert précisément à optimiser toutes les étapes qui nous permettent de converger vers la solution d’un problème.

  • Se poser les bonnes questions au bon moment (quelle interface ? qu’est-ce que ça devrait retourner ?),
  • Décider au bon moment quand ajouter tel ou tel test,
  • Optimiser ses propres routines pour ne pas se perdre (dans l’exemple de TDD : "je suis en REFACTOR ou en RED là ?"),
  • Optimiser son utilisation de ses outils : certes ce n’est pas "la difficulté habituelle au développement informatique", mais c’est quand même bon de s’assurer que l’on sait utiliser correctement son IDE, son outil de tests, de savoir que telle fonctionnalité existe (et donc avoir un cadre dans lequel se poser la question hors développement "au boulot"), et surtout de faire rentrer tous ces micro-gestes comme autant de réflexes dans nos mains,
  • Lorsque l’on est deux derrière un écran, fluidifier la conversation, s’habituer à coopérer, apprendre à connaître les faiblesses de notre collègue pour les compenser, etc.

en variant les exercices (en restant dans les cas qui peuvent s’implémenter complètement en une demi-heure) […] on développe sa logique de résolution de problèmes

Comment peut-on prétendre développer sa logique de résolution de problèmes si on s’entraîne pas à résoudre un même problème en améliorant délibérément d’une fois sur l’autre, les points qui nous ont fait perdre du temps ? En expérimentant pour mesurer l’impact d’une décision sur le reste du développement ? Comment comparer des approches différentes ou identifier une progression, si on ne base pas notre comparaison dans un cadre égal par ailleurs : celui d’un même exercice ?

Tu sembles considérer la résolution de problèmes comme le produit d’une démarche empirique pure : "je croise ce cas, ça m’apprend à le gérer, je saurai la prochaine fois". À te lire on ne dirait pas qu’il s’agit de développer une méthode, juste de "voir des cas", pourtant c’est de ça qu’il s’agit ici. Si tu ne connais pas la solution à un exercice, tu te concentres sur le fait de trouver la solution, pas sur le fait de l’aborder de façon à être sûr de converger vers une bonne solution.

La résolution d’un problème de développement est une succession d’étapes. Est-ce que toutes ces étapes sont bien conscientes et délibérées chez toi, au point d’être capable de décrire ce que tu penses et comment tu réfléchis à voix haute en même temps que tu le fais ?

Si j’en crois mon expérience depuis que je me suis essayé aux katas, il y a fort à parier que non. Maîtriser chaque raisonnement et chaque décision, ça demande d’acquérir justement tous ces petits réflexes, et personnellement les premières fois que je me suis essayé à ce kata à voix haute, j’ai énormément bafouillé, alors que l’exercice en lui-même est pourtant facile.

Par contre au bout de plusieurs jours :

  • J’ai systématisé la façon dont je réalise certaines actions dans mon environnement de développement,
  • J’ai systématisé une routine pour aborder n’importe quel problème, routine que j’améliore au fil du temps mais que je pratique consciemment,
  • J’ai eu l’occasion de remettre des tas de réflexes que j’avais auparavant en question, dans un cadre où je peux les juger objectivement.
  • J’ai l’occasion de m’entraîner à réprimer ces vieux réflexes qui sont contre-productifs.

Edit: Je ne dis pas qu’il faut le faire "tous les jours" comme une prescription de médecin. Quand on maîtrise un kata on peut se contenter de le refaire une fois de temps en temps pour le raffraîchir et voir si on a progressé ces dernières semaines/mois…

+0 -0

OK merci, je pense que je vois mieux ce que tu veux dire et l’utilité du truc.

Après, tu « réponds » à beaucoup de choses qui ne sont que des interprétations erronées de ce que j’ai écrit (les trois paragraphes de « Comment peut-on prétendre… » jusqu’à « au point d’être capable de décrire ce que tu penses et comment tu réfléchis à voix haute en même temps que tu le fais ? »).

En gros : « Je ne comprends pas ton propos » est différent de « Je remets en cause ton propos », et j’ai l’impression à tes réponses que tu es un peu à cran là-dessus ces derniers temps.

Ah non, non, je t’assure qu’il n’y a rien de défensif dans ma dernière réponse. Désolé si je l’ai formulé plus sèche que j’aurais voulu.

Le "comment peut-on prétendre" peut effectivement être lu comme un "comment oses-tu" alors qu’il est beaucoup plus terre à terre que ça. Et le reste ("au point d’être capable de"), était surtout une façon de t’amener à constater qu’il y a des tas d’éléments dans notre pratique que l’on croît maîtriser de base alors que généralement, on n’y fait pas si attention que ça (et donc on ne sait pas si on les maîtrise vraiment).

Bref, désolé si ma réponse donne l’impression que je suis à cran. :)

+0 -0

En fait, je pense que ce serait une bonne idée d’expliquer un vieux réflexe que ça m’a aidé à perdre : faire un kata en se posant toutes les questions "du début", ça aide à rester KISS dans la vraie vie.

On a tendance à traîner notre expérience un peu comme un bagage : quand je démarrais un nouveau projet/programme, je découpais automatiquement tout en fonctions ou en structures en me basant sur les trucs similaires que j’avais écrits avant. Ou encore je rajoutais des dépendances externes sans y réfléchir (par exemple zap pour les logs ou testify pour les tests, parce que ça marche bien et que je ne leur connais aucun inconvénient).

Depuis j’ai repris l’habitude de tout redémarrer par le début. En ne réutilisant les vieilles idées que si il s’avère que le projet en a besoin, mais pas avant de m’être demandé ce que je suis vraiment en train de résoudre comme problème et de quoi j’ai besoin.

En gros, ça m’aide à reprendre un raisonnement en commençant au plus simple. Et au boulot je sens que je suis beaucoup moins tenté de prendre ces raccourcis maintenant. En tout cas je me pose vachement plus la question, et ça me permet d’aller au plus simple au départ, ce qui est une excellente chose.

+0 -0

Bonsoir,

Merci pour cette présentation de TDD. Elle répond aux questions que j’ai posées dans les commentaires de ton précédent billet. J’ai du mal à voir à priori en quoi cette méthode permet de gagner du temps, mais j’imagine que c’est quelque chose qu’on ne peut constater qu’en pratiquant.

Cependant, comme d’autres avant moi, j’ai du mal avec cette histoire de katas. Ca me parait être une évidence qu’à force de refaire un travail répétitif, on finit nécessairement par trouver de quoi l’optimiser. Par contre la répétition n’aide sûrement pas à être créatif, si ? Du moins à mon sens la créativité est plutôt stimulée dans la variété et la contrainte.

Ttant qu’on y est dans la créativité, peut-on le faire sans stocker le résultat de tous les coups dans un tableau ? Au début de la partie "gérer les spar", je crois que C’est plutôt dans cette direction que je serais parti en premier… il va falloir que j’essaye, tiens.

+0 -0

Merci pour cette présentation de TDD. Elle répond aux questions que j’ai posées dans les commentaires de ton précédent billet. J’ai du mal à voir à priori en quoi cette méthode permet de gagner du temps, mais j’imagine que c’est quelque chose qu’on ne peut constater qu’en pratiquant.

Sur une bête fonction qui calcule un score de bowling, ce n’est peut-être pas aussi apparent que sur un composant un peu complexe qui fait intervenir plusieurs types d’objets et des interactions un peu plus compliquées. Mais le simple fait d’avoir un retour immédiat de l’état du code que l’on a écrit sans jamais avoir à le tester manuellement, de pouvoir refactoriser ce qu’on veut quand on veut sans craindre de casser ce qu’on a fait il y a une heure, c’est en soi un gain de temps gigantesque.

Ce gain de temps, tu peux le multiplier par toutes les fois où, quand ton code plante en prod, tu as une couverture de tests qui te permet de savoir quelle n’est pas la cause de l’erreur. Et aussi toutes les fois où il ne plantera en fait juste pas parce que ton code est entièrement couvert de tests unitaires, donc que tu as un niveau très élevé de confiance sur ce que tu envoies en prod.

L’autre avantage, c’est que ton code n’est jamais "à refactoriser" : ça ne prend plus aucun effort, donc il n’y a aucune excuse à ce que le code ne soit pas nickel et amélioré en continu chaque fois qu’on le touche. Et ça, ça engendre encore un autre gain de temps : celui de ne pas galérer à comprendre ce que tu relis 3 mois plus tard. Parce que ça a été commité bien écrit et parce que les tests documentent le code de façon hyper précise : ils montrent comment il s’utilise.

Cependant, comme d’autres avant moi, j’ai du mal avec cette histoire de katas. Ca me parait être une évidence qu’à force de refaire un travail répétitif, on finit nécessairement par trouver de quoi l’optimiser.

La principale qualité du kata, ce n’est pas d’être répétitif : si tu réalises un même kata deux fois de suite de la même façon, sans te poser plus de questions, sans prendre sciemment une décision différente, ça ne sert à rien. La répétition n’est qu’un artefact. Le but du jeu n’est pas de "répéter" mais d’accomplir le moindre geste/pensée délibérément (donc lentement au début). Ce n’est pas juste de l’optimisation comme quand on répète un geste dans tel ou tel sport : c’est un cadre pour expérimenter et progresser.

Par contre quand tu joues le jeu, ce que tu finis par répéter, en revanche, ce sont les gestes que tu gardes pendant longtemps d’une fois sur l’autre : ceux que tu as déjà remis en question et décidé depuis longtemps qu’ils sont la "bonne" façon de réagir.

Par contre la répétition n’aide sûrement pas à être créatif, si ? Du moins à mon sens la créativité est plutôt stimulée dans la variété et la contrainte.

Là, c’est le musicien qui te répond : la "créativité", ce n’est pas des idées qui poppent par magie dans ta tête. La créativité, c’est le luxe de prendre des risques mesurés, des décisions différentes, d’essayer des choses nouvelles. Pour être créatif en musique (et là je parle d’improvisation, donc on peut difficilement imaginer plus créatif comme activité), il faut deux choses :

  • S’entendre hurler ce que l’on veut jouer dans sa tête,
  • Avoir dans les mains toutes les gammes, tous les licks, toutes les techniques que l’on connaît, prêtes à sortir dans l’instrument au moment où on l’entend, de la façon dont on l’entend.

Le premier point se travaille notamment en écoutant activement beaucoup de musique et en imitant (répétition) les impros de ceux qui nous inspirent. Le second, c’est des heures et des années à répéter des gammes et des exercices. Littéralement. Et aussi à s’entraîner à jouer délibérément ce que l’on entend, sur une même grille d’accords, boucle après boucle, heure après heure.

Sans une assise technique extrêmement solide, on ne peut pas être créatif : on est trop parasité à essayer de maîtriser ce qu’on joue pour réussir à décider en même temps ce que l’on jouera la seconde d’après.

En programmation, c’est pareil. Pour être créatif, il faut déjà avoir des gestes sûrs et une méthode qui nous permet de mesurer les risques que l’on prend. Ce n’est que quand tout ça est inscrit dans notre tête comme autant de réflexes que l’on peut décider instantanément si une idée vaut le coup d’être essayée ou pas.

Autrement dit, paradoxalement, la répétition favorise carrément la créativité. Encore une fois, le kata te donne juste un cadre que tu connais par cœur pour expérimenter des choses, sans prêter à conséquences sur ton vrai travail : ce n’est pas lui qui nourrira ta créativité avec des idées nouvelles, par contre il te permet de les tester et de les comparer objectivement en mesurant leur impact sur le reste de ton raisonnement qui n’apparaît pas dans le code final, ce qu’aucun autre exercice ne permet à ma connaissance.

Ttant qu’on y est dans la créativité, peut-on le faire sans stocker le résultat de tous les coups dans un tableau ? Au début de la partie "gérer les spar", je crois que C’est plutôt dans cette direction que je serais parti en premier… il va falloir que j’essaye, tiens.

QuentinC

On peut, oui. Mais ça oblige à maintenir un état beaucoup plus compliqué qu’une simple liste de nombres. Enfin essaye, tu verras.

+0 -0

Bonsoir,

Merci pour cette présentation de TDD. Elle répond aux questions que j’ai posées dans les commentaires de ton précédent billet. J’ai du mal à voir à priori en quoi cette méthode permet de gagner du temps, mais j’imagine que c’est quelque chose qu’on ne peut constater qu’en pratiquant.

Quand tu écris un test, la première étape est de te demander ce que tu veux et te mettre dans une position où ton code va s’aligner avec ta vision

Dans le cas du Bowling Score, tu veux qu’il te donne le score quand N quilles sont tombées. En exprimant ce "je veux", tu vas poser des questions et clarifier ta compréhension du métier

En y allant par petites étapes, tu découpes un problème en tâches simples et successifs plutôt que sa globalité. Tu peux faire un parallèle un peu grossier avec l’Agile et le découpage de tes US en tâches.

En le faisant à deux ou plus, on ouvre la discussion pour s’assurer qu’on a la même compréhension du métier. Et c’est encore mieux

(Disclaimer pour le paragraphe suivant: c’est une version très réduite de l’idée du craft)

D’un point de vue un peu plus large, au-delà de la satisfaction du travail bien fait, le craft a pour objectif le delivery continu de valeur. Le TDD est un levier pour accélérer le Continuous Delivery et se concentrer sur la valeur en premier (i.e. les règles métier). Un bug, une régression, etc. cassent le flot. Construire des tests (et surtout les crafter) permet de réduire tous ces temps de bugfix, debug, etc.

Par contre, le TDD n’est pas si simple à maîtriser et comprendre : si la boucle du TDD est simple, comprendre les objectifs et la finalité du TDD demande beaucoup de pratique et c’est une excellente chose de le pratiquer en kata

Cependant, comme d’autres avant moi, j’ai du mal avec cette histoire de katas. Ca me parait être une évidence qu’à force de refaire un travail répétitif, on finit nécessairement par trouver de quoi l’optimiser. Par contre la répétition n’aide sûrement pas à être créatif, si ? Du moins à mon sens la créativité est plutôt stimulée dans la variété et la contrainte.

Le but des katas est de s’entraîner jusqu’à ce qu’une pratique, principe, etc. devienne un automatisme. Ca fonctionne pour les uns. Pour les autres peut-être moins. Mais ça vaut toujours le coup d’expérimenter

Il y a 2 choses dans un kata : ce que tu veux creuser (ici le TDD) et un sujet de kata (ici le Bowling Score). Tu n’es pas obligé de refaire le même sujet. Perso, j’aime bien le refaire une ou deux fois, quand même. Par contre, tu peux utiliser le même sujet pour différents principes.

Pour la créativité, je le pensais aussi au tout début. Maintenant, je dois avoir une dizaine de versions du Fizzbuzz (kata simple mais pas tant que ça) complètement différentes dont au moins 4 approches du principe Open/Closed.

Les katas sont aussi un environnement où tu peux faire de l’over-engineering, écrire du code que tu ne peux pas autoriser dans le code de prod ou simplement expérimenter sur ton langage favori.

Par contre, le TDD n’est pas si simple à maîtriser et comprendre : si la boucle du TDD est simple, comprendre les objectifs et la finalité du TDD demande beaucoup de pratique et c’est une excellente chose de le pratiquer en kata

C’est très vrai.

En adoptant le TDD sur mon projet au boulot, je me suis retrouvé plusieurs fois à beaucoup bafouiller au début, c’est-à-dire faire, défaire et refaire le début de mon design jusqu’à ce que ce soit clair dans ma tête. Ne maîtrisant pas encore ces ficelles, je me concentrais plus sur la boucle red/green/refactor que sur le code lui-même, et ça introduisait des difficultés supplémentaires par rapport à "avant".

Le fait de pratiquer le kata du bowling game m’a permis de ne pas remettre tout TDD en question pour autant. Celui-ci m’a permis de sentir ce que j’avais acquis et qui était solide, et le distinguer de ce qu’il me reste à travailler.

Et donc, pour travailler ce point (démarrer un composant complexe avec une interface non-triviale), j’utilise un autre kata de Uncle Bob qui fait intervenir cette étape : Args, à savoir designer et écrire un parseur d’arguments en ligne de commande.

+0 -0

Je voudrais apporter un contrepoint. Ça ne veut pas dire que je ne vois pas l’intérêt de ça, mais j’ai une façon de penser différente, donc je voulais confronter ça. Ça concerne surtout ce que je comprends de tes messages, @nohar, en réponse à Spacefox et QuentinC. Je préviens, ça risque d’être un peu brouillon.

Une idée fondamentale du kata, c’est, si j’ai bien compris, de réfléchir à notre pratique et se donner les moyens d’essayer des choses dans un environnement cadré pour voir si c’est efficace. Ce à quoi je réponds : comment peux-tu savoir si ça marche en condition réelle si tu essaies en condition cadrée ?

Précision nécessaire, je travaille dans l’info, mais dans des environnements d’un niveau de bordel incommensurable. Il y a un an, on compilait le logiciel Bidule sur le serveur Machin, parce que ailleurs, ça ne marchait pas, et on ne savait pas pourquoi. Et ça, c’est quelque chose sur lequel on a la main, il y a des problèmes du même niveau de bordel que l’on ne peut pas résoudre.

Puisque tu parles de gamme, je voudrais te parler de gamme en escrime. En gros, deux pratiquants font des mouvements prédéterminés pour les intégrés. Pour les choses ultra-simple, OK. Dans le sens deux mouvements (parade-riposte). Pour les choses complexes à 5 mouvements ou plus, je n’y crois pas. Parce que les jours où tu vas le tenter, en face, il va faire autre chose, donc te casser le truc. Tellement habitué à cet enchainement, tu risques de perdre tes moyens et de te faire toucher avant d’avoir récupéré. Ou d’être super efficace sur cet enchainement et toucher. Jusqu’à ce qu’il trouve la faille et que ça ne passe plus.

Vaut-il mieux apprendre à faire vite un truc précis, ou à gérer l’incertitude ? Dans l’idéale, les deux, mais le temps est limité. Ce n’est pas comme en musique où tu contrôles tout (de ce que je sais). Pour moi, coder s’approche de l’escrime dans le sens où je ne contrôle pas grand-chose, et je dois composer avec.

Ça tombe bien, l’essais-erreur, l’avancé en zigzag, c’est ma façon de faire et penser.

D’ailleurs, je voulais caler une anecdote d’escrime : pour travailler la technique, on a des exercices. On a bien un cadre, mais il est beaucoup plus souple que des gammes. Dans le cadre d’un exo, mon partenaire ayant du mal à faire sa défense, j’ai ralenti. L’instructeur passe par là, et me dit que le défenseur ne peut plus travailler correctement, car en ralentissant, je change le aussi le mouvement et donne quelque chose de faux, qui n’arriverait pas en combat, et tellement artificialisé que le mouvement de défense n’est plus adapté. Tout exercice passe, dans la séance même, progressivement de l’exo de base (déjà moins cadré qu’une gamme) à un assaut libre où on tente de placer la technique.

En assaut, on perd 80 % de son bagage technique. Si on ne pratique le bagage en question que dans le cadre d’exo, sans intermédiaire avec l’assaut réel, c’est pire encore. L’environnement controlé, c’est pour présenter la technique, pas s’entrainer à la réaliser.

Si je grossis le trait pour me faire comprendre, ton kata est tellement éloigné de ma vie d’informaticien que je ne saurai pas comment passer de l’exo gentil contrôlé à la vraie vie. Donc l’exo, à part une présentation, m’est inutile. Et surtout l’itérer encore et encore. Quand à réfléchir à chacun de mes micro-actes, à quoi bon, puisque 90 % d’entre eux ont pour vocation à appréhender l’espace dans lequel je me situe pour mieux le comprendre et non à réellement résoudre le problème ?

+1 -0

Une idée fondamentale du kata, c’est, si j’ai bien compris, de réfléchir à notre pratique et se donner les moyens d’essayer des choses dans un environnement cadré pour voir si c’est efficace. Ce à quoi je réponds : comment peux-tu savoir si ça marche en condition réelle si tu essaies en condition cadrée ?

Gabbro

Si tu me permets de répondre : tu peux faire un kata classique pour découvrir le concept. Mais effectivement, ce sera souvent très/trop loin du monde réel pour être utile. Par contre, peut-être que pour ton prochain projet, tu pourras le mettre en place.

Aujourd’hui, quand une équipe me demande de lui apprendre le TDD, je construis un kata à partir de son code et on le fait en pair/mob. C’est compliqué et souvent casse-gueule mais les devs sont moins perdus et ça rentre souvent mieux

Dans l’absolu, si tu n’es pas dans une organisation qui promeut ce genre de pratiques, le TDD aura un impact très limité.

En fait, d’expérience, la question à se poser n’est pas comment je peux introduire ce genre de technique dans mon travail ? mais plutôt quel est mon besoin ? Et la réponse est rarement le TDD…

@Gabbro : ok je vois ton point de vue. Je ne suis pas d’accord avec et je vais essayer de mettre le doigt sur ce qui nous différencie. Mais d’abord, permets-moi de commencer par un sophisme (enfin, non, mais ça y ressemble). :p

Une idée fondamentale du kata, c’est, si j’ai bien compris, de réfléchir à notre pratique et se donner les moyens d’essayer des choses dans un environnement cadré pour voir si c’est efficace. Ce à quoi je réponds : comment peux-tu savoir si ça marche en condition réelle si tu essaies en condition cadrée ?

C’est justement cette question qui a été la clé pour me faire comprendre ta vision des choses. Je suis très tenté de te répondre que ce n’est pas la bonne question, et que la question devrait plutôt être :

Comment peut-on croire que ça marche en conditions cadrées si ça ne marche pas en conditions réelles ?

Bien sûr, c’est ce que l’on désigne par "ça marche" qui change. Tu sembles penser que le but est d’apprendre et d’intérioriser un mouvement dans le cadre d’un exercice pour le réaliser parfaitement plus tard en conditions réelles. En fait, oui, c’est plutôt ça, mais pas de façon aussi directe que ça.

Au lieu de voir l’apprentissage de base d’un mouvement ou d’un enchaînement, place-toi plutôt dans le cadre du perfectionnement de tes réactions et de tes gestes.

Je vais prendre quelques exemples avant de transposer à l’info :

  • Au piano, quand je semble buter sur un enchaînement entre deux accords, mon prof me dit : "attends, tu as du mal à passer ce changement, tu vas me faire une boucle : deux mesures du premier, deux mesures du second, et tu t’entraînes à négocier tes phrases de l’un à l’autre, sans t’arrêter". En d’autres termes, il coupe un contexte cadré, ralentit le temps (le changement devient 4 fois plus lent), et me donne un cadre répétitif pour apprendre à négocier ce passage.
  • Quand je faisais de la natation en club, il arrivait que notre entraîneur vienne avec une caméra, nous filme et nous passe la vidéo pour qu’on regarde nos mouvements au ralenti. Quand quelque chose n’allait pas (par exemple un problème de synchro entre les jambes et la sortie de la tête de l’eau), il nous demandait de répéter ce mouvement, dans l’eau, pendant 100 mètres.

Ces exercices servent à corriger certains de nos mouvements, à prendre nos erreurs et à les examiner de près, puis lentement, délibérément, les corriger avant de réaccélérer progressivement.

  • On se donne un cadre maîtrisé pour travailler tranquillement, sans pression,
  • On reproduit le mouvement lentement pour trouver la cause de l’erreur, et on la corrige.

Tu bosses sur un projet, tu développes un composant pas trivial, t’avais prévu que ça prenne deux jours. Ça t’a pris la semaine, parce que tu as commis une erreur qui t’a fait perdre du temps. Comment tu réagis ?

  1. "On s’en fout, c’était juste pas de bol !"
  2. "J’essayerai d’y repenser la prochaine fois."
  3. "Y a un truc qui va pas, il faut que le corrige pour que ça ne se reproduise plus."

Si tu choisis 2. ou 3., tu peux continuer à lire. Sinon, il n’y a rien que je puisse te dire pour te convaincre. :)

Et si tu avais pris une mauvaise décision juste parce que tu ne t’étais pas posé une question importante beaucoup plus tôt ? Et si tu pouvais repasser ce moment de la résolution de ton ticket au ralenti, que tu changeais une décision, et que tu essayais de dérouler la suite de ton raisonnement pour voir ce que ça aurait fait ?

Pour comparer les résultats, et comprendre vraiment les implications de ces décisions, la seule chose que tu puisses faire est de te découper un cadre autour de cette décision, et d’examiner le chemin que ça te fait prendre in situ. Au ralenti. Délibérément. Trouver le noeud du problème, trouver la bonne question à te poser au bon moment, et la faire rentrer pour que ça devienne un réflexe dans ces conditions.

Alors certes, comme tu le dis, on ne maîtrise pas tout sur nos projets. Mais la façon dont on réagit est quelque chose sur laquelle on peut exercer un contrôle. Et pour bosser ça on a besoin d’un cadre pour évoluer au ralenti.

Et un kata n’est rien d’autre que ça.

+3 -0

Comment peut-on croire que ça marche en conditions cadrées si ça ne marche pas en conditions réelles ?

Tu es sûr de ne pas avoir fait d’erreur ici ? Parce que, de mon point de vue, la question « Comment peut-on croire que ça marche en conditions réelles si ça ne marche pas en conditions cadrées ? » a un sens, la tienne, moins. Parce que des choses qui dans marchent en exercice mais échouent lamentablement en vrai… ben, c’est la norme, non ?

Pour reprendre la natation, je sais nager en condition cadrée (piscine), moins bien en condition réelle (aller sauver quelqu’un qui se noie, nager dans une rivière avec un courant…). Pour que le TDD marche, il faut que le code soit testable, etc. On pose un cadre pour faciliter l’opération, donc sans la facilité, ça peut plus facilement échouer.

Dans tout tes exemples, tu procèdes à l’envers du kata (dans la manière dont je le comprends) : tu identifies un problème d’abord, puis tu ponds un exercice à même de le mettre en exacerbe ou l’isoler pour le travailler spécifiquement. Le kata n’est pas spécifique, il ne répond pas un problème préalablement identifié. Tes profs identifiaient un problème précis, chez toi en particulier, et cadrait l’exercice. Là, on te poses un problème, et au mieux tu dois toi-même chercher les points de blocage. Une bonne analogie, ce serait plutôt de proposer à un collègue de résoudre tel exercice / kata, en utilisant le TDD, les classes abstraites, ou que sais-je sur quoi il bloque, pour lui faire mieux comprendre le concept.

Ensuite, il y a autre chose dans de ce que je dis : un exercice lent n’a de sens que si on peut derrière le mettre en pratique, ce qui signifie typiquement un exercice intermédiaire pour faire le pont. En tout cas, chez moi, que ce soit pour les choses intellectuelles ou physiques.

Pour ma réaction, ce n’est aucun des trois :

  1. Faire des erreurs est normal, et fait partie du processus de résolution des problèmes.

Ce n’est pas « pas de bol », puisque ce sera pareil a prochaine fois, mais ce sera un autre problème. J’y repense si je rencontre le même problème (en allant fouiller ma mémoire ou des notes — le problème a intérêt de ne pas être vieux), ou je me corrige vraiment s’il apparait 2–3 fois. Mais par défaut, c’est normal, et ça va dans un coin de ma tête, pas plus.

Et si tu avais pris une mauvaise décision juste parce que tu ne t’étais pas posé une question importante beaucoup plus tôt ? Et si tu pouvais repasser ce moment de la résolution de ton ticket au ralenti, que tu changeais une décision, et que tu essayais de dérouler la suite de ton raisonnement pour voir ce que ça aurait fait ?

Ça ne marche pas, je connais la solution. Vraiment, je veux dire, je pars de A, passe par B, ne sais pas vraiment où je vais (à C, en l’occurrence). C’est le cas réel. Je pars de A, je décide de passer par D, mais je sais que vais à C. C’est l’exercice. Comment veux-tu comparer les deux situations ? Je sais où je vais !

Ça me rappelle quand j’avais interrogé une prof de langue sur comment m’améliorer sur un point de grammaire que je ne comprenais pas bien. Sa réponse : refais les exercices vus en cours. La mienne : je me souviens de la réponse, donc je peux remplir l’exercice de façon juste sans rien comprendre. Ça ne m’aide pas.

On ne pense vraiment pas pareil, je crois. C’est plutôt amusant, de mon point de vue. ^^ Ça me rappelle des discussions que j’ai eu avec des perfectionnistes. Pour moi, l’humain est doué pour prendre rapidement des décisions (la plupart du temps) suffisamment juste. Prendre des bonnes décisions, rationnelles, pesées, etc, c’est long, et l’humain est prodigieusement inefficace dans la chose.

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte