#6 Pour bien faire du Lean il faut du Takt
La fête du travail, fériée ? On aurait pas un problème de nommage là ?
⚠️ Lisez cette lettre dans votre navigateur pour profiter des corrections de dernière minute
Les liens marqués avec 🔒 sont accessibles seulement aux membres de M33
Dans la méthodologie Lean le Takt Time est le cœur battant d’une production bien huilée.
Pôle Craft
Kanban des Gemba
J’avais déjà évoqué les Gemba dans la newsletter #2. Avec Shoun notre Engineering Manager nous avons sollicité Clément D afin de mettre en place un Kanban pour organiser la rotation de chaque équipe.
Ce Kanban nous permettra de mieux tirer le follow-up.
Indicateur DDD
Depuis le début je vous en parle 😇
L’indicateur DDD a évolué pour se centrer autour d’un parseur d’AST qui extrait les identifieurs du code source afin de les comparer au langage ubiquitaire.
Ce parseur est l’occasion pour moi de publier mon tout premier package Node ❤️
Faisons un point sur les KPI :
Durée d’exéction
Ma durée d’exécution cible était de 0,1s ou moins.
$ time make ddd
...
make ddd 0,77s user 0,14s system 141% cpu 0,646 total
Nous sommes bien au dessus du seuil.
Echouer à atteindre un objectif est l’occasion de s’interroger sur la solution mais aussi sur l’objectif lui-même.
En l’occurrence la solution apporte quelques avantages : elle permet d’éliminer un bug et offre une plus grande souplesse d’analyse.
J’avais choisi la valeur de 0,1s car elle est en dessous du seuil de perception de la plupart des gens. Mais une durée d’exécution inférieure à la seconde est tout à fait supportable.
Je décide donc de ne pas trop m’inquiéter de cet indicateur pour le moment et de le réévaluer avec l’aide des tech-leads pilotes.
Langages supportés
L’objectif est de supporter Java, Kotlin, Typescript. Pour le moment seul Java est supporté car c’est le langage le plus représenté dans le code que je veux analyser.
J’ai des pistes pour supporter d’autres langages et je ne m’inquiète pas de ne pas avoir encore atteint l’objectif.
Améliorer sa pratique
📚 Book Club Camunda
Tous les 3è vendredi du mois à 17h15 se tient le Book Club Camunda. The place to be pour poser des questions aux experts, faire challenger ses modélisation de processus métier etc.
MultipleBagFetchException
Dans la terminologie de Hibernate, un “bag” est une collection non ordonnée et sans garantie d’unicité. Dans les entités JPA ce sont les Collection / List annotées avec @*ToMany
.
Cette exception semble nous dire que c’est une erreur de récupérer plusieurs “bags”.
Supposons une base de données relationnelle qui représente un article avec une liste de tags et une liste de commentaires.
En JPA on représenterait ces relations avec 3 entités :
@Entity
public class Article {
@Id
private UUID id;
@OneToMany(mappedBy = "article")
private List<Comment> comments;
@OneToMany(mapperBy = "article")
private List<Tag> tags;
...
}
@Entity
public class Comment {
@Id
private UUID id;
@ManyToOne
private Article article;
...
}
@Entity
public class Tag {
@Id
private UUID id;
@ManyToOne
private Article article;
...
}
Supposons qu’un article ait 2 commentaires et 2 tags. Si vous voulez récupérer le tout avec une requête SQL vous allez écrire quelque chose comme
select article.id, comment.id, tag.id
from article
join commentaire on commentaire.article_id = article.id
join tag on tag.article_id = article.id
where article.id = '...';
Et là on récupère 4 lignes. Quand on y pense c’est bien normal, il s’agit du produit cartésion des id d’article, des id de commentaire et des id de tag.
On a heureusement réduit le cardinal des id d’article à 1 grace à la clause “where
” et à 2 celui des deux autres ids avec la clause “on
”.
1 x 2 x 2 = 4
Ici les dégats sont limités, 4 lignes ça reste tout à fait gérable. Mais généralement on cherche à éviter autant que possible les produits cartésiens dans une requête SQL car ils peuvent facilement provoquer de gros problèmes de performances.
La façon dont Hibernate s’y prend c’est de lever une MultipleBagFetchException lorsqu’il détecte le problème.
La solution pour s’en sortir est de faire deux requêtes distinctes : une pour récupérer les commentaires, une autre pour les tags.
Pour faire ça, le plus simple est de configurer le lazy-loading des relations @*ToMany
. Ainsi on ira chercher l’une ou l’autre liste uniquement lorsque nécessaire. Cette solution est simple car c’est la configuration par défaut de JPA.
Pour plus de détails je vous propose cet excellent article de Vlad Mihalcea
Maven
La dernière fois nous avons vu le fonctionnement des cycles de vie de Maven.
Un cycle de vie est une série de phases. Chaque phase active un goal d’un ou plusieurs plugins.
On a vu aussi que par défaut certains phases du cycle de vie ont déjà un goal attaché.
Par exemple, même si vous n’avez configuré aucun plugin dans votre POM, lorsque vous tapez la commande “mvn test”
votre code Java sera compilé et vos tests JUnit seront exécutés.
Surefire
La documentation de Surefire nous rappelle que ce plugin n’a qu’un seul goal
surefire:test runs the unit tests of an application.
Ce plugin est conçu pour exécuter les tests unitaires de votre application.
Il est capable de détecter automatiquement la présence de JUnit3, 4 ou 5 et TestNG dans le classpath (dans la terminologie de Surefire on les appelle des providers).
Par défaut, Surefire délègue au providers l’exécution des tests des classes dont le nom est suffixé par “Test”.
Vous pouvez avoir plusieurs providers dans votre classpath. Par exemple si vous avez à la fois JUnit4 et JUnit5 dans votre classpath, les méthodes annotées avec @org.junit.Test
seront exécutées par JUnit4 tandis que celles annotées avec @org.junit.jupiter.api.Test
seront exécutées par JUnit5.
Bien sûr mélanger plusieurs providers dans un même projet c’est chercher les ennuis 😉
Mais attendez, on a dit que Surefire était conçu pour exécuter les tests unitaires, mais dans mon projet je peux aussi avoir des tests d’intégration ou des tests de comportement (behavior) !
Failsafe
Le cycle de vie par défaut de Maven inclut une phase “integration-test” qui n’a pas de plugin par défaut.
Cependant il existe un plugin qui s’attache par défaut à la phase “integration-test”, il s’agit de Failsafe.
Failsafe est le petit frère (ou le grand frère ?) de Surefire. En fait ils partagent le même repository et pas mal de code.
Par défaut, Failsafe délègue au providers l’exécution des tests des classes dont le nom est suffixé par “IT”.
Ce plugin a 4 goals :
integration-test
pour exécuter les tests d’intégrationverify
pour vérifier le résultat des tests (cf ci-dessous)pre-integration-test
etpost-integration-test
pour le setup / teardown éventuel de l’environnement de test
Lorsque Surefire rencontre un test en erreur, il signale l’erreur à Maven qui stoppe l’exécution du cycle de vie. Alors que Failsafe est stoïque. Lorsqu’il rencontre un test en erreur il le note pour plus tard et continue le cycle de vie.
Ce sang froid permet de garantir l’exécution de la phase post-integration-test
qui libère les ressources allouées dans la phase pre-integration-test
.
Ensuite la phase verify
entre en jeu pour vérifier le résultat des tests et signaler à Maven une erreur éventuelle.
Pour cette raison lorsqu’on veut exécuter les tests d’intégration, on n’invoque pas “mvn integration-test”
mais plutôt “mvn verify”
.
Ainsi avec Failsafe vous pouvez démarrer vos BDD mémoire ou conteneurisées ou vos mocks d’API lors de la phase pre-integration-test
, puis les libérer dans la phase post-integration-test
.
Les plugins Surefire et Failsafe s’exécutent (par défaut) dans des JVM forkées ce qui permet de configurer finement les ressources disponibles pour chaque type de test.
Veille
Avec son enthousiasme communicatif, Nicolai Parlog nous présente le JEP 443 qui s’avérera bien utile à mesure que le pattern matching fait son chemin dans le langage
Kotlin 1.8.20 est sorti !
Puzzler du jour
Réponse au challenge précédent
La dernière fois je vous ai proposé ce code Java qui produit un résultat inattendu pour le béotien :
import java.time.*;
class DateTimeWeirdness {
public static void main(String[] args) {
ZonedDateTime twoThirtyInTheMorning = ZonedDateTime
.of(2023, 3, 26, 2, 30, 0, 0, ZoneId.of("Europe/Paris"));
ZonedDateTime threeInTheMorning = ZonedDateTime
.of(2023, 3, 26, 3, 0, 0, 0, ZoneId.of("Europe/Paris"));
boolean right = twoThirtyInTheMorning.isBefore(threeInTheMorning);
System.out.printf("2:30 AM is before 3:00 AM, right ? %b", right);
}
}
Le code affiche : 2:30 AM is before 3:00 AM, right ? false
alors que notre intuition nous souffle que 2h30 est bien avant 3h
La subtilité bien sûr était sur la date et la zone qui n’ont pas été choisies au hasard : le 26 mars 2023 dans la zone de Paris est le jour du passage en heure d’été !
Or en heure d’été, souvenez-vous, “à 2h il sera 3h” autrement dit, la plage de 2h à 3h le 26 mars 2023 à Paris n’existe pas (c’est un “gap”). Et dans ce cas la spécification de ZonedDateTime
est explicite :
For Gaps, the general strategy is that if the local date-time falls in the middle of a Gap, then the resulting zoned date-time will have a local date-time shifted forwards by the length of the Gap, resulting in a date-time in the later offset, typically "summer" time.
Donc lorsque vous écrivez un ZonedDateTime
à 2h30 :
ZonedDateTime twoThirtyInTheMorning = ZonedDateTime
.of(2023, 3, 26, 2, 30, 0, 0, ZoneId.of("Europe/Paris"));
Vous obtenez un objet qui est égal à un ZonedDateTime
à 3h30 :
ZonedDateTime threeThirtyInTheMorning = ZonedDateTime
.of(2023, 3, 26, 3, 30, 0, 0, ZoneId.of("Europe/Paris"));
Vous pouvez vous en convaincre avec cette variante :
import java.time.*;
class DateTimeWeirdness {
public static void main(String[] args) {
ZonedDateTime twoThirtyInTheMorning = ZonedDateTime
.of(2023, 3, 26, 2, 30, 0, 0, ZoneId.of("Europe/Paris"));
ZonedDateTime threeThirtyInTheMorning = ZonedDateTime
.of(2023, 3, 26, 3, 30, 0, 0, ZoneId.of("Europe/Paris"));
boolean right = twoThirtyInTheMorning.equals(threeThirtyInTheMorning);
System.out.printf("2:30 AM is equal to 3:30 AM, right ? %b", right);
}
}
Challenge du jour
Aujourd’hui on va se demander pourquoi c’est Mal™ d’invoquer une méthode overridable dans un constructeur.
L’exemple est en Kotlin mais il fonctionne pareillement en Java. Savez-vous prédire ce qu’il affiche ?
class Label(var text: String)
abstract class Parent(title: String) {
init {
setTitle(title)
}
abstract fun setTitle(title: String)
}
class Child: Parent("titre") {
val label = Label("sans titre")
override fun setTitle(title: String) {
label.text = title
}
}
fun main() {
val child = Child()
println(child.label.text)
}
Et bien sûr la question la plus intéressante : pourquoi ?